diff options
| author | Christophe Simonis <chs@odoo.com> | 2019-06-06 11:54:35 +0200 |
|---|---|---|
| committer | Christophe Simonis <chs@odoo.com> | 2019-06-06 11:54:35 +0200 |
| commit | 71a50a2214748fec413c5a0f042d0e7172ae6ea9 (patch) | |
| tree | 2e9ee11551350e23923390d9c53baf630b6ed834 | |
| parent | a7621137cb0d75ec545152d5e1d413c9d3548e45 (diff) | |
| parent | 409679866b2ee8352a8cb27460e7332563526924 (diff) | |
[MERGE] forward port branch saas-12.3 up to 409679866b2
258 files changed, 7196 insertions, 995 deletions
diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000000..98eb36db26e --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,40 @@ +# Security Policy + +## Supported Versions + +| Version | Supported | +| ------- | ------------------ | +| 12.0 | :white_check_mark: | +| 11.0 | :white_check_mark: | +| 10.0 | :white_check_mark: | +| <=9.0 | :x: | + +## Reporting a Vulnerability + +Please share privately the details of your security vulnerability by contacting our Security Team: +[Contact Info](https://www.odoo.com/security-report) + +Make sure to include as much information as possible, with the detailed steps to reproduce the problem, +the versions that are affected, the expected results and actual results, and any other information that +might help us react faster and more efficiently. + +We tend to prefer _text-based descriptions_ accompanied with a proof-of-concept script/exploit, rather +than screenshots and videos. + +Our [Responsible Disclosure](https://www.odoo.com/security-report) page gives an overview of the +process, including: + + - Our Incident Response Procedure (what will happen after you report an issue) + - Our Rules (what you can and cannot do while researching security issues) + - Guidelines with **DO REPORT** and **DO NOT REPORT** issues + (what kind of issues will be accepted/rejected) + + +## Important note + +We receive a majority of security reports that have little to no impact on the security of Odoo or +the Odoo Cloud, and we ultimately have to reject them. To avoid a disappointing experience when +contacting us, please try to put together a proof-of-concept attack and take a critical look at +what's really at risk. +If the proposed attack scenario turns out unrealistic, your report will probably be rejected. +Also be sure to review our list of [non-qualifying issues](https://www.odoo.com/security-report#what). diff --git a/addons/account/models/account.py b/addons/account/models/account.py index 0931f8ebd3d..6be56cd4795 100644 --- a/addons/account/models/account.py +++ b/addons/account/models/account.py @@ -1067,18 +1067,20 @@ class AccountTax(models.Model): @api.model def default_get(self, vals): - rslt = super(AccountTax, self).default_get(vals) + # company_id is added so that we are sure to fetch a default value from it to use in repartition lines, below + rslt = super(AccountTax, self).default_get(vals + ['company_id']) + company_id = rslt.get('company_id') if 'refund_repartition_line_ids' in vals: rslt['refund_repartition_line_ids'] = [ - (0, 0, { 'repartition_type': 'base', 'factor_percent': 100.0, 'tag_ids': [], 'company_id': rslt.get('company_id')}), - (0, 0, { 'repartition_type': 'tax', 'factor_percent': 100.0, 'tag_ids': [], 'company_id': rslt.get('company_id')}), + (0, 0, { 'repartition_type': 'base', 'factor_percent': 100.0, 'tag_ids': [], 'company_id': company_id}), + (0, 0, { 'repartition_type': 'tax', 'factor_percent': 100.0, 'tag_ids': [], 'company_id': company_id}), ] if 'invoice_repartition_line_ids' in vals: rslt['invoice_repartition_line_ids'] = [ - (0, 0, { 'repartition_type': 'base', 'factor_percent': 100.0, 'tag_ids': [], 'company_id': rslt.get('company_id')}), - (0, 0, { 'repartition_type': 'tax', 'factor_percent': 100.0, 'tag_ids': [], 'company_id': rslt.get('company_id')}), + (0, 0, { 'repartition_type': 'base', 'factor_percent': 100.0, 'tag_ids': [], 'company_id': company_id}), + (0, 0, { 'repartition_type': 'tax', 'factor_percent': 100.0, 'tag_ids': [], 'company_id': company_id}), ] return rslt @@ -1174,7 +1176,8 @@ class AccountTax(models.Model): str(invoice_tax_val['account_id']) + '-' + \ str(invoice_tax_val['account_analytic_id']) + '-' + \ str(invoice_tax_val.get('analytic_tag_ids', [])) + '-' + \ - str(invoice_tax_val.get('tax_ids') or []) + str(invoice_tax_val.get('tax_ids') or []) + '-' + \ + str(invoice_tax_val.get('tag_ids') or []) def _compute_amount(self, base_amount, price_unit, quantity=1.0, product=None, partner=None): """ Returns the amount of a single tax. base_amount is the actual amount on which the tax is applied, which is @@ -1235,6 +1238,10 @@ class AccountTax(models.Model): all_taxes += tax return all_taxes + def get_tax_tags(self, is_refund, repartition_type): + rep_lines = self.mapped(is_refund and 'refund_repartition_line_ids' or 'invoice_repartition_line_ids') + return rep_lines.filtered(lambda x: x.repartition_type == repartition_type).mapped('tag_ids') + @api.multi def compute_all(self, price_unit, currency=None, quantity=1.0, product=None, partner=None, is_refund=False): """ Returns all information required to apply taxes (in self + their children in case of a tax group). @@ -1392,12 +1399,23 @@ class AccountTax(models.Model): if tax.price_include and not total_included_checkpoints.get(i): cumulated_tax_included_amount += tax_amount + # If the tax affects the base of subsequent taxes, its tax move lines must + # receive the base tags and tag_ids of these taxes, so that the tax report computes + # the right total + subsequent_taxes = self.env['account.tax'] + subsequent_tags = self.env['account.account.tag'] + if tax.include_base_amount: + subsequent_taxes = taxes[i+1:] + subsequent_tags = subsequent_taxes.get_tax_tags(is_refund, 'base') + + # Compute the tax lines tax_repartition_lines = is_refund and tax.refund_repartition_line_ids or tax.invoice_repartition_line_ids repartition_lines_to_treat = len(tax_repartition_lines) total_amount = 0 for repartition_line in tax_repartition_lines.filtered(lambda x: x.repartition_type == 'tax'): # In case some rounding error occurs, we compensate for it on the last line line_amount = round(sign * tax_amount * repartition_line.factor if repartition_lines_to_treat != 1 else tax_amount - total_amount, prec) + taxes_vals.append({ 'id': tax.id, 'name': partner and tax.with_context(lang=partner.lang).name or tax.name, @@ -1409,7 +1427,8 @@ class AccountTax(models.Model): 'price_include': tax.price_include, 'tax_exigibility': tax.tax_exigibility, 'tax_repartition_line_id': repartition_line.id, - 'tag_ids': [(6, False, repartition_line.tag_ids.ids)], + 'tag_ids': [(6, False, (repartition_line.tag_ids + subsequent_tags).ids)], + 'tax_ids': [(6, False, subsequent_taxes.ids)] }) total_amount += line_amount diff --git a/addons/account/models/account_bank_statement.py b/addons/account/models/account_bank_statement.py index 8cea4c9bd92..7537227fdae 100644 --- a/addons/account/models/account_bank_statement.py +++ b/addons/account/models/account_bank_statement.py @@ -446,7 +446,6 @@ class AccountBankStatementLine(models.Model): """ Prepare the dict of values to balance the move. :param recordset move: the account.move to link the move line - :param dict move: a dict of vals of a account.move which will be created later :param float amount: the amount of transaction that wasn't already reconciled """ company_currency = self.journal_id.company_id.currency_id @@ -454,23 +453,19 @@ class AccountBankStatementLine(models.Model): st_line_currency = self.currency_id or statement_currency amount_currency = False st_line_currency_rate = self.currency_id and (self.amount_currency / self.amount) or False - if isinstance(move, dict): - amount_sum = sum(x[2].get('amount_currency', 0) for x in move['line_ids']) - else: - amount_sum = sum(x.amount_currency for x in move.line_ids) # We have several use case here to compare the currency and amount currency of counterpart line to balance the move: if st_line_currency != company_currency and st_line_currency == statement_currency: # company in currency A, statement in currency B and transaction in currency B # counterpart line must have currency B and correct amount is inverse of already existing lines - amount_currency = -amount_sum + amount_currency = -sum([x.amount_currency for x in move.line_ids]) elif st_line_currency != company_currency and statement_currency == company_currency: # company in currency A, statement in currency A and transaction in currency B # counterpart line must have currency B and correct amount is inverse of already existing lines - amount_currency = -amount_sum + amount_currency = -sum([x.amount_currency for x in move.line_ids]) elif st_line_currency != company_currency and st_line_currency != statement_currency: # company in currency A, statement in currency B and transaction in currency C # counterpart line must have currency B and use rate between B and C to compute correct amount - amount_currency = -amount_sum/st_line_currency_rate + amount_currency = -sum([x.amount_currency for x in move.line_ids])/st_line_currency_rate elif st_line_currency == company_currency and statement_currency != company_currency: # company in currency A, statement in currency B and transaction in currency A # counterpart line must have currency B and amount is computed using the rate between A and B @@ -479,8 +474,9 @@ class AccountBankStatementLine(models.Model): # last case is company in currency A, statement in currency A and transaction in currency A # and in this case counterpart line does not need any second currency nor amount_currency - aml_dict = { + return { 'name': self.name, + 'move_id': move.id, 'partner_id': self.partner_id and self.partner_id.id or False, 'account_id': amount >= 0 \ and self.statement_id.journal_id.default_credit_account_id.id \ @@ -491,71 +487,22 @@ class AccountBankStatementLine(models.Model): 'currency_id': statement_currency != company_currency and statement_currency.id or (st_line_currency != company_currency and st_line_currency.id or False), 'amount_currency': amount_currency, } - if isinstance(move, self.env['account.move'].__class__): - aml_dict['move_id'] = move.id - return aml_dict - @api.multi def fast_counterpart_creation(self): """This function is called when confirming a bank statement and will allow to automatically process lines without going in the bank reconciliation widget. By setting an account_id on bank statement lines, it will create a journal entry using that account to counterpart the bank account """ - payment_list = [] - move_list = [] - account_type_receivable = self.env.ref('account.data_account_type_receivable') - already_done_stmt_line_ids = [a['statement_line_id'][0] for a in self.env['account.move.line'].read_group([('statement_line_id', 'in', self.ids)], ['statement_line_id'], ['statement_line_id'])] - managed_st_line = [] for st_line in self: # Technical functionality to automatically reconcile by creating a new move line - if st_line.account_id and not st_line.id in already_done_stmt_line_ids: - managed_st_line.append(st_line.id) - # Create payment vals - total = st_line.amount - payment_methods = (total > 0) and st_line.journal_id.inbound_payment_method_ids or st_line.journal_id.outbound_payment_method_ids - currency = st_line.journal_id.currency_id or st_line.company_id.currency_id - partner_type = 'customer' if st_line.account_id.user_type_id == account_type_receivable else 'supplier' - payment_list.append({ - 'payment_method_id': payment_methods and payment_methods[0].id or False, - 'payment_type': total > 0 and 'inbound' or 'outbound', - 'partner_id': st_line.partner_id.id, - 'partner_type': partner_type, - 'journal_id': st_line.statement_id.journal_id.id, - 'payment_date': st_line.date, - 'state': 'reconciled', - 'currency_id': currency.id, - 'amount': abs(total), - 'communication': st_line._get_communication(payment_methods[0] if payment_methods else False), - 'name': st_line.statement_id.name or _("Bank Statement %s") % st_line.date, - }) - - # Create move and move line vals - move_vals = st_line._prepare_reconciliation_move(st_line.statement_id.name) - aml_dict = { + if st_line.account_id and not st_line.journal_entry_ids.ids: + vals = { 'name': st_line.name, 'debit': st_line.amount < 0 and -st_line.amount or 0.0, 'credit': st_line.amount > 0 and st_line.amount or 0.0, 'account_id': st_line.account_id.id, - 'partner_id': st_line.partner_id.id, - 'statement_line_id': st_line.id, } - st_line._prepare_move_line_for_currency(aml_dict, st_line.date or fields.Date.context_today()) - move_vals['line_ids'] = [(0, 0, aml_dict)] - balance_line = self._prepare_reconciliation_move_line(move_vals, st_line.amount) - move_vals['line_ids'].append((0, 0, balance_line)) - move_list.append(move_vals) - - # Creates - payment_ids = self.env['account.payment'].create(payment_list) - for payment_id, move_vals in zip(payment_ids, move_list): - for line in move_vals['line_ids']: - line[2]['payment_id'] = payment_id.id - move_ids = self.env['account.move'].create(move_list) - move_ids.post() - - for move, st_line, payment in zip(move_ids, self.browse(managed_st_line), payment_ids): - st_line.write({'move_name': move.name}) - payment.write({'payment_reference': move.name}) + st_line.process_reconciliation(new_aml_dicts=[vals]) def _get_communication(self, payment_method_id): return self.name or '' @@ -683,6 +630,8 @@ class AccountBankStatementLine(models.Model): # Create move line(s). Either matching an existing journal entry (eg. invoice), in which # case we reconcile the existing and the new move lines together, or being a write-off. if counterpart_aml_dicts or new_aml_dicts: + st_line_currency = self.currency_id or statement_currency + st_line_currency_rate = self.currency_id and (self.amount_currency / self.amount) or False # Create the move self.sequence = self.statement_id.line_ids.ids.index(self.id) + 1 @@ -705,12 +654,32 @@ class AccountBankStatementLine(models.Model): # Complete dicts to create both counterpart move lines and write-offs to_create = (counterpart_aml_dicts + new_aml_dicts) + company = self.company_id date = self.date or fields.Date.today() for aml_dict in to_create: aml_dict['move_id'] = move.id aml_dict['partner_id'] = self.partner_id.id aml_dict['statement_line_id'] = self.id - self._prepare_move_line_for_currency(aml_dict, date) + if st_line_currency.id != company_currency.id: + aml_dict['amount_currency'] = aml_dict['debit'] - aml_dict['credit'] + aml_dict['currency_id'] = st_line_currency.id + if self.currency_id and statement_currency.id == company_currency.id and st_line_currency_rate: + # Statement is in company currency but the transaction is in foreign currency + aml_dict['debit'] = company_currency.round(aml_dict['debit'] / st_line_currency_rate) + aml_dict['credit'] = company_currency.round(aml_dict['credit'] / st_line_currency_rate) + elif self.currency_id and st_line_currency_rate: + # Statement is in foreign currency and the transaction is in another one + aml_dict['debit'] = statement_currency._convert(aml_dict['debit'] / st_line_currency_rate, company_currency, company, date) + aml_dict['credit'] = statement_currency._convert(aml_dict['credit'] / st_line_currency_rate, company_currency, company, date) + else: + # Statement is in foreign currency and no extra currency is given for the transaction + aml_dict['debit'] = st_line_currency._convert(aml_dict['debit'], company_currency, company, date) + aml_dict['credit'] = st_line_currency._convert(aml_dict['credit'], company_currency, company, date) + elif statement_currency.id != company_currency.id: + # Statement is in foreign currency but the transaction is in company currency + prorata_factor = (aml_dict['debit'] - aml_dict['credit']) / self.amount_currency + aml_dict['amount_currency'] = prorata_factor * self.amount + aml_dict['currency_id'] = statement_currency.id # Create write-offs for aml_dict in new_aml_dicts: @@ -760,36 +729,6 @@ class AccountBankStatementLine(models.Model): counterpart_moves.assert_balanced() return counterpart_moves - @api.multi - def _prepare_move_line_for_currency(self, aml_dict, date): - self.ensure_one() - company_currency = self.journal_id.company_id.currency_id - statement_currency = self.journal_id.currency_id or company_currency - st_line_currency = self.currency_id or statement_currency - st_line_currency_rate = self.currency_id and (self.amount_currency / self.amount) or False - company = self.company_id - - if st_line_currency.id != company_currency.id: - aml_dict['amount_currency'] = aml_dict['debit'] - aml_dict['credit'] - aml_dict['currency_id'] = st_line_currency.id - if self.currency_id and statement_currency.id == company_currency.id and st_line_currency_rate: - # Statement is in company currency but the transaction is in foreign currency - aml_dict['debit'] = company_currency.round(aml_dict['debit'] / st_line_currency_rate) - aml_dict['credit'] = company_currency.round(aml_dict['credit'] / st_line_currency_rate) - elif self.currency_id and st_line_currency_rate: - # Statement is in foreign currency and the transaction is in another one - aml_dict['debit'] = statement_currency._convert(aml_dict['debit'] / st_line_currency_rate, company_currency, company, date) - aml_dict['credit'] = statement_currency._convert(aml_dict['credit'] / st_line_currency_rate, company_currency, company, date) - else: - # Statement is in foreign currency and no extra currency is given for the transaction - aml_dict['debit'] = st_line_currency._convert(aml_dict['debit'], company_currency, company, date) - aml_dict['credit'] = st_line_currency._convert(aml_dict['credit'], company_currency, company, date) - elif statement_currency.id != company_currency.id: - # Statement is in foreign currency but the transaction is in company currency - prorata_factor = (aml_dict['debit'] - aml_dict['credit']) / self.amount_currency - aml_dict['amount_currency'] = prorata_factor * self.amount - aml_dict['currency_id'] = statement_currency.id - def _check_invoice_state(self, invoice): if invoice.state == 'in_payment' and all([payment.state == 'reconciled' for payment in invoice.mapped('payment_move_line_ids.payment_id')]): invoice.write({'state': 'paid'}) diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py index 47c0cb42b27..8770b232808 100644 --- a/addons/account/models/account_invoice.py +++ b/addons/account/models/account_invoice.py @@ -879,7 +879,7 @@ class AccountInvoice(models.Model): 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 + type = self.type or self.env.context.get('type', 'out_invoice') if p: rec_account = p.property_account_receivable_id pay_account = p.property_account_payable_id @@ -1081,7 +1081,7 @@ class AccountInvoice(models.Model): else: return self.env.ref('account.invoice_form').id - def _prepare_tax_line_vals(self, line, tax, tax_ids): + def _prepare_tax_line_vals(self, line, tax): ''' Prepare values to create an account.invoice.tax line. :param line: An account.invoice.line record. :param tax: Tax values outputted by compute_all() in account.tax. @@ -1099,8 +1099,9 @@ class AccountInvoice(models.Model): 'account_analytic_id': tax['analytic'] and line.account_analytic_id.id or False, 'account_id': tax['account_id'] or line.account_id.id, 'analytic_tag_ids': tax['analytic'] and line.analytic_tag_ids.ids or False, - 'tax_ids': tax_ids and [(6, None, tax_ids)] or False, 'tax_repartition_line_id': tax.get('tax_repartition_line_id'), # For base amount, we let this field empty + 'tag_ids': tax['tag_ids'], + 'tax_ids': tax['tax_ids'], } # If the taxes generate moves on the same financial account as the invoice line, @@ -1130,7 +1131,6 @@ class AccountInvoice(models.Model): price_unit = line.price_unit * (1 - (line.discount or 0.0) / 100.0) taxes = line.invoice_line_tax_ids._origin.compute_all(price_unit, self.currency_id, line.quantity, line.product_id, self.partner_id, is_refund=self.type in ('in_refund', 'out_refund'))['taxes'] - affecting_base_tax_ids = [] for tax_vals in taxes: # Retrieve the tax record (not in tax_vals when dealing with group of taxes). if tax_map.get(tax_vals['id']): @@ -1138,7 +1138,7 @@ class AccountInvoice(models.Model): else: tax = tax_map[tax_vals['id']] = self.env['account.tax'].browse(tax_vals['id']) - val = self._prepare_tax_line_vals(line, tax_vals, affecting_base_tax_ids) + val = self._prepare_tax_line_vals(line, tax_vals) key = tax.get_grouping_key(val) if key not in tax_grouped: @@ -1148,11 +1148,6 @@ class AccountInvoice(models.Model): for field in default_tax_group_fields: tax_grouped[key][field] += round_curr(val.get(field)) or 0 - if tax.amount_type == "group": - affecting_base_tax_ids += tax.children_tax_ids.filtered(lambda t: is_tax_affecting_base_amount(t)).ids - elif is_tax_affecting_base_amount(tax): - affecting_base_tax_ids.append(tax.id) - return tax_grouped @api.multi @@ -1268,7 +1263,6 @@ class AccountInvoice(models.Model): def tax_line_move_line_get(self): self.ensure_one() - res = [] # loop the invoice.tax.line in reversal sequence for tax_line in sorted(self.tax_line_ids, key=lambda x: -x.sequence): @@ -1288,7 +1282,8 @@ class AccountInvoice(models.Model): 'invoice_id': self.id, 'tax_ids': tax_line.tax_ids and [(6, 0, tax_line.tax_ids.ids)] or False, # We don't pass an empty recordset here, as it would reset the tax_exibility of the line, due to the condition in account.move.line's create 'tax_repartition_line_id': tax_line.tax_repartition_line_id.id, - 'tag_ids': [(6, 0, tax_line.tax_repartition_line_id.tag_ids.ids)], + 'tag_ids': [(6, 0, tax_line.tag_ids.ids)], + 'tax_base_amount': tax_line.base, }) return res @@ -2146,8 +2141,8 @@ class AccountInvoiceTax(models.Model): company_id = fields.Many2one('res.company', string='Company', related='account_id.company_id', store=True, readonly=True) currency_id = fields.Many2one('res.currency', related='invoice_id.currency_id', store=True, readonly=True) base = fields.Monetary(string='Base') - tax_ids = fields.Many2many('account.tax', string='Affecting Base Taxes', - help='Taxes affecting the tax base amount applied before this one.') + tax_ids = fields.Many2many('account.tax', string='Affecting Base Taxes', help='Taxes whose base amount needs has to be affected by this tax line.') + tag_ids = fields.Many2many(string="Tags", comodel_name='account.account.tag', help="The taxes that will be applied on the move line generated for this tax entry") @api.depends('amount', 'amount_rounding') def _compute_amount_total(self): diff --git a/addons/account/models/account_journal_dashboard.py b/addons/account/models/account_journal_dashboard.py index cf1f40f7a94..08b119405b4 100644 --- a/addons/account/models/account_journal_dashboard.py +++ b/addons/account/models/account_journal_dashboard.py @@ -3,6 +3,7 @@ from datetime import datetime, timedelta from babel.dates import format_datetime, format_date from odoo import models, api, _, fields +from odoo.osv import expression from odoo.release import version from odoo.tools import DEFAULT_SERVER_DATE_FORMAT as DF, safe_eval from odoo.tools.misc import formatLang, format_date as odoo_format_date @@ -427,10 +428,16 @@ class account_journal(models.Model): action_name = 'action_view_bank_statement_tree' elif self.type == 'sale': action_name = 'action_invoice_tree1' - self = self.with_context(use_domain=[('journal_id', '=', self.id)]) + use_domain = expression.AND( + [self.env.context.get('use_domain', []), [('journal_id', '=', self.id)]] + ) + self = self.with_context(use_domain=use_domain) elif self.type == 'purchase': action_name = 'action_vendor_bill_template' - self = self.with_context(use_domain=[('journal_id', '=', self.id)]) + use_domain = expression.AND( + [self.env.context.get('use_domain', []), [('journal_id', '=', self.id)]] + ) + self = self.with_context(use_domain=use_domain) else: action_name = 'action_move_journal_line' diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index 8fb43c81d3d..19e3ea5dec4 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -87,14 +87,12 @@ class AccountMove(models.Model): if line.account_id.user_type_id.type in ('receivable', 'payable'): amount = abs(line.debit - line.credit) total_amount += amount + for partial_line in (line.matched_debit_ids + line.matched_credit_ids): + total_reconciled += partial_line.amount precision_currency = move.currency_id or move.company_id.currency_id if float_is_zero(total_amount, precision_rounding=precision_currency.rounding): move.matched_percentage = 1.0 else: - for line in move.line_ids: - if line.account_id.user_type_id.type in ('receivable', 'payable'): - for partial_line in (line.matched_debit_ids + line.matched_credit_ids): - total_reconciled += partial_line.amount move.matched_percentage = total_reconciled / total_amount @api.multi @@ -290,6 +288,7 @@ class AccountMove(models.Model): 'company_currency_id': self.company_id.currency_id.id, 'tax_repartition_line_id': line_vals['tax_repartition_line_id'], 'tag_ids': line_vals['tag_ids'], + 'tax_base_amount': line_vals['base'], } # N.B. currency_id/amount_currency are not set because if we have two lines with the same tax # and different currencies, we have no idea which currency set on this line. @@ -580,7 +579,7 @@ class AccountMoveLine(models.Model): else: date = partial_line.credit_move_id.date if partial_line.debit_move_id == line else partial_line.debit_move_id.date rate = line.currency_id.with_context(date=date).rate - amount_residual_currency += sign_partial_line * line.currency_id.round(partial_line.amount * rate) + amount_residual_currency += sign_partial_line * partial_line.amount * rate #computing the `reconciled` field. reconciled = False @@ -609,15 +608,6 @@ class AccountMoveLine(models.Model): currency = self.env['account.journal'].browse(context['default_journal_id']).currency_id return currency - @api.depends('move_id.line_ids', 'move_id.line_ids.tax_line_id', 'move_id.line_ids.debit', 'move_id.line_ids.credit') - def _compute_tax_base_amount(self): - for move_line in self: - if move_line.tax_line_id: - base_lines = move_line.move_id.line_ids.filtered(lambda line: move_line.tax_line_id in line.tax_ids) - move_line.tax_base_amount = abs(sum(base_lines.mapped('balance'))) - else: - move_line.tax_base_amount = 0 - @api.depends('move_id') def _compute_parent_state(self): for record in self.filtered('move_id'): @@ -641,7 +631,7 @@ class AccountMoveLine(models.Model): help="The residual amount on a journal item expressed in the company currency.") amount_residual_currency = fields.Monetary(compute='_amount_residual', string='Residual Amount in Currency', store=True, help="The residual amount on a journal item expressed in its currency (possibly not the company currency).") - tax_base_amount = fields.Monetary(string="Base Amount", compute='_compute_tax_base_amount', currency_field='company_currency_id', store=True) + tax_base_amount = fields.Monetary(string="Base Amount", currency_field='company_currency_id') account_id = fields.Many2one('account.account', string='Account', required=True, index=True, ondelete="cascade", domain=[('deprecated', '=', False)], default=lambda self: self._context.get('account_id', False)) move_id = fields.Many2one('account.move', string='Journal Entry', ondelete="cascade", @@ -699,21 +689,23 @@ class AccountMoveLine(models.Model): if {'debit', 'credit', 'partner_id', 'account_id'}.isdisjoint(fields): return rec #compute the default credit/debit of the next line in case of a manual entry - AccountMove = self.env['account.move'] + move = self.env['account.move'].new({'line_ids': self._context['line_ids']}) balance = 0 - for line in AccountMove.resolve_2many_commands( - 'line_ids', self._context['line_ids'], fields=['credit', 'debit']): - balance += line.get('debit', 0) - line.get('credit', 0) - if len(self._context['line_ids']) > 1: - lines = AccountMove.resolve_2many_commands('line_ids', self._context['line_ids'][-2:], fields=['partner_id', 'account_id']) - if lines[0].get('partner_id', False) == lines[1].get('partner_id', False): - rec.update({'partner_id': lines[0].get('partner_id', False)}) - if lines[0].get('account_id', False) == lines[1].get('account_id', False): - rec.update({'account_id': lines[0].get('account_id', False)}) + for line in move.line_ids: + balance += line.debit - line.credit if balance < 0: rec.update({'debit': -balance}) if balance > 0: rec.update({'credit': balance}) + + # Set the partner/account to the same value as the two last lines if they are equal + if len(move.line_ids) > 1: + last_lines = move.line_ids[-2:] + if last_lines[0].account_id == last_lines[1].account_id: + rec['account_id'] = last_lines[0].account_id.id + if last_lines[0].partner_id == last_lines[1].partner_id: + rec['partner_id'] = last_lines[0].partner_id.id + return rec @api.multi @@ -821,21 +813,22 @@ class AccountMoveLine(models.Model): In case of full reconciliation, all moves belonging to the reconciliation will belong to the same account_full_reconcile object. """ # Get first all aml involved - todo = self.env['account.partial.reconcile'].search_read(['|', ('debit_move_id', 'in', self.ids), ('credit_move_id', 'in', self.ids)], ['debit_move_id', 'credit_move_id']) - amls = set(self.ids) + part_recs = self.env['account.partial.reconcile'].search(['|', ('debit_move_id', 'in', self.ids), ('credit_move_id', 'in', self.ids)]) + amls = self + todo = set(part_recs) seen = set() while todo: - aml_ids = [rec['debit_move_id'][0] for rec in todo if rec['debit_move_id']] + [rec['credit_move_id'][0] for rec in todo if rec['credit_move_id']] - amls |= set(aml_ids) - seen |= set([rec['id'] for rec in todo]) - todo = self.env['account.partial.reconcile'].search_read(['&', '|', ('credit_move_id', 'in', aml_ids), ('debit_move_id', 'in', aml_ids), '!', ('id', 'in', list(seen))], ['debit_move_id', 'credit_move_id']) - - partial_rec_ids = list(seen) + partial_rec = todo.pop() + seen.add(partial_rec) + for aml in [partial_rec.debit_move_id, partial_rec.credit_move_id]: + if aml not in amls: + amls += aml + for x in aml.matched_debit_ids | aml.matched_credit_ids: + if x not in seen: + todo.add(x) + partial_rec_ids = [x.id for x in seen] if not amls: return - else: - amls = self.browse(list(amls)) - # If we have multiple currency, we can only base ourselve on debit-credit to see if it is fully reconciled currency = set([a.currency_id for a in amls if a.currency_id.id != False]) multiple_currency = False @@ -998,7 +991,10 @@ class AccountMoveLine(models.Model): debit_moves = debit_moves.sorted(key=lambda a: (a.date_maturity or a.date, a.currency_id)) credit_moves = credit_moves.sorted(key=lambda a: (a.date_maturity or a.date, a.currency_id)) # Compute on which field reconciliation should be based upon: - field = self[0].account_id.currency_id and 'amount_residual_currency' or 'amount_residual' + if self[0].account_id.currency_id and self[0].account_id.currency_id != self[0].account_id.company_id.currency_id: + field = 'amount_residual_currency' + else: + field = 'amount_residual' #if all lines share the same currency, use amount_residual_currency to avoid currency rounding error if self[0].currency_id and all([x.amount_currency and x.currency_id == self[0].currency_id for x in self]): field = 'amount_residual_currency' @@ -1712,6 +1708,7 @@ class AccountPartialReconcile(models.Model): 'move_id': newly_created_move.id, 'partner_id': line.partner_id.id, 'tax_repartition_line_id': line.tax_repartition_line_id.id, + 'tax_base_amount': line.tax_base_amount, 'tag_ids': [(6, 0, line.tag_ids.ids)], }) if line.account_id.reconcile: @@ -1737,6 +1734,7 @@ class AccountPartialReconcile(models.Model): 'amount_currency': self.amount_currency and line.currency_id.round(line.amount_currency * amount / line.balance) or 0.0, 'partner_id': line.partner_id.id, 'tax_repartition_line_id': line.tax_repartition_line_id.id, + 'tax_base_amount': line.tax_base_amount, 'tag_ids': [(6, 0, line.tag_ids.ids)], }) self.env['account.move.line'].with_context(check_move_validity=False).create({ diff --git a/addons/account/models/chart_template.py b/addons/account/models/chart_template.py index 53b36274de1..6409ae13445 100644 --- a/addons/account/models/chart_template.py +++ b/addons/account/models/chart_template.py @@ -894,7 +894,9 @@ class AccountTaxTemplate(models.Model): 'account_dict': dictionary containing a to-do list with all the accounts to assign on new taxes } """ - ChartTemplate = self.env['account.chart.template'] + # default_company_id is needed in context to allow creation of default + # repartition lines on taxes + ChartTemplate = self.env['account.chart.template'].with_context(default_company_id=company.id) todo_dict = {'account.tax': {}, 'account.tax.repartition.line': {}} tax_template_to_tax = {} @@ -926,7 +928,7 @@ class AccountTaxTemplate(models.Model): # We also have to delay the assignation of accounts to repartition lines all_tax_rep_lines = tax.invoice_repartition_line_ids + tax.refund_repartition_line_ids all_template_rep_lines = template.invoice_repartition_line_ids + template.refund_repartition_line_ids - for i in range(0, len(all_tax_rep_lines)): + for i in range(0, len(all_template_rep_lines)): # We assume template and tax repartition lines are in the same order template_account = all_template_rep_lines[i].account_id if template_account: diff --git a/addons/account/models/product.py b/addons/account/models/product.py index 655b457bbf0..f9b046c652a 100644 --- a/addons/account/models/product.py +++ b/addons/account/models/product.py @@ -81,4 +81,5 @@ class ProductProduct(models.Model): 'analytic_tag_ids': line.get('analytic_tag_ids', False), 'tax_repartition_line_id': line.get('tax_repartition_line_id'), 'tag_ids': line.get('tag_ids'), + 'tax_base_amount': line.get('tax_base_amount'), } diff --git a/addons/account/models/reconciliation_widget.py b/addons/account/models/reconciliation_widget.py index 0d6577fe34c..f10bacfa3f3 100644 --- a/addons/account/models/reconciliation_widget.py +++ b/addons/account/models/reconciliation_widget.py @@ -83,28 +83,43 @@ class AccountReconciliation(models.AbstractModel): @api.model def _get_bank_statement_line_partners(self, st_lines): + params = [] + + # Add the res.partner.ban's IR rules. In case partners are not shared between companies, + # identical bank accounts may exist in a company we don't have access to. + ir_rules_query = self.env['res.partner.bank']._where_calc([]) + self.env['res.partner.bank']._apply_ir_rules(ir_rules_query, 'read') + from_clause, where_clause, where_clause_params = ir_rules_query.get_sql() + if where_clause: + where_bank = ('AND %s' % where_clause).replace('res_partner_bank', 'bank') + params += where_clause_params + else: + where_bank = '' + + # Add the res.partner's IR rules. In case partners are not shared between companies, + # identical partners may exist in a company we don't have access to. + ir_rules_query = self.env['res.partner']._where_calc([]) + self.env['res.partner']._apply_ir_rules(ir_rules_query, 'read') + from_clause, where_clause, where_clause_params = ir_rules_query.get_sql() + if where_clause: + where_partner = ('AND %s' % where_clause).replace('res_partner', 'p3') + params += where_clause_params + else: + where_partner = '' + query = ''' SELECT st_line.id AS id, COALESCE(p1.id,p2.id,p3.id) AS partner_id FROM account_bank_statement_line st_line - LEFT JOIN res_partner_bank bank ON bank.id = st_line.bank_account_id OR bank.acc_number = st_line.account_number - LEFT JOIN res_partner p1 ON st_line.partner_id=p1.id - LEFT JOIN res_partner p2 ON bank.partner_id=p2.id - LEFT JOIN res_partner p3 ON p3.name ILIKE st_line.partner_name - WHERE st_line.id IN %s ''' - params = [tuple(st_lines.ids)] + query += 'LEFT JOIN res_partner_bank bank ON bank.id = st_line.bank_account_id OR bank.acc_number = st_line.account_number %s\n' % (where_bank) + query += 'LEFT JOIN res_partner p1 ON st_line.partner_id=p1.id \n' + query += 'LEFT JOIN res_partner p2 ON bank.partner_id=p2.id \n' + query += 'LEFT JOIN res_partner p3 ON p3.name ILIKE st_line.partner_name %s\n' % (where_partner) + query += 'WHERE st_line.id IN %s' - # Add the res.partner's IR rules to the WHERE clause. In case partners are not shared - # between companies, identical partners may exist in a company we don't have access to. - ir_rules_query = self.env['res.partner']._where_calc([]) - self.env['res.partner']._apply_ir_rules(ir_rules_query, 'read') - from_clause, where_clause, where_clause_params = ir_rules_query.get_sql() - where_p3 = (" AND %s" % where_clause).replace('res_partner', 'p3') if where_clause else '' - if where_p3: - query += where_p3 - params += where_clause_params + params += [tuple(st_lines.ids)] self._cr.execute(query, params) diff --git a/addons/account/report/account_invoice_report.py b/addons/account/report/account_invoice_report.py index 594a49d831e..b94bd711c63 100644 --- a/addons/account/report/account_invoice_report.py +++ b/addons/account/report/account_invoice_report.py @@ -112,13 +112,13 @@ class AccountInvoiceReport(models.Model): 1 AS nbr, ai.id AS invoice_id, ai.type, ai.state, pt.categ_id, ai.date_due, ai.account_id, ail.account_id AS account_line_id, ai.partner_bank_id, - SUM ((invoice_type.sign_qty * ail.quantity) / u.factor * u2.factor) AS product_qty, + SUM ((invoice_type.sign_qty * ail.quantity) / COALESCE(u.factor,1) * COALESCE(u2.factor,1)) AS product_qty, SUM(ail.price_subtotal_signed * invoice_type.sign) AS price_total, - (ai.amount_total * invoice_type.sign) / (SELECT count(*) FROM account_invoice_line l where invoice_id = ai.id) * + ai.amount_total_signed / (SELECT count(*) FROM account_invoice_line l where invoice_id = ai.id) * count(*) * invoice_type.sign AS amount_total, SUM(ABS(ail.price_subtotal_signed)) / CASE - WHEN SUM(ail.quantity / u.factor * u2.factor) <> 0::numeric - THEN SUM(ail.quantity / u.factor * u2.factor) + WHEN SUM(ail.quantity / COALESCE(u.factor,1) * COALESCE(u2.factor,1)) <> 0::numeric + THEN SUM(ail.quantity / COALESCE(u.factor,1) * COALESCE(u2.factor,1)) ELSE 1::numeric END AS price_average, ai.residual_company_signed / (SELECT count(*) FROM account_invoice_line l where invoice_id = ai.id) * diff --git a/addons/account/static/src/js/reconciliation/reconciliation_action.js b/addons/account/static/src/js/reconciliation/reconciliation_action.js index 67d5de9de08..5f27b0970f6 100644 --- a/addons/account/static/src/js/reconciliation/reconciliation_action.js +++ b/addons/account/static/src/js/reconciliation/reconciliation_action.js @@ -346,9 +346,12 @@ var StatementAction = AbstractAction.extend({ 'context': self.model.getContext(), }); _.each(result.handles, function (handle) { - self._getWidget(handle).destroy(); - var index = _.findIndex(self.widgets, function (widget) {return widget.handle===handle;}); - self.widgets.splice(index, 1); + var widget = self._getWidget(handle); + if (widget) { + widget.destroy(); + var index = _.findIndex(self.widgets, function (widget) {return widget.handle===handle;}); + self.widgets.splice(index, 1); + } }); // Get number of widget and if less than constant and if there are more to laod, load until constant if (self.widgets.length < self.model.defaultDisplayQty diff --git a/addons/account/static/src/js/reconciliation/reconciliation_model.js b/addons/account/static/src/js/reconciliation/reconciliation_model.js index c28790796d2..d4dce5581ca 100644 --- a/addons/account/static/src/js/reconciliation/reconciliation_model.js +++ b/addons/account/static/src/js/reconciliation/reconciliation_model.js @@ -623,13 +623,13 @@ var StatementModel = BasicModel.extend({ prop = this._formatQuickCreate(line); line.reconciliation_proposition.push(prop); } - if (!line.reconciliation_proposition.slice(0,-1).every(function(prop) {return prop.to_check == values.to_check;})) { + if ('to_check' in values && !line.reconciliation_proposition.slice(0,-1).every(function(prop) {return prop.to_check == values.to_check;})) { new CrashManager().show_warning({data: { exception_type: _t("Incorrect Operation"), message: _t("You cannot mix items with and without the 'To Check' checkbox ticked.") }}); // FIXME: model should not be tied to the DOM ! - $('.create_to_check input').click(); + $('.create_to_check input:visible').prop('checked', !values.to_check).change(); return Promise.resolve(); } _.each(values, function (value, fieldName) { diff --git a/addons/account/static/src/js/reconciliation/reconciliation_renderer.js b/addons/account/static/src/js/reconciliation/reconciliation_renderer.js index b59b99bc881..53cd0b76598 100644 --- a/addons/account/static/src/js/reconciliation/reconciliation_renderer.js +++ b/addons/account/static/src/js/reconciliation/reconciliation_renderer.js @@ -938,10 +938,12 @@ var ManualLineRenderer = LineRenderer.extend({ * @override */ _renderCreate: function (state) { - var parentPromise = this._super(state); - this.$('.create .create_journal_id').show(); - this.$('.create .create_date').removeClass('d-none'); - this.$('.create .create_journal_id .o_input').addClass('o_required_modifier'); + var self = this; + var parentPromise = this._super(state).then(function() { + self.$('.create .create_journal_id').show(); + self.$('.create .create_date').removeClass('d-none'); + self.$('.create .create_journal_id .o_input').addClass('o_required_modifier'); + }); return parentPromise; }, diff --git a/addons/account/tests/test_account_customer_invoice.py b/addons/account/tests/test_account_customer_invoice.py index 5334151eebb..1a84c82cda2 100644 --- a/addons/account/tests/test_account_customer_invoice.py +++ b/addons/account/tests/test_account_customer_invoice.py @@ -306,30 +306,30 @@ class TestAccountCustomerInvoice(AccountTestUsers): dashboard_data = journal.get_journal_dashboard_datas() self.assertEquals(dashboard_data['number_draft'], 2) - self.assertEquals(dashboard_data['sum_draft'], '$ 68.42') + self.assertIn('68.42', dashboard_data['sum_draft']) self.assertEquals(dashboard_data['number_waiting'], 0) - self.assertEquals(dashboard_data['sum_waiting'], '$ 0.00') + self.assertIn('0.00', dashboard_data['sum_waiting']) # Check Both invoice.action_invoice_open() dashboard_data = journal.get_journal_dashboard_datas() self.assertEquals(dashboard_data['number_draft'], 1) - self.assertEquals(dashboard_data['sum_draft'], '$ -13.30') + self.assertIn('-13.30', dashboard_data['sum_draft']) self.assertEquals(dashboard_data['number_waiting'], 1) - self.assertEquals(dashboard_data['sum_waiting'], '$ 81.72') + self.assertIn('81.72', dashboard_data['sum_waiting']) # Check waiting payment refund.action_invoice_open() dashboard_data = journal.get_journal_dashboard_datas() self.assertEquals(dashboard_data['number_draft'], 0) - self.assertEquals(dashboard_data['sum_draft'], '$ 0.00') + self.assertIn('0.00', dashboard_data['sum_draft']) self.assertEquals(dashboard_data['number_waiting'], 2) - self.assertEquals(dashboard_data['sum_waiting'], '$ 68.42') + self.assertIn('68.42', dashboard_data['sum_waiting']) # Check partial receivable_account = refund.move_id.line_ids.mapped('account_id').filtered(lambda a: a.internal_type == 'receivable') @@ -353,12 +353,12 @@ class TestAccountCustomerInvoice(AccountTestUsers): dashboard_data = journal.get_journal_dashboard_datas() self.assertEquals(dashboard_data['number_draft'], 0) - self.assertEquals(dashboard_data['sum_draft'], '$ 0.00') + self.assertIn('0.00', dashboard_data['sum_draft']) self.assertEquals(dashboard_data['number_waiting'], 2) - self.assertEquals(dashboard_data['sum_waiting'], '$ 78.42') + self.assertIn('78.42', dashboard_data['sum_waiting']) with patch('odoo.fields.Date.today', patched_today): dashboard_data = journal.get_journal_dashboard_datas() self.assertEquals(dashboard_data['number_late'], 2) - self.assertEquals(dashboard_data['sum_late'], '$ 78.42') + self.assertIn('78.42', dashboard_data['sum_late']) diff --git a/addons/account/tests/test_invoice_taxes.py b/addons/account/tests/test_invoice_taxes.py index 4b0d8380f72..b6759271579 100644 --- a/addons/account/tests/test_invoice_taxes.py +++ b/addons/account/tests/test_invoice_taxes.py @@ -100,8 +100,8 @@ class TestInvoiceTaxes(AccountingTestCase): Expected: Tax | Taxes | Base | Amount -------------------------------------------- - 21% incl | / | 100 | 21 - 12% | 21% incl | 121 | 14.52 + 21% incl | 12% | 100 | 21 + 12% | / | 121 | 14.52 12% | / | 100 | 12 ''' invoice = self._create_invoice([ @@ -110,9 +110,8 @@ class TestInvoiceTaxes(AccountingTestCase): ]) invoice.action_invoice_open() self.assertRecordValues(invoice.tax_line_ids.sorted(lambda x: x.amount), [ - {'name': self.percent_tax_2.name, 'base': 100, 'amount': 12, 'tax_ids': []}, - {'name': self.percent_tax_2.name, 'base': 121, 'amount': 14.52, 'tax_ids': [self.percent_tax_1_incl.id]}, - {'name': self.percent_tax_1_incl.name, 'base': 100, 'amount': 21, 'tax_ids': []}, + {'name': self.percent_tax_1_incl.name, 'base': 100, 'amount': 21, 'tax_ids': [self.percent_tax_2.id]}, + {'name': self.percent_tax_2.name, 'base': 221, 'amount': 26.52, 'tax_ids': []}, ]) def test_group_of_taxes(self): @@ -135,9 +134,8 @@ class TestInvoiceTaxes(AccountingTestCase): ]) invoice.action_invoice_open() self.assertRecordValues(invoice.tax_line_ids.sorted(lambda x: x.amount), [ - {'name': self.percent_tax_2.name, 'base': 100, 'amount': 12, 'tax_ids': []}, - {'name': self.percent_tax_2.name, 'base': 121, 'amount': 14.52, 'tax_ids': [self.percent_tax_1_incl.id]}, - {'name': self.percent_tax_1_incl.name, 'base': 100, 'amount': 21, 'tax_ids': []}, + {'name': self.percent_tax_1_incl.name, 'base': 100, 'amount': 21, 'tax_ids': [self.percent_tax_2.id]}, + {'name': self.percent_tax_2.name, 'base': 221, 'amount': 26.52, 'tax_ids': []}, ]) def _create_tax_tag(self, tag_name): diff --git a/addons/account/tests/test_reconciliation.py b/addons/account/tests/test_reconciliation.py index 62d414fdf54..0a35310f8a7 100644 --- a/addons/account/tests/test_reconciliation.py +++ b/addons/account/tests/test_reconciliation.py @@ -91,6 +91,11 @@ class TestReconciliation(AccountingTestCase): 'code': 'CABA', 'type': 'general', }) + self.general_journal = self.env['account.journal'].create({ + 'name': 'general', + 'code': 'GENE', + 'type': 'general', + }) # Tax Cash Basis self.tax_cash_basis = self.env['account.tax'].create({ @@ -1304,6 +1309,7 @@ class TestReconciliationExec(TestReconciliation): 'debit': 16.67, 'move_id': purchase_move.id, 'tax_repartition_line_id': self.tax_cash_basis.invoice_repartition_line_ids.filtered(lambda x: x.repartition_type == 'tax').id, + 'tax_base_amount': 83.33, }) purchase_move.post() @@ -1422,6 +1428,7 @@ class TestReconciliationExec(TestReconciliation): 'debit': 5, 'move_id': purchase_move.id, 'tax_repartition_line_id': tax_cash_basis10percent.invoice_repartition_line_ids.filtered(lambda x: x.repartition_type == 'tax').id, + 'tax_base_amount': 50, }) AccountMoveLine.create({ 'name': 'expenseTaxed 20%', @@ -1436,6 +1443,7 @@ class TestReconciliationExec(TestReconciliation): 'debit': 16.67, 'move_id': purchase_move.id, 'tax_repartition_line_id': self.tax_cash_basis.invoice_repartition_line_ids.filtered(lambda x: x.repartition_type == 'tax').id, + 'tax_base_amount': 83.33, }) purchase_move.post() @@ -1646,6 +1654,7 @@ class TestReconciliationExec(TestReconciliation): 'currency_id': self.currency_usd_id, 'amount_currency': 848.16, 'tax_repartition_line_id': self.tax_cash_basis.invoice_repartition_line_ids.filtered(lambda x: x.repartition_type == 'tax').id, + 'tax_base_amount': 106841.65, }) purchase_payable_line0 = aml_obj.create({ 'name': 'Payable', @@ -1816,6 +1825,7 @@ class TestReconciliationExec(TestReconciliation): 'debit': 17094.66, 'move_id': purchase_move.id, 'tax_repartition_line_id': self.tax_cash_basis.invoice_repartition_line_ids.filtered(lambda x: x.repartition_type == 'tax').id, + 'tax_base_amount': 106841.65, 'currency_id': self.currency_usd_id, 'amount_currency': 848.16, }) @@ -1975,6 +1985,7 @@ class TestReconciliationExec(TestReconciliation): 'debit': 5, 'move_id': purchase_move.id, 'tax_repartition_line_id': tax_cash_basis10percent.invoice_repartition_line_ids.filtered(lambda x: x.repartition_type == 'tax').id, + 'tax_base_amount': 50, }) AccountMoveLine.create({ 'name': 'expenseTaxed 20%', @@ -1989,6 +2000,7 @@ class TestReconciliationExec(TestReconciliation): 'debit': 20, 'move_id': purchase_move.id, 'tax_repartition_line_id': self.tax_cash_basis.invoice_repartition_line_ids.filtered(lambda x: x.repartition_type == 'tax').id, + 'tax_base_amount': 100, }) purchase_move.post() @@ -2056,6 +2068,71 @@ class TestReconciliationExec(TestReconciliation): self.assertTrue(pay_receivable_line1.reconciled) self.assertEqual(pay_receivable_line1.matched_debit_ids, move_caba1.tax_cash_basis_rec_id) + def test_reconciliation_with_currency(self): + #reconciliation on an account having a foreign currency being + #the same as the company one + account_rcv = self.account_rcv + account_rcv.currency_id = self.currency_euro_id + aml_obj = self.env['account.move.line'].with_context( + check_move_validity=False) + general_move1 = self.env['account.move'].create({ + 'name': 'general1', + 'journal_id': self.general_journal.id, + }) + aml_obj.create({ + 'name': 'debit1', + 'account_id': account_rcv.id, + 'debit': 11, + 'move_id': general_move1.id, + }) + aml_obj.create({ + 'name': 'credit1', + 'account_id': self.account_rsa.id, + 'credit': 11, + 'move_id': general_move1.id, + }) + general_move1.post() + general_move2 = self.env['account.move'].create({ + 'name': 'general2', + 'journal_id': self.general_journal.id, + }) + aml_obj.create({ + 'name': 'credit2', + 'account_id': account_rcv.id, + 'credit': 10, + 'move_id': general_move2.id, + }) + aml_obj.create({ + 'name': 'debit2', + 'account_id': self.account_rsa.id, + 'debit': 10, + 'move_id': general_move2.id, + }) + general_move2.post() + general_move3 = self.env['account.move'].create({ + 'name': 'general3', + 'journal_id': self.general_journal.id, + }) + aml_obj.create({ + 'name': 'credit3', + 'account_id': account_rcv.id, + 'credit': 1, + 'move_id': general_move3.id, + }) + aml_obj.create({ + 'name': 'debit3', + 'account_id': self.account_rsa.id, + 'debit': 1, + 'move_id': general_move3.id, + }) + general_move3.post() + to_reconcile = ((general_move1 + general_move2 + general_move3) + .mapped('line_ids') + .filtered(lambda l: l.account_id.id == account_rcv.id)) + to_reconcile.reconcile() + for aml in to_reconcile: + self.assertEqual(aml.amount_residual, 0.0) + def test_reconciliation_process_move_lines_with_mixed_currencies(self): # Delete any old rate - to make sure that we use the ones we need. old_rates = self.env['res.currency.rate'].search( diff --git a/addons/account/views/account_invoice_view.xml b/addons/account/views/account_invoice_view.xml index b965b67f488..a55aafc795e 100644 --- a/addons/account/views/account_invoice_view.xml +++ b/addons/account/views/account_invoice_view.xml @@ -384,6 +384,7 @@ <field name="amount_total" invisible="1"/> <field name="currency_id" invisible="1" force_save="1"/> <field name="tax_repartition_line_id" invisible="1"/> <!--Needed in order for the onchange computing the taxes to keep the repartition lines--> + <field name="tag_ids" invisible="1"/> <!--Needed in order for the onchange computing the taxes to keep the tags--> <field name="base" invisible="1"/> </tree> </field> @@ -655,6 +656,7 @@ <field name="amount_total"/> <field name="currency_id" invisible="1" force_save="1"/> <field name="tax_repartition_line_id" invisible="1"/> <!--Needed in order for the onchange computing the taxes to keep the repartition lines--> + <field name="tag_ids" invisible="1"/> <!--Needed in order for the onchange computing the taxes to keep the tags--> <field name="base" invisible="1"/> </tree> </field> diff --git a/addons/account/views/account_move_views.xml b/addons/account/views/account_move_views.xml index 0eadb5cdcf8..aa2e1a35123 100644 --- a/addons/account/views/account_move_views.xml +++ b/addons/account/views/account_move_views.xml @@ -357,6 +357,7 @@ <field name="recompute_tax_line" invisible="1" readonly="1"/> <field name="tax_line_grouping_key" invisible="1" readonly="1"/> <field name="tax_repartition_line_id" invisible="1"/> <!--Not to be removed, we need it so that the call to new() takes it into account when autocompleting the lines of a tax--> + <field name="tax_base_amount" invisible="1"/> <!--Not to be removed, we need it so that the call to new() takes it into account when autocompleting the lines of a tax--> <field name="tag_ids" invisible="1"/> </tree> </field> diff --git a/addons/account/views/account_payment_view.xml b/addons/account/views/account_payment_view.xml index 772a29affff..a3e175bdf20 100644 --- a/addons/account/views/account_payment_view.xml +++ b/addons/account/views/account_payment_view.xml @@ -99,7 +99,8 @@ <filter string="Payment Method" name="paymentmethod" domain="[]" context="{'group_by': 'payment_method_id'}"/> <filter string="State" name="state" domain="[]" context="{'group_by': 'state'}"/> <filter string="Currency" name="currency" domain="[]" context="{'group_by': 'currency_id'}" groups="base.group_multi_currency"/> - <filter string="Company" name="company" domain="[]" context="{'group_by': 'company_id'}" groups="base.group_multi_company"/> + <!-- company_id is not stored (cdca5d4751a979f6b694f02d88160843f81a77f5). But we can't break structure--> + <filter string="Company" name="company" domain="[]" context="{'group_by': 'company_id'}" groups="base.group_multi_company" invisible="1" /> <separator/> <filter string="Late Activities" name="activities_overdue" domain="[('activity_ids.date_deadline', '<', context_today().strftime('%Y-%m-%d'))]" diff --git a/addons/account/wizard/wizard_tax_adjustments.py b/addons/account/wizard/wizard_tax_adjustments.py index a394c240ffe..4dd21030802 100644 --- a/addons/account/wizard/wizard_tax_adjustments.py +++ b/addons/account/wizard/wizard_tax_adjustments.py @@ -37,6 +37,7 @@ class TaxAdjustments(models.TransientModel): 'credit': not is_debit and abs(self.amount) or 0, 'account_id': is_debit and self.debit_account_id.id or self.credit_account_id.id, 'tax_repartition_line_id': repartition_line.id, + 'tax_base_amount': tax_vals['base'], 'tag_ids': [(6, False, repartition_line.tag_ids.ids)], })) diff --git a/addons/account_analytic_default/models/account_analytic_default.py b/addons/account_analytic_default/models/account_analytic_default.py index cfbcbbcafc8..208be7e3ab8 100644 --- a/addons/account_analytic_default/models/account_analytic_default.py +++ b/addons/account_analytic_default/models/account_analytic_default.py @@ -70,13 +70,27 @@ class AccountInvoiceLine(models.Model): @api.onchange('product_id', 'account_id') def _onchange_product_id_account_id(self): - rec = self.env['account.analytic.default'].account_get(product_id=self.product_id.id, partner_id=self.invoice_id.commercial_partner_id.id, account_id=self.account_id.id, user_id=self.env.uid, date=self.invoice_id.date_due, company_id=self.invoice_id.company_id.id) + rec = self.env['account.analytic.default'].account_get( + product_id=self.product_id.id, + partner_id=self.invoice_id.commercial_partner_id.id, + account_id=self.account_id.id, + user_id=self.invoice_id.user_id.id or self.env.uid, + date=self.invoice_id.date_due, + company_id=self.invoice_id.company_id.id, + ) self.account_analytic_id = rec.analytic_id.id self.analytic_tag_ids = rec.analytic_tag_ids.ids def _set_additional_fields(self): if not self.account_analytic_id or not self.analytic_tag_ids: - rec = self.env['account.analytic.default'].account_get(product_id=self.product_id.id, partner_id=self.invoice_id.commercial_partner_id.id, account_id=self.account_id.id, user_id=self.env.uid, date=self.invoice_id.date_due, company_id=self.invoice_id.company_id.id) + rec = self.env['account.analytic.default'].account_get( + product_id=self.product_id.id, + partner_id=self.invoice_id.commercial_partner_id.id, + account_id=self.account_id.id, + user_id=self.invoice_id.user_id.id or self.env.uid, + date=self.invoice_id.date_due, + company_id=self.invoice_id.company_id.id, + ) if rec: if not self.account_analytic_id: self.account_analytic_id = rec.analytic_id.id diff --git a/addons/account_payment/controllers/payment.py b/addons/account_payment/controllers/payment.py index 8ac01bd398d..c3922258c6f 100644 --- a/addons/account_payment/controllers/payment.py +++ b/addons/account_payment/controllers/payment.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +from werkzeug.urls import url_encode + from odoo import http, _ from odoo.addons.portal.controllers.portal import _build_url_w_params from odoo.addons.payment.controllers.portal import PaymentProcessing @@ -17,8 +19,6 @@ class PaymentPortal(http.Controller): :return html: form containing all values related to the acquirer to redirect customers to the acquirer website """ - success_url = kwargs.get('success_url', '/my') - invoice_sudo = request.env['account.invoice'].sudo().browse(invoice_id) if not invoice_sudo: return False @@ -30,6 +30,10 @@ class PaymentPortal(http.Controller): if request.env.user._is_public(): save_token = False # we avoid to create a token for the public user + + success_url = kwargs.get( + 'success_url', "%s?%s" % (invoice_sudo.access_url, url_encode({'access_token': access_token}) if access_token else '') + ) vals = { 'acquirer_id': acquirer_id, 'return_url': success_url, @@ -54,7 +58,6 @@ class PaymentPortal(http.Controller): def invoice_pay_token(self, invoice_id, pm_id=None, **kwargs): """ Use a token to perform a s2s transaction """ error_url = kwargs.get('error_url', '/my') - success_url = kwargs.get('success_url', '/my') access_token = kwargs.get('access_token') params = {} if access_token: @@ -65,6 +68,9 @@ class PaymentPortal(http.Controller): params['error'] = 'pay_invoice_invalid_doc' return request.redirect(_build_url_w_params(error_url, params)) + success_url = kwargs.get( + 'success_url', "%s?%s" % (invoice_sudo.access_url, url_encode({'access_token': access_token}) if access_token else '') + ) try: token = request.env['payment.token'].sudo().browse(int(pm_id)) except (ValueError, TypeError): diff --git a/addons/account_voucher/models/account_voucher.py b/addons/account_voucher/models/account_voucher.py index 7a760249a5d..4fa88c8de43 100644 --- a/addons/account_voucher/models/account_voucher.py +++ b/addons/account_voucher/models/account_voucher.py @@ -37,7 +37,7 @@ class AccountVoucher(models.Model): ('purchase', 'Purchase') ], string='Type', readonly=True, states={'draft': [('readonly', False)]}, oldname="type") name = fields.Char('Payment Memo', - readonly=True, states={'draft': [('readonly', False)]}, default='') + readonly=True, states={'draft': [('readonly', False)]}, default='',copy=False) date = fields.Date("Bill Date", readonly=True, index=True, states={'draft': [('readonly', False)]}, copy=False, default=fields.Date.context_today) diff --git a/addons/bus/controllers/main.py b/addons/bus/controllers/main.py index 6ff4d7ac8e0..54bb005d5fc 100644 --- a/addons/bus/controllers/main.py +++ b/addons/bus/controllers/main.py @@ -38,3 +38,7 @@ class BusController(Controller): if request.registry.in_test_mode(): raise exceptions.UserError(_("bus.Bus not available in test mode")) return self._poll(request.db, channels, last, options) + + @route('/longpolling/im_status', type="json", auth="user") + def im_status(self, partner_ids): + return request.env['res.partner'].search_read([['id', 'in', partner_ids]], ['id', 'im_status']) diff --git a/addons/calendar/models/calendar.py b/addons/calendar/models/calendar.py index 16a95ffdbd6..d32b9178818 100644 --- a/addons/calendar/models/calendar.py +++ b/addons/calendar/models/calendar.py @@ -245,7 +245,7 @@ class AlarmManager(models.AbstractModel): _name = 'calendar.alarm_manager' _description = 'Event Alarm Manager' - def get_next_potential_limit_alarm(self, alarm_type, seconds=None, partner_id=None): + def _get_next_potential_limit_alarm(self, alarm_type, seconds=None, partner_id=None): result = {} delta_request = """ SELECT @@ -325,6 +325,12 @@ class AlarmManager(models.AbstractModel): 'rrule': rule } + # determine accessible events + events = self.env['calendar.event'].browse(result) + result = { + key: result[key] + for key in set(events._filter_access_rules('read').ids) + } return result def do_check_alarm_for_one_date(self, one_date, event, event_maxdelta, in_the_next_X_seconds, alarm_type, after=False, missing=False): @@ -376,7 +382,7 @@ class AlarmManager(models.AbstractModel): cron_interval = cron.interval_number * interval_to_second[cron.interval_type] - all_meetings = self.get_next_potential_limit_alarm('email', seconds=cron_interval) + all_meetings = self._get_next_potential_limit_alarm('email', seconds=cron_interval) for meeting in self.env['calendar.event'].browse(all_meetings): max_delta = all_meetings[meeting.id]['max_duration'] @@ -406,7 +412,7 @@ class AlarmManager(models.AbstractModel): if not partner: return [] - all_meetings = self.get_next_potential_limit_alarm('notification', partner_id=partner.id) + all_meetings = self._get_next_potential_limit_alarm('notification', partner_id=partner.id) time_limit = 3600 * 24 # return alarms of the next 24 hours for event_id in all_meetings: max_delta = all_meetings[event_id]['max_duration'] diff --git a/addons/calendar_sms/models/calendar.py b/addons/calendar_sms/models/calendar.py index 1f87174e150..65cd1a8cad5 100644 --- a/addons/calendar_sms/models/calendar.py +++ b/addons/calendar_sms/models/calendar.py @@ -52,7 +52,7 @@ class AlarmManager(models.AbstractModel): } cron_interval = cron.interval_number * interval_to_second[cron.interval_type] - events_data = self.get_next_potential_limit_alarm('sms', seconds=cron_interval) + events_data = self._get_next_potential_limit_alarm('sms', seconds=cron_interval) for event in self.env['calendar.event'].browse(events_data): max_delta = events_data[event.id]['max_duration'] diff --git a/addons/crm/models/crm_lead.py b/addons/crm/models/crm_lead.py index 29685f8e2b1..7ffab80372a 100644 --- a/addons/crm/models/crm_lead.py +++ b/addons/crm/models/crm_lead.py @@ -54,7 +54,10 @@ class Lead(models.Model): _primary_email = 'email_from' def _default_probability(self): - stage_id = self._default_stage_id() + if 'default_stage_id' in self._context: + stage_id = self._context.get('default_stage_id') + else: + stage_id = self._default_stage_id() if stage_id: return self.env['crm.stage'].browse(stage_id).probability return 10 @@ -305,9 +308,10 @@ class Lead(models.Model): if vals.get('user_id') and 'date_open' not in vals: vals['date_open'] = fields.Datetime.now() - if context.get('default_partner_id') and not vals.get('email_from'): - partner = self.env['res.partner'].browse(context['default_partner_id']) - vals['email_from'] = partner.email + partner_id = vals.get('partner_id') or context.get('default_partner_id') + onchange_values = self._onchange_partner_id_values(partner_id) + onchange_values.update(vals) # we don't want to overwrite any existing key + vals = onchange_values return super(Lead, self.with_context(context)).create(vals) @@ -316,7 +320,8 @@ class Lead(models.Model): # stage change: update date_last_stage_update if 'stage_id' in vals: vals['date_last_stage_update'] = fields.Datetime.now() - if vals.get('user_id') and 'date_open' not in vals: + # Only write the 'date_open' if no salesperson was assigned. + if vals.get('user_id') and 'date_open' not in vals and not self.mapped('user_id'): vals['date_open'] = fields.Datetime.now() # stage change with new stage: update probability and date_closed if vals.get('stage_id') and 'probability' not in vals: diff --git a/addons/event_sale/models/sale_order.py b/addons/event_sale/models/sale_order.py index 5a759e8a68d..d705ab57881 100644 --- a/addons/event_sale/models/sale_order.py +++ b/addons/event_sale/models/sale_order.py @@ -81,7 +81,7 @@ class SaleOrderLine(models.Model): @api.onchange('event_ticket_id') def _onchange_event_ticket_id(self): company = self.event_id.company_id or self.env.company - currency = company.currency_id + currency = company.currency_id or self.env.company.currency_id self.price_unit = currency._convert( self.event_ticket_id.price, self.order_id.currency_id, self.order_id.company_id, self.order_id.date_order or fields.Date.today()) diff --git a/addons/gamification/models/res_users.py b/addons/gamification/models/res_users.py index 6a188f2003a..daa9d4bc1e1 100644 --- a/addons/gamification/models/res_users.py +++ b/addons/gamification/models/res_users.py @@ -3,6 +3,9 @@ from odoo import api, fields, models +import logging + +_logger = logging.getLogger(__name__) class Users(models.Model): _inherit = 'res.users' @@ -18,26 +21,9 @@ class Users(models.Model): @api.depends('karma') def _compute_karma_position(self): - where_query = self._where_calc([]) - self._apply_ir_rules(where_query, 'read') - from_clause, where_clause, where_clause_params = where_query.get_sql() - - query = """ - SELECT sub.id, sub.karma_position - FROM ( - SELECT id, row_number() OVER (ORDER BY res_users.karma DESC) AS karma_position - FROM {from_clause} - WHERE {where_clause} - ) sub - WHERE sub.id IN %s - """.format(from_clause=from_clause, where_clause=where_clause) - - self.env.cr.execute(query, where_clause_params + [tuple(self.ids)]) - - position_map = {item['id']: item['karma_position'] for item in self.env.cr.dictfetchall()} - + _logger.warning("The field karma_position from res.users is deprecated. Don't use it anymore.") for user in self: - user.karma_position = position_map.get(user.id, 0) + user.karma_position = 0 @api.multi @api.depends('badge_ids') diff --git a/addons/google_account/models/google_service.py b/addons/google_account/models/google_service.py index 5292a490515..fb9213f96b2 100644 --- a/addons/google_account/models/google_service.py +++ b/addons/google_account/models/google_service.py @@ -189,12 +189,12 @@ class GoogleService(models.TransientModel): status = error.response.status_code response = "" else: - req = json.loads(error.request.body) + req = json.loads(error.request.body or 'null') res = error.response.json() _logger.exception("Error while requesting Google Services\nRequest:\n%s\nResponse:\n%s", pformat(req), pformat(res)) if error.response.status_code in (400, 401, 410): - raise UserError(_("Error while requesting Google Services: %s" % res['error']['message'])) - raise self.env['res.config.settings'].get_config_warning(_("Something went wrong with your request to google")) + raise UserError(_("Error while requesting Google Services: %s") % res['error']['message']) + raise self.env['res.config.settings'].get_config_warning(_("Something went wrong with your request to google: %s") % res['error']['message']) return (status, response, ask_time) # TODO : remove me, it is only used in google calendar. Make google_calendar use the constants diff --git a/addons/hr/models/res_users.py b/addons/hr/models/res_users.py index 65561a52480..547ef54d7df 100644 --- a/addons/hr/models/res_users.py +++ b/addons/hr/models/res_users.py @@ -15,7 +15,7 @@ class User(models.Model): job_title = fields.Char(related='employee_id.job_title', readonly=False) work_phone = fields.Char(related='employee_id.work_phone', readonly=False) mobile_phone = fields.Char(related='employee_id.mobile_phone', readonly=False) - phone = fields.Char(related='employee_id.phone', readonly=False, related_sudo=False) + employee_phone = fields.Char(related='employee_id.phone', readonly=False, related_sudo=False) work_email = fields.Char(related='employee_id.work_email', readonly=False, related_sudo=False) category_ids = fields.Many2many(related='employee_id.category_ids', string="Employee Tags", readonly=False, related_sudo=False) department_id = fields.Many2one(related='employee_id.department_id', readonly=False, related_sudo=False) @@ -99,7 +99,7 @@ class User(models.Model): 'parent_id', 'passport_id', 'permit_no', - 'phone', + 'employee_phone', 'pin', 'place_of_birth', 'spouse_birthdate', diff --git a/addons/hr/views/res_users.xml b/addons/hr/views/res_users.xml index d544fa568d0..6838d961adc 100644 --- a/addons/hr/views/res_users.xml +++ b/addons/hr/views/res_users.xml @@ -98,7 +98,7 @@ It should be different from the work address. </div> <field name="km_home_work"/> - <field name="phone"/> + <field name="employee_phone"/> <field name="employee_bank_account_id"/> </group> <group string="Citizenship"> diff --git a/addons/hr_attendance_presence/views/hr_employee.xml b/addons/hr_attendance_presence/views/hr_employee.xml index c49871522a2..a645e594b20 100644 --- a/addons/hr_attendance_presence/views/hr_employee.xml +++ b/addons/hr_attendance_presence/views/hr_employee.xml @@ -4,7 +4,7 @@ <record id="hr_employee_view_kanban" model="ir.ui.view"> <field name="name">hr.employee</field> <field name="model">hr.employee</field> - <field name="inherit_id" ref="hr_presence.hr_employee_view_kanban"/> + <field name="inherit_id" ref="hr_presence.hr_employee_view_kanban_inherit"/> <field name="priority">20</field> <field name="arch" type="xml"> <xpath expr="//span[@name='presence_absent']" position="attributes"> diff --git a/addons/hr_contract/models/hr_contract.py b/addons/hr_contract/models/hr_contract.py index 366624ab8f4..f8662c1f55f 100644 --- a/addons/hr_contract/models/hr_contract.py +++ b/addons/hr_contract/models/hr_contract.py @@ -60,12 +60,12 @@ class Contract(models.Model): @api.constrains('employee_id', 'state', 'date_start', 'date_end') def _check_current_contract(self): - """ Two contracts in state [incoming | pending | open | close] cannot overlap """ - for contract in self.filtered(lambda c: c.state not in ['draft', 'cancel']): + """ Two contracts in state [incoming | pending | open] cannot overlap """ + for contract in self.filtered(lambda c: c.state not in ['draft', 'cancel', 'close']): domain = [ ('id', '!=', contract.id), ('employee_id', '=', contract.employee_id.id), - ('state', 'in', ['incoming', 'pending', 'open', 'close']), + ('state', 'in', ['incoming', 'pending', 'open']), ] if not contract.date_end: diff --git a/addons/hr_expense/models/hr_expense.py b/addons/hr_expense/models/hr_expense.py index 5ece94762a4..5c865fc053e 100644 --- a/addons/hr_expense/models/hr_expense.py +++ b/addons/hr_expense/models/hr_expense.py @@ -151,14 +151,14 @@ class HrExpense(models.Model): @api.model def get_empty_list_help(self, help_message): - if help_message and "oe_view_nocontent_smiling_face" not in help_message: + if help_message and "o_view_nocontent_smiling_face" not in help_message: use_mailgateway = self.env['ir.config_parameter'].sudo().get_param('hr_expense.use_mailgateway') alias_record = use_mailgateway and self.env.ref('hr_expense.mail_alias_expense') or False if alias_record and alias_record.alias_domain and alias_record.alias_name: link = "<a id='o_mail_test' href='mailto:%(email)s?subject=Lunch%%20with%%20customer%%3A%%20%%2412.32'>%(email)s</a>" % { 'email': '%s@%s' % (alias_record.alias_name, alias_record.alias_domain) } - return '<p class="oe_view_nocontent_smiling_face">%s</p><p class="oe_view_nocontent_alias">%s</p>' % ( + return '<p class="o_view_nocontent_smiling_face">%s</p><p class="oe_view_nocontent_alias">%s</p>' % ( _('Add a new expense,'), _('or send receipts by email to %s.') % (link),) return super(HrExpense, self).get_empty_list_help(help_message) @@ -212,6 +212,25 @@ class HrExpense(models.Model): # ---------------------------------------- @api.multi + def _prepare_move_values(self): + """ + This function prepares move values related to an expense + """ + self.ensure_one() + journal = self.sheet_id.bank_journal_id if self.payment_mode == 'company_account' else self.sheet_id.journal_id + account_date = self.sheet_id.accounting_date or self.date + move_values = { + 'journal_id': journal.id, + 'company_id': self.env.company.id, + 'date': account_date, + 'ref': self.sheet_id.name, + # force the name to the default value, to avoid an eventual 'default_name' in the context + # to set it to '' which cause no number to be given to the account.move when posted. + 'name': '/', + } + return move_values + + @api.multi def _get_account_move_by_sheet(self): """ Return a mapping between the expense sheet of current expense and its account move :returns dict where key is a sheet id, and value is an account move record @@ -219,18 +238,8 @@ class HrExpense(models.Model): move_grouped_by_sheet = {} for expense in self: # create the move that will contain the accounting entries - account_date = expense.sheet_id.accounting_date or expense.date if expense.sheet_id.id not in move_grouped_by_sheet: - journal = expense.sheet_id.bank_journal_id if expense.payment_mode == 'company_account' else expense.sheet_id.journal_id - move = self.env['account.move'].create({ - 'journal_id': journal.id, - 'company_id': self.env.company.id, - 'date': account_date, - 'ref': expense.sheet_id.name, - # force the name to the default value, to avoid an eventual 'default_name' in the context - # to set it to '' which cause no number to be given to the account.move when posted. - 'name': '/', - }) + move = self.env['account.move'].create(expense._prepare_move_values()) move_grouped_by_sheet[expense.sheet_id.id] = move else: move = move_grouped_by_sheet[expense.sheet_id.id] @@ -325,6 +334,7 @@ class HrExpense(models.Model): 'amount_currency': amount_currency if different_currency else 0.0, 'account_id': tax['account_id'] or move_line_src['account_id'], 'tax_repartition_line_id': tax['tax_repartition_line_id'], + 'tax_base_amount': tax['base'], 'expense_id': expense.id, 'partner_id': partner_id, 'currency_id': expense.currency_id.id if different_currency else False, diff --git a/addons/hr_holidays/models/hr_employee.py b/addons/hr_holidays/models/hr_employee.py index 459cf0a642d..3ff7b0ebcf6 100644 --- a/addons/hr_holidays/models/hr_employee.py +++ b/addons/hr_holidays/models/hr_employee.py @@ -153,8 +153,8 @@ class HrEmployeeBase(models.AbstractModel): hr_vals['manager_id'] = values['parent_id'] if values.get('department_id') is not None: hr_vals['department_id'] = values['department_id'] - holidays = self.env['hr.leave'].search(['|', ('state', 'in', ['draft', 'confirm']), ('date_from', '>', today_date), ('employee_id', 'in', self.ids)]) + holidays = self.env['hr.leave'].sudo().search(['|', ('state', 'in', ['draft', 'confirm']), ('date_from', '>', today_date), ('employee_id', 'in', self.ids)]) holidays.write(hr_vals) - allocations = self.env['hr.leave.allocation'].search([('state', 'in', ['draft', 'confirm']), ('employee_id', 'in', self.ids)]) + allocations = self.env['hr.leave.allocation'].sudo().search([('state', 'in', ['draft', 'confirm']), ('employee_id', 'in', self.ids)]) allocations.write(hr_vals) return res diff --git a/addons/hr_holidays/models/hr_leave.py b/addons/hr_holidays/models/hr_leave.py index add502f82fb..ec3ee75f609 100644 --- a/addons/hr_holidays/models/hr_leave.py +++ b/addons/hr_holidays/models/hr_leave.py @@ -13,6 +13,7 @@ from odoo import api, fields, models, tools, SUPERUSER_ID from odoo.addons.resource.models.resource import float_to_time, HOURS_PER_DAY from odoo.exceptions import AccessError, UserError, ValidationError from odoo.tools import float_compare +from odoo.tools.float_utils import float_round from odoo.tools.translate import _ _logger = logging.getLogger(__name__) @@ -146,6 +147,8 @@ class HolidaysRequest(models.Model): number_of_hours_display = fields.Float( 'Duration in hours', compute='_compute_number_of_hours_display', copy=False, readonly=True, help='Number of hours of the time off request according to your working schedule. Used for interface.') + duration_display = fields.Char('Requested (Days/Hours)', compute='_compute_duration_display', + help="Field allowing to see the leave request duration in days or hours depending on the leave_type_request_unit") # details # details meeting_id = fields.Many2one('calendar.event', string='Meeting') parent_id = fields.Many2one('hr.leave', string='Parent', copy=False) @@ -355,11 +358,17 @@ class HolidaysRequest(models.Model): self.mode_company_id = False self.department_id = False + @api.multi + def _sync_employee_details(self): + for holiday in self: + holiday.manager_id = holiday.employee_id.parent_id.id + if holiday.employee_id: + holiday.department_id = holiday.employee_id.department_id + @api.onchange('employee_id') def _onchange_employee_id(self): - self.manager_id = self.employee_id.parent_id.id - if self.employee_id: - self.department_id = self.employee_id.department_id + self._sync_employee_details() + self.holiday_status_id = False @api.onchange('date_from', 'date_to', 'employee_id') def _onchange_leave_dates(self): @@ -386,6 +395,16 @@ class HolidaysRequest(models.Model): holiday.number_of_hours_display = 0 @api.multi + @api.depends('number_of_hours_display', 'number_of_days_display') + def _compute_duration_display(self): + for leave in self: + leave.duration_display = '%g %s' % ( + (float_round(leave.number_of_hours_display, precision_digits=2) + if leave.leave_type_request_unit == 'hour' + else float_round(leave.number_of_days_display, precision_digits=2)), + _('hour(s)') if leave.leave_type_request_unit == 'hour' else _('day(s)')) + + @api.multi @api.depends('state', 'employee_id', 'department_id') def _compute_can_reset(self): for holiday in self: @@ -413,7 +432,7 @@ class HolidaysRequest(models.Model): def _check_date(self): for holiday in self: domain = [ - ('date_from', '<=', holiday.date_to), + ('date_from', '<', holiday.date_to), ('date_to', '>', holiday.date_from), ('employee_id', '=', holiday.employee_id.id), ('id', '!=', holiday.id), @@ -456,7 +475,10 @@ class HolidaysRequest(models.Model): res = [] for leave in self: if self.env.context.get('short_name'): - res.append((leave.id, _("%s : %.2f day(s)") % (leave.name or leave.holiday_status_id.name, leave.number_of_days))) + if leave.leave_type_request_unit == 'hour': + res.append((leave.id, _("%s : %.2f hour(s)") % (leave.name or leave.holiday_status_id.name, leave.number_of_hours_display))) + else: + res.append((leave.id, _("%s : %.2f day(s)") % (leave.name or leave.holiday_status_id.name, leave.number_of_days))) else: if leave.holiday_type == 'company': target = leave.mode_company_id.name @@ -466,11 +488,18 @@ class HolidaysRequest(models.Model): target = leave.category_id.name else: target = leave.employee_id.name - res.append( - (leave.id, - _("%s on %s: %.2f day(s)") % - (target, leave.holiday_status_id.name, leave.number_of_days)) - ) + if leave.leave_type_request_unit == 'hour': + res.append( + (leave.id, + _("%s on %s : %.2f hour(s)") % + (target, leave.holiday_status_id.name, leave.number_of_hours_display)) + ) + else: + res.append( + (leave.id, + _("%s on %s: %.2f day(s)") % + (target, leave.holiday_status_id.name, leave.number_of_days)) + ) return res @api.multi @@ -511,7 +540,7 @@ class HolidaysRequest(models.Model): if holiday.validation_type == 'hr': holiday.message_subscribe(partner_ids=(holiday.employee_id.parent_id.user_id.partner_id | holiday.employee_id.leave_manager_id.partner_id).ids) if employee_id: - holiday._onchange_employee_id() + holiday._sync_employee_details() if 'number_of_days' not in values and ('date_from' in values or 'date_to' in values): holiday._onchange_leave_dates() if leave_type.validation_type == 'no_validation': @@ -550,7 +579,7 @@ class HolidaysRequest(models.Model): for holiday in self: if employee_id: holiday.add_follower(employee_id) - holiday._onchange_employee_id() + self._sync_employee_details() if 'number_of_days' not in values and ('date_from' in values or 'date_to' in values): holiday._onchange_leave_dates() return result diff --git a/addons/hr_holidays/models/hr_leave_allocation.py b/addons/hr_holidays/models/hr_leave_allocation.py index 91aa37b2e2d..c400d47aedf 100644 --- a/addons/hr_holidays/models/hr_leave_allocation.py +++ b/addons/hr_holidays/models/hr_leave_allocation.py @@ -218,7 +218,10 @@ class HolidaysAllocation(models.Model): @api.depends('number_of_days', 'employee_id') def _compute_number_of_hours_display(self): for allocation in self: - allocation.number_of_hours_display = allocation.number_of_days * (allocation.employee_id.resource_calendar_id.hours_per_day or HOURS_PER_DAY) + if allocation.parent_id and allocation.parent_id.type_request_unit == "hour": + allocation.number_of_hours_display = allocation.number_of_days * HOURS_PER_DAY + else: + allocation.number_of_hours_display = allocation.number_of_days * (allocation.employee_id.resource_calendar_id.hours_per_day or HOURS_PER_DAY) @api.multi @api.depends('number_of_hours_display', 'number_of_days_display') diff --git a/addons/hr_holidays/tests/test_leave_requests.py b/addons/hr_holidays/tests/test_leave_requests.py index 737e904b438..eacd2a396e0 100644 --- a/addons/hr_holidays/tests/test_leave_requests.py +++ b/addons/hr_holidays/tests/test_leave_requests.py @@ -158,9 +158,9 @@ class TestLeaveRequests(TestHrHolidaysBase): self.employee_hrmanager.write({'department_id': self.hr_dept.id}) self.assertFalse(self.env['hr.leave'].search([('employee_id', 'in', self.hr_dept.member_ids.ids)])) leave_form = Form(self.env['hr.leave'].sudo(self.user_hrmanager), view='hr_holidays.hr_leave_view_form_manager') - leave_form.holiday_status_id = self.holidays_type_1 leave_form.holiday_type = 'department' leave_form.department_id = self.hr_dept + leave_form.holiday_status_id = self.holidays_type_1 leave = leave_form.save() leave.action_approve() member_ids = self.hr_dept.member_ids.ids diff --git a/addons/hr_holidays/views/hr_leave_views.xml b/addons/hr_holidays/views/hr_leave_views.xml index 1208b7f4ab6..8ff0691879e 100644 --- a/addons/hr_holidays/views/hr_leave_views.xml +++ b/addons/hr_holidays/views/hr_leave_views.xml @@ -387,7 +387,7 @@ <field name="name"/> <field name="date_from"/> <field name="date_to"/> - <field name="number_of_days" string="Number of Days" sum="Total time off"/> + <field name="duration_display"/> <field name="state"/> <field name="payslip_status" widget="toggle_button" options='{"active": "Reported in last payslips", "inactive": "To Report in Payslip"}' groups="hr_holidays.group_hr_holidays_manager"/> <field name="category_id" invisible="1"/> diff --git a/addons/hr_maintenance/views/hr_views.xml b/addons/hr_maintenance/views/hr_views.xml index 070328c9464..826ddcf2d44 100644 --- a/addons/hr_maintenance/views/hr_views.xml +++ b/addons/hr_maintenance/views/hr_views.xml @@ -7,7 +7,7 @@ <field name="arch" type="xml"> <xpath expr="//div[@name='button_box']" position="inside"> <button name="%(maintenance.hr_equipment_action)d" - context="{'search_default_my':1}" + context="{'search_default_employee_id': id}" groups="maintenance.group_equipment_manager" class="o_stat_button" icon="fa-cubes" diff --git a/addons/hr_org_chart/controllers/hr_org_chart.py b/addons/hr_org_chart/controllers/hr_org_chart.py index d9aeb0ce5fa..fb389f6bb51 100644 --- a/addons/hr_org_chart/controllers/hr_org_chart.py +++ b/addons/hr_org_chart/controllers/hr_org_chart.py @@ -56,7 +56,7 @@ class HrOrgChartController(http.Controller): return {} # compute employee data for org chart - ancestors, current = request.env['hr.employee.public'], employee + ancestors, current = request.env['hr.employee.public'].sudo(), employee.sudo() while current.parent_id and len(ancestors) < self._managers_level+1: ancestors += current.parent_id current = current.parent_id diff --git a/addons/hw_escpos/escpos/escpos.py b/addons/hw_escpos/escpos/escpos.py index a92b0d44eb1..d9f40d24816 100644 --- a/addons/hw_escpos/escpos/escpos.py +++ b/addons/hw_escpos/escpos/escpos.py @@ -893,14 +893,23 @@ class Escpos: self._raw(PAPER_FULL_CUT) - def cashdraw(self, pin): - """ Send pulse to kick the cash drawer """ - if pin == 2: - self._raw(CD_KICK_2) - elif pin == 5: - self._raw(CD_KICK_5) - else: - raise CashDrawerError() + def cashdraw(self, pin, tries=5): + """ Send pulse to kick the cash drawer + + With some printers the drawer will not open after one pulse, for this reason we will check + the drawer status up to 'tries' times and send a new pulse if the drawer is still closed. If the + drawer status is open, we will stop sending pulses. + """ + for i in range(tries): + if pin == 2: + self._raw(CD_KICK_2) + elif pin == 5: + self._raw(CD_KICK_5) + else: + raise CashDrawerError() + + if i != tries - 1 and not self.get_printer_status()['printer']['drawer_pin_high']: + break def hw(self, hw): diff --git a/addons/hw_scale/controllers/main.py b/addons/hw_scale/controllers/main.py index bb98a8521d0..1ebb6e39af2 100644 --- a/addons/hw_scale/controllers/main.py +++ b/addons/hw_scale/controllers/main.py @@ -8,7 +8,7 @@ import time from collections import namedtuple from os import listdir -from threading import Thread, Lock +from threading import Event, Lock, Thread from odoo import http @@ -134,6 +134,7 @@ class Scale(Thread): self.path_to_scale = '' self.protocol = None self.disabled = False + self.weight_request_event = Event() def lockedstart(self): with self.lock: @@ -261,8 +262,8 @@ class Scale(Thread): return None def get_weight(self): - self.repeats = 5 self.disabled = False + self.weight_request_event.set() self.lockedstart() return self.weight @@ -361,11 +362,13 @@ class Scale(Thread): self.device = self.get_device() if not self.device: # retry later to support "plug and play" - time.sleep(10) + self.weight_request_event.clear() + self.weight_request_event.wait(10) else: self.disabled = self.protocol.disable else: - time.sleep(10) + self.weight_request_event.clear() + self.weight_request_event.wait(10) scale_thread = None diff --git a/addons/iap/models/iap.py b/addons/iap/models/iap.py index 998eb6850b2..7dd9b6c206b 100644 --- a/addons/iap/models/iap.py +++ b/addons/iap/models/iap.py @@ -162,7 +162,7 @@ class IapAccount(models.Model): @api.model def get(self, service_name, force_create=True): - account = self.search([('service_name', '=', service_name), ('company_id', 'in', [self.env.company.id, False])]) + account = self.search([('service_name', '=', service_name), ('company_id', 'in', [self.env.company.id, False])], limit=1, order='company_id desc, id desc') if not account and force_create: account = self.create({'service_name': service_name}) # Since the account did not exist yet, we will encounter a NoCreditError, diff --git a/addons/iap/security/ir.model.access.csv b/addons/iap/security/ir.model.access.csv index 7e423f3c57a..80fd4e2212f 100644 --- a/addons/iap/security/ir.model.access.csv +++ b/addons/iap/security/ir.model.access.csv @@ -1,3 +1,3 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink -access_client_iap_account_manager,iap.account.manager,model_iap_account,base.group_system,1,1,1,0 +access_client_iap_account_manager,iap.account.manager,model_iap_account,base.group_system,1,1,1,1 access_client_iap_account_user,iap.account.user,model_iap_account,base.group_user,1,0,1,0 diff --git a/addons/im_livechat/models/im_livechat_channel.py b/addons/im_livechat/models/im_livechat_channel.py index 9ef4df884f7..93770819191 100644 --- a/addons/im_livechat/models/im_livechat_channel.py +++ b/addons/im_livechat/models/im_livechat_channel.py @@ -162,8 +162,11 @@ class ImLivechatChannel(models.Model): operator_partner_id = operator.partner_id.id # partner to add to the mail.channel channel_partner_to_add = [(4, operator_partner_id)] - if self.env.user and self.env.user.active: # valid session user (not public) - channel_partner_to_add.append((4, self.env.user.partner_id.id)) + visitor_user = False + if user_id: + visitor_user = self.env['res.users'].browse(user_id) + if visitor_user and visitor_user.active: # valid session user (not public) + channel_partner_to_add.append((4, visitor_user.partner_id.id)) # create the session, and add the link with the given channel mail_channel = self.env["mail.channel"].with_context(mail_create_nosubscribe=False).sudo().create({ 'channel_partner_ids': channel_partner_to_add, @@ -172,7 +175,7 @@ class ImLivechatChannel(models.Model): 'anonymous_name': False if user_id else anonymous_name, 'country_id': country_id, 'channel_type': 'livechat', - 'name': ', '.join([self.env['res.users'].browse(user_id).name if user_id else anonymous_name, operator.livechat_username if operator.livechat_username else operator.name]), + 'name': ', '.join([visitor_user.name if visitor_user else anonymous_name, operator.livechat_username if operator.livechat_username else operator.name]), 'public': 'private', 'email_send': False, }) diff --git a/addons/im_livechat/tests/test_get_mail_channel.py b/addons/im_livechat/tests/test_get_mail_channel.py index ac3d5807333..2c0106ea37b 100644 --- a/addons/im_livechat/tests/test_get_mail_channel.py +++ b/addons/im_livechat/tests/test_get_mail_channel.py @@ -24,6 +24,11 @@ class TestGetMailChannel(TransactionCase): 'login': 'georges' }]) + self.visitor_user = self.env['res.users'].create({ + 'name': 'Rajesh', + 'login': 'rajesh', + }) + self.livechat_channel = self.env['im_livechat.channel'].create({ 'name': 'The channel', 'user_ids': (6, 0, self.operators.ids) @@ -46,6 +51,9 @@ class TestGetMailChannel(TransactionCase): channel_operator_ids = [channel_operator[0] for channel_operator in channel_operators] self.assertTrue(all(partner_id in channel_operator_ids for partner_id in self.operators.mapped('partner_id').ids)) + visitor_user_channel = self.livechat_channel._get_mail_channel('Visitor', user_id=self.visitor_user.id) + self.assertEqual(visitor_user_channel['correspondent_name'], self.visitor_user.name, "Chat title should be correct") + def _get_mail_channels(self): mail_channels = [] diff --git a/addons/im_support/static/src/js/support_message.js b/addons/im_support/static/src/js/support_message.js index 7a4df85ca3a..92c85842aad 100644 --- a/addons/im_support/static/src/js/support_message.js +++ b/addons/im_support/static/src/js/support_message.js @@ -14,7 +14,10 @@ var SupportMessage = Message.extend({ */ init: function () { this._super.apply(this, arguments); - if (this._serverAuthorID !== this.call('mail_service', 'getOdoobotID')) { + if ( + !this._serverAuthorID || + this._serverAuthorID[0] !== this.call('mail_service', 'getOdoobotID')[0] + ) { if (!this._serverAuthorID[0]) { // the author is the client this._serverAuthorID = [session.partner_id, session.name]; diff --git a/addons/l10n_ae/__manifest__.py b/addons/l10n_ae/__manifest__.py index d9542262df7..8f9f25a7b5f 100644 --- a/addons/l10n_ae/__manifest__.py +++ b/addons/l10n_ae/__manifest__.py @@ -14,11 +14,15 @@ United Arab Emirates accounting chart and localization. """, 'depends': ['base', 'account'], 'data': [ + 'data/l10n_ae_data.xml', 'data/account_data.xml', 'data/l10n_ae_chart_data.xml', 'data/account.account.template.csv', 'data/l10n_ae_chart_post_data.xml', + 'data/account_tax_report_data.xml', 'data/account_tax_template_data.xml', + 'data/fiscal_templates_data.xml', 'data/account_chart_template_data.xml', + 'views/report_invoice_templates.xml', ], } diff --git a/addons/l10n_ae/data/account_data.xml b/addons/l10n_ae/data/account_data.xml index 48fcd567909..df9792faae5 100644 --- a/addons/l10n_ae/data/account_data.xml +++ b/addons/l10n_ae/data/account_data.xml @@ -1,9 +1,15 @@ <?xml version="1.0" encoding="utf-8"?> <odoo> - <data noupdate="1"> + <data noupdate="0"> <!-- Account Tax Group --> - <record id="tax_group_15" model="account.tax.group"> - <field name="name">Tax 15%</field> + <record id="ae_tax_group_5" model="account.tax.group"> + <field name="name">Tax 5%</field> + </record> + <record id="ae_tax_group_0" model="account.tax.group"> + <field name="name">Tax 0%</field> + </record> + <record id="ae_tax_group_exempted" model="account.tax.group"> + <field name="name">Tax Exempted</field> </record> </data> </odoo> diff --git a/addons/l10n_ae/data/account_tax_report_data.xml b/addons/l10n_ae/data/account_tax_report_data.xml new file mode 100644 index 00000000000..1f89d63f8a2 --- /dev/null +++ b/addons/l10n_ae/data/account_tax_report_data.xml @@ -0,0 +1,421 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + + <record id="tax_report_line_base_all_sales" model="account.tax.report.line"> + <field name="name">VAT on Sales and all other Outputs (Base)</field> + <field name="sequence" eval="1"/> + <field name="country_id" ref="base.ae"/> + </record> + + <record id="tax_report_line_standard_rated_supplies_base" model="account.tax.report.line"> + <field name="name">1. Standard Rated supplies (Base)</field> + <field name="parent_id" ref="tax_report_line_base_all_sales"/> + <field name="sequence" eval="1"/> + <field name="country_id" ref="base.ae"/> + </record> + + <record id="tax_report_line_standard_rated_supplies_base_abu_dhabi" model="account.tax.report.line"> + <field name="name">a. Abu Dhabi</field> + <field name="tag_name">a. Abu Dhabi</field> + <field name="code">STD_RATE_SUPP_BASE_AB</field> + <field name="parent_id" ref="tax_report_line_standard_rated_supplies_base"/> + <field name="sequence" eval="1"/> + <field name="country_id" ref="base.ae"/> + </record> + + <record id="tax_report_line_standard_rated_supplies_base_dubai" model="account.tax.report.line"> + <field name="name">b. Dubai</field> + <field name="tag_name">b. Dubai</field> + <field name="code">STD_RATE_SUPP_BASE_DB</field> + <field name="parent_id" ref="tax_report_line_standard_rated_supplies_base"/> + <field name="sequence" eval="2"/> + <field name="country_id" ref="base.ae"/> + </record> + + <record id="tax_report_line_standard_rated_supplies_base_sharjah" model="account.tax.report.line"> + <field name="name">c. Sharjah</field> + <field name="tag_name">c. Sharjah</field> + <field name="code">STD_RATE_SUPP_BASE_SJ</field> + <field name="parent_id" ref="tax_report_line_standard_rated_supplies_base"/> + <field name="sequence" eval="3"/> + <field name="country_id" ref="base.ae"/> + </record> + + <record id="tax_report_line_standard_rated_supplies_base_ajman" model="account.tax.report.line"> + <field name="name">d. Ajman</field> + <field name="tag_name">d. Ajman</field> + <field name="code">STD_RATE_SUPP_BASE_AJ</field> + <field name="parent_id" ref="tax_report_line_standard_rated_supplies_base"/> + <field name="sequence" eval="4"/> + <field name="country_id" ref="base.ae"/> + </record> + + <record id="tax_report_line_standard_rated_supplies_base_umm_al_quwain" model="account.tax.report.line"> + <field name="name">e. Umm Al Quwain</field> + <field name="tag_name">e. Umm Al Quwain</field> + <field name="code">STD_RATE_SUPP_BASE_UM</field> + <field name="parent_id" ref="tax_report_line_standard_rated_supplies_base"/> + <field name="sequence" eval="5"/> + <field name="country_id" ref="base.ae"/> + </record> + + <record id="tax_report_line_standard_rated_supplies_base_ras_al_khaima" model="account.tax.report.line"> + <field name="name">f. Ras Al-Khaima</field> + <field name="tag_name">f. Ras Al-Khaima</field> + <field name="code">STD_RATE_SUPP_BASE_RA</field> + <field name="parent_id" ref="tax_report_line_standard_rated_supplies_base"/> + <field name="sequence" eval="6"/> + <field name="country_id" ref="base.ae"/> + </record> + + <record id="tax_report_line_standard_rated_supplies_base_fujairah" model="account.tax.report.line"> + <field name="name">g. Fujairah</field> + <field name="tag_name">g. Fujairah</field> + <field name="code">STD_RATE_SUPP_BASE_FU</field> + <field name="parent_id" ref="tax_report_line_standard_rated_supplies_base"/> + <field name="sequence" eval="7"/> + <field name="country_id" ref="base.ae"/> + </record> + + <record id="tax_report_line_standard_rated_supplies_base_subtotal" model="account.tax.report.line"> + <field name="name">Sub Total</field> + <field name="formula">STD_RATE_SUPP_BASE_AB + STD_RATE_SUPP_BASE_DB + STD_RATE_SUPP_BASE_SJ + STD_RATE_SUPP_BASE_AJ + STD_RATE_SUPP_BASE_UM + STD_RATE_SUPP_BASE_RA + STD_RATE_SUPP_BASE_FU</field> + <field name="parent_id" ref="tax_report_line_standard_rated_supplies_base"/> + <field name="sequence" eval="8"/> + <field name="country_id" ref="base.ae"/> + </record> + + <record id="tax_report_line_tax_refund_tourist_base" model="account.tax.report.line"> + <field name="name">2. Tax Refunds provided to Tourists under the Tax Refunds for Tourists Scheme</field> + <field name="tag_name">2. Tax Refunds provided to Tourists under the Tax Refunds for Tourists Scheme</field> + <field name="code">TAX_REF_TOUR_SCHEME_BASE</field> + <field name="parent_id" ref="tax_report_line_base_all_sales"/> + <field name="sequence" eval="2"/> + <field name="country_id" ref="base.ae"/> + </record> + + <record id="tax_report_line_supplies_reverse_charge_base" model="account.tax.report.line"> + <field name="name">3. Supplies subject to reverse charge provisions</field> + <field name="tag_name">3. Supplies subject to reverse charge provisions</field> + <field name="code">REVERSE_CHARGE_PRO_BASE</field> + <field name="parent_id" ref="tax_report_line_base_all_sales"/> + <field name="sequence" eval="3"/> + <field name="country_id" ref="base.ae"/> + </record> + + <record id="tax_report_line_zero_rated_supplies_base" model="account.tax.report.line"> + <field name="name">4. Zero rated supplies</field> + <field name="tag_name">4. Zero rated supplies</field> + <field name="code">ZERO_RATE_SUPP_BASE</field> + <field name="parent_id" ref="tax_report_line_base_all_sales"/> + <field name="sequence" eval="4"/> + <field name="country_id" ref="base.ae"/> + </record> + + <record id="tax_report_line_exempt_supplies_base" model="account.tax.report.line"> + <field name="name">5. Exempt supplies</field> + <field name="tag_name">5. Exempt supplies</field> + <field name="code">EXAMPT_SUPP_BASE</field> + <field name="parent_id" ref="tax_report_line_base_all_sales"/> + <field name="sequence" eval="5"/> + <field name="country_id" ref="base.ae"/> + </record> + + <record id="tax_report_line_supplies_out_of_scope_base" model="account.tax.report.line"> + <field name="name">6. Out of scope</field> + <field name="code">OUT_OF_SCOPE_BASE_0</field> + <field name="parent_id" ref="tax_report_line_base_all_sales"/> + <field name="sequence" eval="6"/> + <field name="country_id" ref="base.ae"/> + </record> + + <record id="tax_report_line_import_uae_base" model="account.tax.report.line"> + <field name="name">7. Goods imported into the UAE</field> + <field name="tag_name">7. Goods imported into the UAE</field> + <field name="code">GOODS_IMPORT_IN_UAE_BASE</field> + <field name="parent_id" ref="tax_report_line_base_all_sales"/> + <field name="sequence" eval="7"/> + <field name="country_id" ref="base.ae"/> + </record> + + <record id="tax_report_line_adjustment_import_uae_base" model="account.tax.report.line"> + <field name="name">8. Adjustments to goods imported into the UAE</field> + <field name="code">ADJUST_GOODS_IMPORT_IN_UAE_BASE</field> + <field name="parent_id" ref="tax_report_line_base_all_sales"/> + <field name="sequence" eval="8"/> + <field name="country_id" ref="base.ae"/> + </record> + + <record id="tax_report_line_base_all_sales_total" model="account.tax.report.line"> + <field name="name">9. Total</field> + <field name="formula">ADJUST_GOODS_IMPORT_IN_UAE_BASE + GOODS_IMPORT_IN_UAE_BASE + OUT_OF_SCOPE_BASE_0 + EXAMPT_SUPP_BASE + ZERO_RATE_SUPP_BASE + REVERSE_CHARGE_PRO_BASE + TAX_REF_TOUR_SCHEME_BASE + (STD_RATE_SUPP_BASE_AB + STD_RATE_SUPP_BASE_DB + STD_RATE_SUPP_BASE_SJ + STD_RATE_SUPP_BASE_AJ + STD_RATE_SUPP_BASE_UM + STD_RATE_SUPP_BASE_RA + STD_RATE_SUPP_BASE_FU)</field> + <field name="parent_id" ref="tax_report_line_base_all_sales"/> + <field name="sequence" eval="9"/> + <field name="country_id" ref="base.ae"/> + </record> + + <record id="tax_report_line_base_all_expense" model="account.tax.report.line"> + <field name="name">VAT on Expenses and all other Inputs (Base)</field> + <field name="sequence" eval="2"/> + <field name="country_id" ref="base.ae"/> + </record> + + <record id="tax_report_line_standard_rated_expense_base" model="account.tax.report.line"> + <field name="name">10. Standard rated expenses</field> + <field name="tag_name">10. Standard rated expenses</field> + <field name="code">STD_RATE_EXPENSES_BASE</field> + <field name="parent_id" ref="tax_report_line_base_all_expense"/> + <field name="sequence" eval="1"/> + <field name="country_id" ref="base.ae"/> + </record> + + <record id="tax_report_line_expense_supplies_reverse_base" model="account.tax.report.line"> + <field name="name">11. Supplies subject to the reverse charge provisions</field> + <field name="tag_name">11. Supplies subject to the reverse charge provisions</field> + <field name="code">SUPP_REV_CHARGE_PRO_BASE</field> + <field name="parent_id" ref="tax_report_line_base_all_expense"/> + <field name="sequence" eval="2"/> + <field name="country_id" ref="base.ae"/> + </record> + + <record id="tax_report_line_expense_out_of_scope" model="account.tax.report.line"> + <field name="name">12. Out of scope</field> + <field name="code">OUT_OF_SCOPE_1_BASE</field> + <field name="parent_id" ref="tax_report_line_base_all_expense"/> + <field name="sequence" eval="3"/> + <field name="country_id" ref="base.ae"/> + </record> + + <record id="tax_report_line_base_all_expense_total" model="account.tax.report.line"> + <field name="name">13. Totals</field> + <field name="formula">OUT_OF_SCOPE_1_BASE + SUPP_REV_CHARGE_PRO_BASE + STD_RATE_EXPENSES_BASE</field> + <field name="parent_id" ref="tax_report_line_base_all_expense"/> + <field name="sequence" eval="4"/> + <field name="country_id" ref="base.ae"/> + </record> + + <record id="tax_report_line_vat_all_sales" model="account.tax.report.line"> + <field name="name">VAT on Sales and all other Outputs (Tax)</field> + <field name="sequence" eval="3"/> + <field name="country_id" ref="base.ae"/> + </record> + + <record id="tax_report_line_standard_rated_supplies_vat" model="account.tax.report.line"> + <field name="name">1. Standard Rated supplies (Tax)</field> + <field name="sequence" eval="1"/> + <field name="parent_id" ref="tax_report_line_vat_all_sales"/> + <field name="country_id" ref="base.ae"/> + </record> + + <record id="tax_report_line_standard_rated_supplies_vat_abu_dhabi" model="account.tax.report.line"> + <field name="name">a. Abu Dhabi</field> + <field name="tag_name">a. Abu Dhabi</field> + <field name="code">STD_RATE_SUPP_TAX_AB</field> + <field name="parent_id" ref="tax_report_line_standard_rated_supplies_vat"/> + <field name="sequence" eval="1"/> + <field name="country_id" ref="base.ae"/> + </record> + + <record id="tax_report_line_standard_rated_supplies_vat_dubai" model="account.tax.report.line"> + <field name="name">b. Dubai</field> + <field name="tag_name">b. Dubai</field> + <field name="code">STD_RATE_SUPP_TAX_DB</field> + <field name="parent_id" ref="tax_report_line_standard_rated_supplies_vat"/> + <field name="sequence" eval="2"/> + <field name="country_id" ref="base.ae"/> + </record> + + <record id="tax_report_line_standard_rated_supplies_vat_sharjah" model="account.tax.report.line"> + <field name="name">c. Sharjah</field> + <field name="tag_name">c. Sharjah</field> + <field name="code">STD_RATE_SUPP_TAX_SJ</field> + <field name="parent_id" ref="tax_report_line_standard_rated_supplies_vat"/> + <field name="sequence" eval="3"/> + <field name="country_id" ref="base.ae"/> + </record> + + <record id="tax_report_line_standard_rated_supplies_vat_ajman" model="account.tax.report.line"> + <field name="name">d. Ajman</field> + <field name="tag_name">d. Ajman</field> + <field name="code">STD_RATE_SUPP_TAX_AJ</field> + <field name="parent_id" ref="tax_report_line_standard_rated_supplies_vat"/> + <field name="sequence" eval="4"/> + <field name="country_id" ref="base.ae"/> + </record> + + <record id="tax_report_line_standard_rated_supplies_vat_umm_al_quwain" model="account.tax.report.line"> + <field name="name">e. Umm Al Quwain</field> + <field name="tag_name">e. Umm Al Quwain</field> + <field name="code">STD_RATE_SUPP_TAX_UM</field> + <field name="parent_id" ref="tax_report_line_standard_rated_supplies_vat"/> + <field name="sequence" eval="5"/> + <field name="country_id" ref="base.ae"/> + </record> + + <record id="tax_report_line_standard_rated_supplies_vat_ras_al_khaima" model="account.tax.report.line"> + <field name="name">f. Ras Al-Khaima</field> + <field name="tag_name">f. Ras Al-Khaima</field> + <field name="code">STD_RATE_SUPP_TAX_RA</field> + <field name="parent_id" ref="tax_report_line_standard_rated_supplies_vat"/> + <field name="sequence" eval="6"/> + <field name="country_id" ref="base.ae"/> + </record> + + <record id="tax_report_line_standard_rated_supplies_vat_fujairah" model="account.tax.report.line"> + <field name="name">g. Fujairah</field> + <field name="tag_name">g. Fujairah</field> + <field name="code">STD_RATE_SUPP_TAX_FU</field> + <field name="parent_id" ref="tax_report_line_standard_rated_supplies_vat"/> + <field name="sequence" eval="7"/> + <field name="country_id" ref="base.ae"/> + </record> + + <record id="tax_report_line_standard_rated_supplies_vat_subtotal" model="account.tax.report.line"> + <field name="name">Sub Total</field> + <field name="formula">STD_RATE_SUPP_TAX_AB + STD_RATE_SUPP_TAX_DB + STD_RATE_SUPP_TAX_SJ + STD_RATE_SUPP_TAX_AJ + STD_RATE_SUPP_TAX_UM + STD_RATE_SUPP_TAX_RA + STD_RATE_SUPP_TAX_FU</field> + <field name="parent_id" ref="tax_report_line_standard_rated_supplies_vat"/> + <field name="sequence" eval="8"/> + <field name="country_id" ref="base.ae"/> + </record> + + <record id="tax_report_line_tax_refund_tourist_vat" model="account.tax.report.line"> + <field name="name">2. Tax Refunds provided to Tourists under the Tax Refunds for Tourists Scheme</field> + <field name="tag_name">2. Tax Refunds provided to Tourists under the Tax Refunds for Tourists Scheme</field> + <field name="code">TAX_REF_TOUR_SCHEME_TAX</field> + <field name="parent_id" ref="tax_report_line_vat_all_sales"/> + <field name="sequence" eval="2"/> + <field name="country_id" ref="base.ae"/> + </record> + + <record id="tax_report_line_supplies_reverse_charge_vat" model="account.tax.report.line"> + <field name="name">3. Supplies subject to reverse charge provisions</field> + <field name="tag_name">3. Supplies subject to reverse charge provisions</field> + <field name="code">REVERSE_CHARGE_PRO_TAX</field> + <field name="parent_id" ref="tax_report_line_vat_all_sales"/> + <field name="sequence" eval="3"/> + <field name="country_id" ref="base.ae"/> + </record> + + <record id="tax_report_line_zero_rated_supplies_vat" model="account.tax.report.line"> + <field name="name">4. Zero rated supplies</field> + <field name="tag_name">4. Zero rated supplies</field> + <field name="code">ZERO_RATE_SUPP_TAX</field> + <field name="parent_id" ref="tax_report_line_vat_all_sales"/> + <field name="sequence" eval="4"/> + <field name="country_id" ref="base.ae"/> + </record> + + <record id="tax_report_line_exempt_supplies_vat" model="account.tax.report.line"> + <field name="name">5. Exempt supplies</field> + <field name="tag_name">5. Exempt supplies</field> + <field name="code">EXAMPT_SUPP_TAX</field> + <field name="parent_id" ref="tax_report_line_vat_all_sales"/> + <field name="sequence" eval="5"/> + <field name="country_id" ref="base.ae"/> + </record> + + <record id="tax_report_line_supplies_out_of_scope_vat" model="account.tax.report.line"> + <field name="name">6. Out of scope</field> + <field name="code">OUT_OF_SCOPE_TAX_0</field> + <field name="parent_id" ref="tax_report_line_vat_all_sales"/> + <field name="sequence" eval="6"/> + <field name="country_id" ref="base.ae"/> + </record> + + <record id="tax_report_line_import_uae_vat" model="account.tax.report.line"> + <field name="name">7. Goods imported into the UAE</field> + <field name="tag_name">7. Goods imported into the UAE</field> + <field name="code">GOODS_IMPORT_IN_UAE_TAX</field> + <field name="parent_id" ref="tax_report_line_vat_all_sales"/> + <field name="sequence" eval="7"/> + <field name="country_id" ref="base.ae"/> + </record> + + <record id="tax_report_line_adjustment_import_uae_vat" model="account.tax.report.line"> + <field name="name">8. Adjustments to goods imported into the UAE</field> + <field name="code">ADJUST_GOODS_IMPORT_IN_UAE_TAX</field> + <field name="parent_id" ref="tax_report_line_vat_all_sales"/> + <field name="sequence" eval="8"/> + <field name="country_id" ref="base.ae"/> + </record> + + <record id="tax_report_line_vat_all_sales_total" model="account.tax.report.line"> + <field name="name">9. Total</field> + <field name="formula">(STD_RATE_SUPP_TAX_AB + STD_RATE_SUPP_TAX_DB + STD_RATE_SUPP_TAX_SJ + STD_RATE_SUPP_TAX_AJ + STD_RATE_SUPP_TAX_UM + STD_RATE_SUPP_TAX_RA + STD_RATE_SUPP_TAX_FU) + OUT_OF_SCOPE_TAX_0 + ADJUST_GOODS_IMPORT_IN_UAE_TAX + GOODS_IMPORT_IN_UAE_TAX + EXAMPT_SUPP_TAX + ZERO_RATE_SUPP_TAX + REVERSE_CHARGE_PRO_TAX + TAX_REF_TOUR_SCHEME_TAX</field> + <field name="parent_id" ref="tax_report_line_vat_all_sales"/> + <field name="sequence" eval="9"/> + <field name="country_id" ref="base.ae"/> + </record> + + <record id="tax_report_line_vat_all_expense" model="account.tax.report.line"> + <field name="name">VAT on Expenses and all other Inputs (Tax)</field> + <field name="sequence" eval="4"/> + <field name="country_id" ref="base.ae"/> + </record> + + <record id="tax_report_line_standard_rated_expense_vat" model="account.tax.report.line"> + <field name="name">10. Standard rated expenses</field> + <field name="tag_name">10. Standard rated expenses</field> + <field name="code">STD_RATE_EXPENSES_TAX</field> + <field name="parent_id" ref="tax_report_line_vat_all_expense"/> + <field name="sequence" eval="1"/> + <field name="country_id" ref="base.ae"/> + </record> + + <record id="tax_report_line_expense_supplies_reverse_vat" model="account.tax.report.line"> + <field name="name">11. Supplies subject to the reverse charge provisions</field> + <field name="tag_name">11. Supplies subject to the reverse charge provisions</field> + <field name="code">SUPP_REV_CHARGE_PRO_TAX</field> + <field name="parent_id" ref="tax_report_line_vat_all_expense"/> + <field name="sequence" eval="2"/> + <field name="country_id" ref="base.ae"/> + </record> + + <record id="tax_report_line_expense_out_of_scope_vat" model="account.tax.report.line"> + <field name="name">12. Out of scope</field> + <field name="code">OUT_OF_SCOPE_1_TAX</field> + <field name="parent_id" ref="tax_report_line_vat_all_expense"/> + <field name="sequence" eval="3"/> + <field name="country_id" ref="base.ae"/> + </record> + + <record id="tax_report_line_vat_all_expense_total" model="account.tax.report.line"> + <field name="name">13. Totals</field> + <field name="formula">OUT_OF_SCOPE_1_TAX + SUPP_REV_CHARGE_PRO_TAX + STD_RATE_EXPENSES_TAX</field> + <field name="parent_id" ref="tax_report_line_vat_all_expense"/> + <field name="sequence" eval="4"/> + <field name="country_id" ref="base.ae"/> + </record> + + <record id="tax_report_line_net_vat_due" model="account.tax.report.line"> + <field name="name">Net VAT Due</field> + <field name="sequence" eval="5"/> + <field name="formula">((STD_RATE_SUPP_TAX_AB + STD_RATE_SUPP_TAX_DB + STD_RATE_SUPP_TAX_SJ + STD_RATE_SUPP_TAX_AJ + STD_RATE_SUPP_TAX_UM + STD_RATE_SUPP_TAX_RA + STD_RATE_SUPP_TAX_FU) + OUT_OF_SCOPE_TAX_0 + ADJUST_GOODS_IMPORT_IN_UAE_TAX + GOODS_IMPORT_IN_UAE_TAX + EXAMPT_SUPP_TAX + ZERO_RATE_SUPP_TAX + REVERSE_CHARGE_PRO_TAX + TAX_REF_TOUR_SCHEME_TAX) - (OUT_OF_SCOPE_1_TAX + SUPP_REV_CHARGE_PRO_TAX + STD_RATE_EXPENSES_TAX)</field> + <field name="country_id" ref="base.ae"/> + </record> + + <record id="tax_report_line_total_value_due_tax_period" model="account.tax.report.line"> + <field name="name">14. Total value of due tax for the period</field> + <field name="formula">(STD_RATE_SUPP_TAX_AB + STD_RATE_SUPP_TAX_DB + STD_RATE_SUPP_TAX_SJ + STD_RATE_SUPP_TAX_AJ + STD_RATE_SUPP_TAX_UM + STD_RATE_SUPP_TAX_RA + STD_RATE_SUPP_TAX_FU) + OUT_OF_SCOPE_TAX_0 + ADJUST_GOODS_IMPORT_IN_UAE_TAX + GOODS_IMPORT_IN_UAE_TAX + EXAMPT_SUPP_TAX + ZERO_RATE_SUPP_TAX + REVERSE_CHARGE_PRO_TAX + TAX_REF_TOUR_SCHEME_TAX</field> + <field name="parent_id" ref="tax_report_line_net_vat_due"/> + <field name="sequence" eval="1"/> + <field name="country_id" ref="base.ae"/> + </record> + + <record id="tax_report_line_total_value_recoverable_tax_period" model="account.tax.report.line"> + <field name="name">15. Total value of recoverable tax for the period</field> + <field name="formula">OUT_OF_SCOPE_1_TAX + SUPP_REV_CHARGE_PRO_TAX + STD_RATE_EXPENSES_TAX</field> + <field name="parent_id" ref="tax_report_line_net_vat_due"/> + <field name="sequence" eval="2"/> + <field name="country_id" ref="base.ae"/> + </record> + + <record id="tax_report_line_net_vat_due_period" model="account.tax.report.line"> + <field name="name">16. Net VAT due (or reclaimed) for the period</field> + <field name="formula">((STD_RATE_SUPP_TAX_AB + STD_RATE_SUPP_TAX_DB + STD_RATE_SUPP_TAX_SJ + STD_RATE_SUPP_TAX_AJ + STD_RATE_SUPP_TAX_UM + STD_RATE_SUPP_TAX_RA + STD_RATE_SUPP_TAX_FU) + OUT_OF_SCOPE_TAX_0 + ADJUST_GOODS_IMPORT_IN_UAE_TAX + GOODS_IMPORT_IN_UAE_TAX + EXAMPT_SUPP_TAX + ZERO_RATE_SUPP_TAX + REVERSE_CHARGE_PRO_TAX + TAX_REF_TOUR_SCHEME_TAX) - (OUT_OF_SCOPE_1_TAX + SUPP_REV_CHARGE_PRO_TAX + STD_RATE_EXPENSES_TAX)</field> + <field name="parent_id" ref="tax_report_line_net_vat_due"/> + <field name="sequence" eval="3"/> + <field name="country_id" ref="base.ae"/> + </record> + +</odoo> diff --git a/addons/l10n_ae/data/account_tax_template_data.xml b/addons/l10n_ae/data/account_tax_template_data.xml index 198d573bc10..15ae5ae9869 100644 --- a/addons/l10n_ae/data/account_tax_template_data.xml +++ b/addons/l10n_ae/data/account_tax_template_data.xml @@ -1,52 +1,787 @@ <?xml version="1.0" encoding="utf-8"?> <odoo> - <record id="sale_tax_template" model="account.tax.template"> + <record id="uae_sale_tax_5_dubai" model="account.tax.template"> + <field name="name">TVA 5% (Dubai)</field> + <field name="type_tax_use">sale</field> + <field name="amount">5</field> + <field name="amount_type">percent</field> + <field name="description">TVA 5%</field> + <field name="tax_group_id" ref="ae_tax_group_5"/> <field name="chart_template_id" ref="uae_chart_template_standard"/> - <field name="name">Tax 10%</field> - <field name="amount">10</field> + <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'plus_report_line_ids': [ref('tax_report_line_standard_rated_supplies_base_dubai')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('uae_account_3785'), + 'plus_report_line_ids': [ref('tax_report_line_standard_rated_supplies_vat_dubai')], + }), + ]"/> + <field name="refund_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'minus_report_line_ids': [ref('tax_report_line_standard_rated_supplies_base_dubai')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('uae_account_3785'), + 'minus_report_line_ids': [ref('tax_report_line_standard_rated_supplies_vat_dubai')], + }), + ]"/> + </record> + <record id="uae_sale_tax_5_abu_dhabi" model="account.tax.template"> + <field name="name">TVA 5% (Abu Dhabi)</field> + <field name="type_tax_use">sale</field> + <field name="amount">5</field> + <field name="amount_type">percent</field> + <field name="description">TVA 5%</field> + <field name="tax_group_id" ref="ae_tax_group_5"/> + <field name="chart_template_id" ref="uae_chart_template_standard"/> + <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'plus_report_line_ids': [ref('tax_report_line_standard_rated_supplies_base_abu_dhabi')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('uae_account_3785'), + 'plus_report_line_ids': [ref('tax_report_line_standard_rated_supplies_vat_abu_dhabi')], + }), + ]"/> + <field name="refund_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'minus_report_line_ids': [ref('tax_report_line_standard_rated_supplies_base_abu_dhabi')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('uae_account_3785'), + 'minus_report_line_ids': [ref('tax_report_line_standard_rated_supplies_vat_abu_dhabi')], + }), + ]"/> + </record> + <record id="uae_sale_tax_5_sharjah" model="account.tax.template"> + <field name="name">TVA 5% (Sharjah)</field> <field name="type_tax_use">sale</field> - <field name="tax_group_id" ref="tax_group_15"/> + <field name="amount">5</field> + <field name="amount_type">percent</field> + <field name="description">TVA 5%</field> + <field name="tax_group_id" ref="ae_tax_group_5"/> + <field name="chart_template_id" ref="uae_chart_template_standard"/> <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), (0,0, { 'factor_percent': 100, 'repartition_type': 'base', + 'plus_report_line_ids': [ref('tax_report_line_standard_rated_supplies_base_sharjah')], }), - (0,0, { 'factor_percent': 100, 'repartition_type': 'tax', 'account_id': ref('uae_account_3785'), + 'plus_report_line_ids': [ref('tax_report_line_standard_rated_supplies_vat_sharjah')], }), ]"/> <field name="refund_repartition_line_ids" eval="[(5, 0, 0), (0,0, { 'factor_percent': 100, 'repartition_type': 'base', + 'minus_report_line_ids': [ref('tax_report_line_standard_rated_supplies_base_sharjah')], }), - (0,0, { 'factor_percent': 100, 'repartition_type': 'tax', 'account_id': ref('uae_account_3785'), + 'minus_report_line_ids': [ref('tax_report_line_standard_rated_supplies_vat_sharjah')], }), ]"/> </record> - <record id="purchase_tax_template" model="account.tax.template"> + <record id="uae_sale_tax_5_ajman" model="account.tax.template"> + <field name="name">TVA 5% (Ajman)</field> + <field name="type_tax_use">sale</field> + <field name="amount">5</field> + <field name="amount_type">percent</field> + <field name="description">TVA 5%</field> + <field name="tax_group_id" ref="ae_tax_group_5"/> <field name="chart_template_id" ref="uae_chart_template_standard"/> - <field name="name">Purchase Tax 10%</field> - <field name="amount">10</field> + <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'plus_report_line_ids': [ref('tax_report_line_standard_rated_supplies_base_ajman')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('uae_account_3785'), + 'plus_report_line_ids': [ref('tax_report_line_standard_rated_supplies_vat_ajman')], + }), + ]"/> + <field name="refund_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'minus_report_line_ids': [ref('tax_report_line_standard_rated_supplies_base_ajman')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('uae_account_3785'), + 'minus_report_line_ids': [ref('tax_report_line_standard_rated_supplies_vat_ajman')], + }), + ]"/> + </record> + <record id="uae_sale_tax_5_umm_al_quwain" model="account.tax.template"> + <field name="name">TVA 5% (Umm Al Quwain)</field> + <field name="type_tax_use">sale</field> + <field name="amount">5</field> + <field name="amount_type">percent</field> + <field name="description">TVA 5%</field> + <field name="tax_group_id" ref="ae_tax_group_5"/> + <field name="chart_template_id" ref="uae_chart_template_standard"/> + <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'plus_report_line_ids': [ref('tax_report_line_standard_rated_supplies_base_umm_al_quwain')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('uae_account_3785'), + 'plus_report_line_ids': [ref('tax_report_line_standard_rated_supplies_vat_umm_al_quwain')], + }), + ]"/> + <field name="refund_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'minus_report_line_ids': [ref('tax_report_line_standard_rated_supplies_base_umm_al_quwain')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('uae_account_3785'), + 'minus_report_line_ids': [ref('tax_report_line_standard_rated_supplies_vat_umm_al_quwain')], + }), + ]"/> + </record> + <record id="uae_sale_tax_5_ras_al_khaima" model="account.tax.template"> + <field name="name">TVA 5% (Ras Al-Khaima)</field> + <field name="type_tax_use">sale</field> + <field name="amount">5</field> + <field name="amount_type">percent</field> + <field name="description">TVA 5%</field> + <field name="tax_group_id" ref="ae_tax_group_5"/> + <field name="chart_template_id" ref="uae_chart_template_standard"/> + <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'plus_report_line_ids': [ref('tax_report_line_standard_rated_supplies_base_ras_al_khaima')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('uae_account_3785'), + 'plus_report_line_ids': [ref('tax_report_line_standard_rated_supplies_vat_ras_al_khaima')], + }), + ]"/> + <field name="refund_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'minus_report_line_ids': [ref('tax_report_line_standard_rated_supplies_base_ras_al_khaima')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('uae_account_3785'), + 'minus_report_line_ids': [ref('tax_report_line_standard_rated_supplies_vat_ras_al_khaima')], + }), + ]"/> + </record> + <record id="uae_sale_tax_5_fujairah" model="account.tax.template"> + <field name="name">TVA 5% (Fujairah)</field> + <field name="type_tax_use">sale</field> + <field name="amount">5</field> + <field name="amount_type">percent</field> + <field name="description">TVA 5%</field> + <field name="tax_group_id" ref="ae_tax_group_5"/> + <field name="chart_template_id" ref="uae_chart_template_standard"/> + <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'plus_report_line_ids': [ref('tax_report_line_standard_rated_supplies_base_fujairah')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('uae_account_3785'), + 'plus_report_line_ids': [ref('tax_report_line_standard_rated_supplies_vat_fujairah')], + }), + ]"/> + <field name="refund_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'minus_report_line_ids': [ref('tax_report_line_standard_rated_supplies_base_fujairah')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('uae_account_3785'), + 'minus_report_line_ids': [ref('tax_report_line_standard_rated_supplies_vat_fujairah')], + }), + ]"/> + </record> + <record id="uae_sale_tax_exempted" model="account.tax.template"> + <field name="name">Exempted Tax</field> + <field name="type_tax_use">sale</field> + <field name="amount">0</field> + <field name="amount_type">percent</field> + <field name="description">Exempted</field> + <field name="tax_group_id" ref="ae_tax_group_exempted"/> + <field name="chart_template_id" ref="uae_chart_template_standard"/> + <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'plus_report_line_ids': [ref('tax_report_line_exempt_supplies_base')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('uae_account_3785'), + }), + ]"/> + <field name="refund_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'minus_report_line_ids': [ref('tax_report_line_exempt_supplies_base')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('uae_account_3785'), + }), + ]"/> + </record> + <record id="uae_sale_tax_0" model="account.tax.template"> + <field name="name">TVA 0%</field> + <field name="type_tax_use">sale</field> + <field name="amount">0</field> + <field name="amount_type">percent</field> + <field name="description">TVA 0%</field> + <field name="tax_group_id" ref="ae_tax_group_0"/> + <field name="chart_template_id" ref="uae_chart_template_standard"/> + <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'plus_report_line_ids': [ref('tax_report_line_zero_rated_supplies_base')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('uae_account_3785'), + }), + ]"/> + <field name="refund_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'minus_report_line_ids': [ref('tax_report_line_zero_rated_supplies_base')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('uae_account_3785'), + }), + ]"/> + </record> + <record id="uae_export_tax" model="account.tax.template"> + <field name="name">Export Tax 0%</field> + <field name="type_tax_use">sale</field> + <field name="amount">0</field> + <field name="amount_type">percent</field> + <field name="description">Export Tax</field> + <field name="chart_template_id" ref="uae_chart_template_standard"/> + <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + }), + ]"/> + <field name="refund_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + }), + ]"/> + </record> + <record id="uae_sale_tax_reverse_charge_dubai" model="account.tax.template"> + <field name="name">Reverse Charge Provision (Dubai)</field> + <field name="type_tax_use">sale</field> + <field name="amount">5</field> + <field name="amount_type">percent</field> + <field name="description">Supplies subject to reverse charge provisions</field> + <field name="chart_template_id" ref="uae_chart_template_standard"/> + <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'plus_report_line_ids': [ref('tax_report_line_supplies_reverse_charge_base'),ref('tax_report_line_standard_rated_supplies_base_dubai')], + }), + (0,0, { + 'factor_percent': -100, + 'repartition_type': 'tax', + 'account_id': ref('uae_account_3726'), + 'minus_report_line_ids': [ref('tax_report_line_supplies_reverse_charge_vat')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('uae_account_3785'), + 'plus_report_line_ids': [ref('tax_report_line_standard_rated_supplies_vat_dubai')], + }), + ]"/> + <field name="refund_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'minus_report_line_ids': [ref('tax_report_line_supplies_reverse_charge_base'),ref('tax_report_line_standard_rated_supplies_base_dubai')], + }), + (0,0, { + 'factor_percent': -100, + 'repartition_type': 'tax', + 'account_id': ref('uae_account_3726'), + 'plus_report_line_ids': [ref('tax_report_line_supplies_reverse_charge_vat')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('uae_account_3785'), + 'minus_report_line_ids': [ref('tax_report_line_standard_rated_supplies_vat_dubai')], + }), + ]"/> + </record> + <record id="uae_sale_tax_reverse_charge_abu_dhabi" model="account.tax.template"> + <field name="name">Reverse Charge Provision (Abi Dhabi)</field> + <field name="type_tax_use">sale</field> + <field name="amount">5</field> + <field name="amount_type">percent</field> + <field name="description">Supplies subject to reverse charge provisions</field> + <field name="chart_template_id" ref="uae_chart_template_standard"/> + <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'plus_report_line_ids': [ref('tax_report_line_supplies_reverse_charge_base'),ref('tax_report_line_standard_rated_supplies_base_abu_dhabi')], + }), + (0,0, { + 'factor_percent': -100, + 'repartition_type': 'tax', + 'account_id': ref('uae_account_3726'), + 'plus_report_line_ids': [ref('tax_report_line_supplies_reverse_charge_vat')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('uae_account_3785'), + 'plus_report_line_ids': [ref('tax_report_line_standard_rated_supplies_vat_abu_dhabi')], + }), + ]"/> + <field name="refund_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'minus_report_line_ids': [ref('tax_report_line_supplies_reverse_charge_base'),ref('tax_report_line_standard_rated_supplies_base_abu_dhabi')], + }), + (0,0, { + 'factor_percent': -100, + 'repartition_type': 'tax', + 'account_id': ref('uae_account_3726'), + 'minus_report_line_ids': [ref('tax_report_line_supplies_reverse_charge_vat')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('uae_account_3785'), + 'minus_report_line_ids': [ref('tax_report_line_standard_rated_supplies_vat_abu_dhabi')], + }), + ]"/> + </record> + <record id="uae_sale_tax_reverse_charge_sharjah" model="account.tax.template"> + <field name="name">Reverse Charge Provision (Sharjah)</field> + <field name="type_tax_use">sale</field> + <field name="amount">5</field> + <field name="amount_type">percent</field> + <field name="description">Supplies subject to reverse charge provisions</field> + <field name="chart_template_id" ref="uae_chart_template_standard"/> + <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'plus_report_line_ids': [ref('tax_report_line_supplies_reverse_charge_base'),ref('tax_report_line_standard_rated_supplies_base_sharjah')], + }), + (0,0, { + 'factor_percent': -100, + 'repartition_type': 'tax', + 'account_id': ref('uae_account_3726'), + 'plus_report_line_ids': [ref('tax_report_line_supplies_reverse_charge_vat')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('uae_account_3785'), + 'plus_report_line_ids': [ref('tax_report_line_standard_rated_supplies_vat_sharjah')], + }), + ]"/> + <field name="refund_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'minus_report_line_ids': [ref('tax_report_line_supplies_reverse_charge_base'),ref('tax_report_line_standard_rated_supplies_base_sharjah')], + }), + (0,0, { + 'factor_percent': -100, + 'repartition_type': 'tax', + 'account_id': ref('uae_account_3726'), + 'minus_report_line_ids': [ref('tax_report_line_supplies_reverse_charge_vat')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('uae_account_3785'), + 'minus_report_line_ids': [ref('tax_report_line_standard_rated_supplies_vat_sharjah')], + }), + ]"/> + </record> + <record id="uae_sale_tax_reverse_charge_ajman" model="account.tax.template"> + <field name="name">Reverse Charge Provision (Ajman)</field> + <field name="type_tax_use">sale</field> + <field name="amount">5</field> + <field name="amount_type">percent</field> + <field name="description">Supplies subject to reverse charge provisions</field> + <field name="chart_template_id" ref="uae_chart_template_standard"/> + <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'plus_report_line_ids': [ref('tax_report_line_supplies_reverse_charge_base'),ref('tax_report_line_standard_rated_supplies_base_ajman')], + }), + (0,0, { + 'factor_percent': -100, + 'repartition_type': 'tax', + 'account_id': ref('uae_account_3726'), + 'plus_report_line_ids': [ref('tax_report_line_supplies_reverse_charge_vat')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('uae_account_3785'), + 'plus_report_line_ids': [ref('tax_report_line_standard_rated_supplies_vat_ajman')], + }), + ]"/> + <field name="refund_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'minus_report_line_ids': [ref('tax_report_line_supplies_reverse_charge_base'),ref('tax_report_line_standard_rated_supplies_base_ajman')], + }), + (0,0, { + 'factor_percent': -100, + 'repartition_type': 'tax', + 'account_id': ref('uae_account_3726'), + 'minus_report_line_ids': [ref('tax_report_line_supplies_reverse_charge_vat')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('uae_account_3785'), + 'minus_report_line_ids': [ref('tax_report_line_standard_rated_supplies_vat_ajman')], + }), + ]"/> + </record> + <record id="uae_sale_tax_reverse_charge_umm_al_quwain" model="account.tax.template"> + <field name="name">Reverse Charge Provision (Umm Al Quwain)</field> + <field name="type_tax_use">sale</field> + <field name="amount">5</field> + <field name="amount_type">percent</field> + <field name="description">Supplies subject to reverse charge provisions</field> + <field name="chart_template_id" ref="uae_chart_template_standard"/> + <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'plus_report_line_ids': [ref('tax_report_line_supplies_reverse_charge_base'),ref('tax_report_line_standard_rated_supplies_base_umm_al_quwain')], + }), + (0,0, { + 'factor_percent': -100, + 'repartition_type': 'tax', + 'account_id': ref('uae_account_3726'), + 'plus_report_line_ids': [ref('tax_report_line_supplies_reverse_charge_vat')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('uae_account_3785'), + 'plus_report_line_ids': [ref('tax_report_line_standard_rated_supplies_vat_umm_al_quwain')], + }), + ]"/> + <field name="refund_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'minus_report_line_ids': [ref('tax_report_line_supplies_reverse_charge_base'),ref('tax_report_line_standard_rated_supplies_base_umm_al_quwain')], + }), + (0,0, { + 'factor_percent': -100, + 'repartition_type': 'tax', + 'account_id': ref('uae_account_3726'), + 'minus_report_line_ids': [ref('tax_report_line_supplies_reverse_charge_vat')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('uae_account_3785'), + 'minus_report_line_ids': [ref('tax_report_line_standard_rated_supplies_vat_umm_al_quwain')], + }), + ]"/> + </record> + <record id="uae_sale_tax_reverse_charge_ras_al_khaima" model="account.tax.template"> + <field name="name">Reverse Charge Provision (Ras Al-Khaima)</field> + <field name="type_tax_use">sale</field> + <field name="amount">5</field> + <field name="amount_type">percent</field> + <field name="description">Supplies subject to reverse charge provisions</field> + <field name="chart_template_id" ref="uae_chart_template_standard"/> + <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'plus_report_line_ids': [ref('tax_report_line_supplies_reverse_charge_base'),ref('tax_report_line_standard_rated_supplies_base_ras_al_khaima')], + }), + (0,0, { + 'factor_percent': -100, + 'repartition_type': 'tax', + 'account_id': ref('uae_account_3726'), + 'plus_report_line_ids': [ref('tax_report_line_supplies_reverse_charge_vat')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('uae_account_3785'), + 'plus_report_line_ids': [ref('tax_report_line_standard_rated_supplies_vat_ras_al_khaima')], + }), + ]"/> + <field name="refund_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'minus_report_line_ids': [ref('tax_report_line_supplies_reverse_charge_base'),ref('tax_report_line_standard_rated_supplies_base_ras_al_khaima')], + }), + (0,0, { + 'factor_percent': -100, + 'repartition_type': 'tax', + 'account_id': ref('uae_account_3726'), + 'minus_report_line_ids': [ref('tax_report_line_supplies_reverse_charge_vat')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('uae_account_3785'), + 'minus_report_line_ids': [ref('tax_report_line_standard_rated_supplies_vat_ras_al_khaima')], + }), + ]"/> + </record> + <record id="uae_sale_tax_reverse_charge_fujairah" model="account.tax.template"> + <field name="name">Reverse Charge Provision (Fujairah)</field> + <field name="type_tax_use">sale</field> + <field name="amount">5</field> + <field name="amount_type">percent</field> + <field name="description">Supplies subject to reverse charge provisions</field> + <field name="chart_template_id" ref="uae_chart_template_standard"/> + <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'plus_report_line_ids': [ref('tax_report_line_supplies_reverse_charge_base'),ref('tax_report_line_standard_rated_supplies_base_fujairah')], + }), + (0,0, { + 'factor_percent': -100, + 'repartition_type': 'tax', + 'account_id': ref('uae_account_3726'), + 'plus_report_line_ids': [ref('tax_report_line_supplies_reverse_charge_vat')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('uae_account_3785'), + 'plus_report_line_ids': [ref('tax_report_line_standard_rated_supplies_vat_fujairah')], + }), + ]"/> + <field name="refund_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'minus_report_line_ids': [ref('tax_report_line_supplies_reverse_charge_base'),ref('tax_report_line_standard_rated_supplies_base_fujairah')], + }), + (0,0, { + 'factor_percent': -100, + 'repartition_type': 'tax', + 'account_id': ref('uae_account_3726'), + 'minus_report_line_ids': [ref('tax_report_line_supplies_reverse_charge_vat')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('uae_account_3785'), + 'minus_report_line_ids': [ref('tax_report_line_standard_rated_supplies_vat_fujairah')], + }), + ]"/> + </record> + <record id="uae_sale_tax_tourist_refund" model="account.tax.template"> + <field name="name">Tourist Refund scheme 5%</field> + <field name="type_tax_use">sale</field> + <field name="amount">5.0</field> + <field name="amount_type">percent</field> + <field name="description">Tax Refunds provided to Tourists under the Tax Refunds for Tourists Scheme</field> + <field name="chart_template_id" ref="uae_chart_template_standard"/> + <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'plus_report_line_ids': [ref('tax_report_line_tax_refund_tourist_base')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('uae_account_3726'), + 'plus_report_line_ids': [ref('tax_report_line_tax_refund_tourist_vat')], + }), + ]"/> + <field name="refund_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'minus_report_line_ids': [ref('tax_report_line_tax_refund_tourist_base')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('uae_account_3726'), + 'minus_report_line_ids': [ref('tax_report_line_tax_refund_tourist_vat')], + }), + ]"/> + </record> + <!-- purchase taxes --> + <record id="uae_purchase_tax_5" model="account.tax.template"> + <field name="name">TVA 5%</field> + <field name="type_tax_use">purchase</field> + <field name="amount">5</field> + <field name="amount_type">percent</field> + <field name="description">TVA 5%</field> + <field name="tax_group_id" ref="ae_tax_group_5"/> + <field name="chart_template_id" ref="uae_chart_template_standard"/> + <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'plus_report_line_ids': [ref('tax_report_line_standard_rated_expense_base')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('uae_account_3726'), + 'plus_report_line_ids': [ref('tax_report_line_standard_rated_expense_vat')], + }), + ]"/> + <field name="refund_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'minus_report_line_ids': [ref('tax_report_line_standard_rated_expense_base')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('uae_account_3726'), + 'minus_report_line_ids': [ref('tax_report_line_standard_rated_expense_vat')], + }), + ]"/> + </record> + <record id="uae_purchase_tax_exempted" model="account.tax.template"> + <field name="name">Exempted Tax</field> + <field name="type_tax_use">purchase</field> + <field name="amount">0</field> + <field name="amount_type">percent</field> + <field name="description">Exempted Tax</field> + <field name="tax_group_id" ref="ae_tax_group_exempted"/> + <field name="chart_template_id" ref="uae_chart_template_standard"/> + <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('uae_account_3726'), + 'minus_report_line_ids': [ref('tax_report_line_exempt_supplies_vat')], + }), + ]"/> + <field name="refund_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('uae_account_3726'), + 'plus_report_line_ids': [ref('tax_report_line_exempt_supplies_vat')], + }), + ]"/> + </record> + <record id="uae_purchase_tax_0" model="account.tax.template"> + <field name="name">TVA 0%</field> <field name="type_tax_use">purchase</field> - <field name="tax_group_id" ref="tax_group_15"/> + <field name="amount">0</field> + <field name="amount_type">percent</field> + <field name="description">TVA 0%</field> + <field name="tax_group_id" ref="ae_tax_group_0"/> + <field name="chart_template_id" ref="uae_chart_template_standard"/> <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), (0,0, { 'factor_percent': 100, 'repartition_type': 'base', }), - (0,0, { 'factor_percent': 100, 'repartition_type': 'tax', 'account_id': ref('uae_account_3726'), + 'minus_report_line_ids': [ref('tax_report_line_zero_rated_supplies_vat')], }), ]"/> <field name="refund_repartition_line_ids" eval="[(5, 0, 0), @@ -54,11 +789,89 @@ 'factor_percent': 100, 'repartition_type': 'base', }), - (0,0, { 'factor_percent': 100, 'repartition_type': 'tax', 'account_id': ref('uae_account_3726'), + 'plus_report_line_ids': [ref('tax_report_line_zero_rated_supplies_vat')], + }), + ]"/> + </record> + <record id="uae_import_tax" model="account.tax.template"> + <field name="name">Import Tax 5%</field> + <field name="type_tax_use">purchase</field> + <field name="amount">5</field> + <field name="amount_type">percent</field> + <field name="description">Import Tax</field> + <field name="chart_template_id" ref="uae_chart_template_standard"/> + <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'plus_report_line_ids': [ref('tax_report_line_import_uae_base')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'plus_report_line_ids': [ref('tax_report_line_import_uae_vat')], + }), + ]"/> + <field name="refund_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'minus_report_line_ids': [ref('tax_report_line_import_uae_base')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'minus_report_line_ids': [ref('tax_report_line_import_uae_vat')], + }), + ]"/> + </record> + <record id="uae_purchase_tax_reverse_charge" model="account.tax.template"> + <field name="name">Reverse Charge Provision</field> + <field name="type_tax_use">purchase</field> + <field name="amount">5</field> + <field name="amount_type">percent</field> + <field name="description">Supplies subject to reverse charge provisions</field> + <field name="chart_template_id" ref="uae_chart_template_standard"/> + <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'minus_report_line_ids': [ref('tax_report_line_expense_supplies_reverse_base'),ref('tax_report_line_standard_rated_expense_base')], + }), + (0,0, { + 'factor_percent': -100, + 'repartition_type': 'tax', + 'account_id': ref('uae_account_3726'), + 'minus_report_line_ids': [ref('tax_report_line_expense_supplies_reverse_vat')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('uae_account_3726'), + 'minus_report_line_ids': [ref('tax_report_line_standard_rated_expense_vat')], + }), + ]"/> + <field name="refund_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'plus_report_line_ids': [ref('tax_report_line_expense_supplies_reverse_base'),ref('tax_report_line_standard_rated_expense_base')], + }), + (0,0, { + 'factor_percent': -100, + 'repartition_type': 'tax', + 'account_id': ref('uae_account_3726'), + 'plus_report_line_ids': [ref('tax_report_line_expense_supplies_reverse_vat')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('uae_account_3726'), + 'plus_report_line_ids': [ref('tax_report_line_standard_rated_expense_vat')], }), ]"/> </record> diff --git a/addons/l10n_ae/data/fiscal_templates_data.xml b/addons/l10n_ae/data/fiscal_templates_data.xml new file mode 100644 index 00000000000..14a68c1863e --- /dev/null +++ b/addons/l10n_ae/data/fiscal_templates_data.xml @@ -0,0 +1,112 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + <record id="account_fiscal_position_dubai" model="account.fiscal.position.template"> + <field name="name">Dubai</field> + <field name="auto_apply" eval="True"/> + <field name="sequence">16</field> + <field name="country_id" ref="base.ae"/> + <field name="state_ids" eval="[(6,0,[ref('base.state_ae_du')])]"/> + <field name="chart_template_id" ref="uae_chart_template_standard"/> + </record> + + <record id="account_fiscal_position_abu_dhabi" model="account.fiscal.position.template"> + <field name="name">Abu Dhabi</field> + <field name="auto_apply" eval="True"/> + <field name="sequence">16</field> + <field name="country_id" ref="base.ae"/> + <field name="state_ids" eval="[(6,0,[ref('base.state_ae_az')])]"/> + <field name="chart_template_id" ref="uae_chart_template_standard"/> + </record> + <record id="account_fiscal_position_abu_dhabi_01" model="account.fiscal.position.tax.template"> + <field name="tax_src_id" ref="uae_sale_tax_5_dubai"/> + <field name="tax_dest_id" ref="uae_sale_tax_5_abu_dhabi"/> + <field name="position_id" ref="account_fiscal_position_abu_dhabi"/> + </record> + + <record id="account_fiscal_position_sharjah" model="account.fiscal.position.template"> + <field name="name">Sharjah</field> + <field name="auto_apply" eval="True"/> + <field name="sequence">16</field> + <field name="country_id" ref="base.ae"/> + <field name="state_ids" eval="[(6,0,[ref('base.state_ae_sh')])]"/> + <field name="chart_template_id" ref="uae_chart_template_standard"/> + </record> + <record id="account_fiscal_position_sharjah_01" model="account.fiscal.position.tax.template"> + <field name="tax_src_id" ref="uae_sale_tax_5_dubai"/> + <field name="tax_dest_id" ref="uae_sale_tax_5_sharjah"/> + <field name="position_id" ref="account_fiscal_position_sharjah"/> + </record> + + <record id="account_fiscal_position_ajman" model="account.fiscal.position.template"> + <field name="name">Ajman</field> + <field name="auto_apply" eval="True"/> + <field name="sequence">16</field> + <field name="country_id" ref="base.ae"/> + <field name="state_ids" eval="[(6,0,[ref('base.state_ae_aj')])]"/> + <field name="chart_template_id" ref="uae_chart_template_standard"/> + </record> + <record id="account_fiscal_position_ajman_01" model="account.fiscal.position.tax.template"> + <field name="tax_src_id" ref="uae_sale_tax_5_dubai"/> + <field name="tax_dest_id" ref="uae_sale_tax_5_ajman"/> + <field name="position_id" ref="account_fiscal_position_ajman"/> + </record> + + <record id="account_fiscal_position_umm_al_quwain" model="account.fiscal.position.template"> + <field name="name">Umm Al Quwain</field> + <field name="auto_apply" eval="True"/> + <field name="sequence">16</field> + <field name="country_id" ref="base.ae"/> + <field name="state_ids" eval="[(6,0,[ref('base.state_ae_uq')])]"/> + <field name="chart_template_id" ref="uae_chart_template_standard"/> + </record> + <record id="account_fiscal_position_umm_al_quwain_01" model="account.fiscal.position.tax.template"> + <field name="tax_src_id" ref="uae_sale_tax_5_dubai"/> + <field name="tax_dest_id" ref="uae_sale_tax_5_umm_al_quwain"/> + <field name="position_id" ref="account_fiscal_position_umm_al_quwain"/> + </record> + + <record id="account_fiscal_position_ras_al_khaima" model="account.fiscal.position.template"> + <field name="name">Ras Al-Khaima</field> + <field name="auto_apply" eval="True"/> + <field name="sequence">16</field> + <field name="country_id" ref="base.ae"/> + <field name="state_ids" eval="[(6,0,[ref('base.state_ae_rk')])]"/> + <field name="chart_template_id" ref="uae_chart_template_standard"/> + </record> + <record id="account_fiscal_position_ras_al_khaima_01" model="account.fiscal.position.tax.template"> + <field name="tax_src_id" ref="uae_sale_tax_5_dubai"/> + <field name="tax_dest_id" ref="uae_sale_tax_5_ras_al_khaima"/> + <field name="position_id" ref="account_fiscal_position_ras_al_khaima"/> + </record> + + <record id="account_fiscal_position_fujairah" model="account.fiscal.position.template"> + <field name="name">Fujairah</field> + <field name="auto_apply" eval="True"/> + <field name="sequence">16</field> + <field name="country_id" ref="base.ae"/> + <field name="state_ids" eval="[(6,0,[ref('base.state_ae_fu')])]"/> + <field name="chart_template_id" ref="uae_chart_template_standard"/> + </record> + <record id="account_fiscal_position_fujairah_01" model="account.fiscal.position.tax.template"> + <field name="tax_src_id" ref="uae_sale_tax_5_dubai"/> + <field name="tax_dest_id" ref="uae_sale_tax_5_fujairah"/> + <field name="position_id" ref="account_fiscal_position_fujairah"/> + </record> + + <record id="account_fiscal_position_non_uae_countries" model="account.fiscal.position.template"> + <field name="name">Non-UAE</field> + <field name="sequence">20</field> + <field name="auto_apply" eval="True"/> + <field name="chart_template_id" ref="uae_chart_template_standard"/> + </record> + <record id="acccount_fiscal_position_tax_non_uae_01" model="account.fiscal.position.tax.template"> + <field name="tax_src_id" ref="uae_sale_tax_5_dubai"/> + <field name="tax_dest_id" ref="uae_sale_tax_0"/> + <field name="position_id" ref="account_fiscal_position_non_uae_countries"/> + </record> + <record id="acccount_fiscal_position_tax_non_uae_02" model="account.fiscal.position.tax.template"> + <field name="tax_src_id" ref="uae_purchase_tax_5"/> + <field name="tax_dest_id" ref="uae_purchase_tax_0"/> + <field name="position_id" ref="account_fiscal_position_non_uae_countries"/> + </record> +</odoo> diff --git a/addons/l10n_ae/data/l10n_ae_chart_data.xml b/addons/l10n_ae/data/l10n_ae_chart_data.xml index 0c8202a34f2..eb648164386 100644 --- a/addons/l10n_ae/data/l10n_ae_chart_data.xml +++ b/addons/l10n_ae/data/l10n_ae_chart_data.xml @@ -1,5 +1,7 @@ <?xml version="1.0" encoding="utf-8"?> <odoo> + <menuitem id="account_reports_ae_statements_menu" name="UAE" parent="account.menu_finance_reports" sequence="0" groups="account.group_account_user"/> + <record id="uae_chart_template_standard" model="account.chart.template"> <field name="name">U.A.E Chart of Accounts - Standard</field> <field name="code_digits">6</field> @@ -7,6 +9,6 @@ <field name="cash_account_code_prefix">105</field> <field name="transfer_account_code_prefix">100</field> <field name="currency_id" ref="base.AED" /> - <field name="complete_tax_set" eval="False"/> + <field name="complete_tax_set" eval="True"/> </record> </odoo> diff --git a/addons/l10n_ae/data/l10n_ae_data.xml b/addons/l10n_ae/data/l10n_ae_data.xml new file mode 100644 index 00000000000..94692698ecd --- /dev/null +++ b/addons/l10n_ae/data/l10n_ae_data.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + <!-- set VAT label to show on invoice report --> + <record id="base.ae" model="res.country"> + <field name="vat_label">TRN</field> + </record> + <record id="base.AED" model="res.currency"> + <field name="symbol">AED</field> + </record> + <record id="gcc_countries_group" model="res.country.group"> + <field name="name">GCC VAT implementing States</field> + <field name="country_ids" eval="[(6,0,[ref('base.ae'),ref('base.sa'),ref('base.bh')])]"/> + </record> +</odoo> diff --git a/addons/l10n_ae/views/report_invoice_templates.xml b/addons/l10n_ae/views/report_invoice_templates.xml new file mode 100644 index 00000000000..681fe038284 --- /dev/null +++ b/addons/l10n_ae/views/report_invoice_templates.xml @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + <template id="report_invoice_document" inherit_id="account.report_invoice_document"> + <xpath expr="//p[@name='payment_term']" position="after"> + <p t-if="o.company_id.country_id.code == 'AE' and o.partner_id.country_id.code != 'AE' and o.env.ref('l10n_ae.gcc_countries_group') in o.partner_id.country_id.country_group_ids"> + Supply between <b>United Arad Emirates</b> and <b><span t-field="o.partner_id.country_id.name"/></b> + </p> + </xpath> + <xpath expr="//h2/span" position="before"> + <span t-if="o.company_id.country_id.code == 'AE' and o.type in ['out_invoice', 'out_refund']">Tax</span> + </xpath> + + <xpath expr="//div[hasclass('clearfix')]" position="after"> + <div t-if="o.company_id.country_id.code == 'AE' and o.currency_id != o.company_id.currency_id" id="aed_amounts" class="row clearfix ml-auto my-3 text-nowrap table"> + <t t-set="aed_rate" t-value="o.env['res.currency']._get_conversion_rate(o.currency_id, o.company_id.currency_id, o.company_id, o.date_invoice or datetime.date.today())"/> + <div name="exchange_rate" class="col-auto"> + <strong>Exchange Rate</strong> + <p class="m-0" t-esc="aed_rate" t-options='{"widget": "float", "precision": 5}'/> + </div> + <div name="aed_subtotal" class="col-auto"> + <strong>Subtotal (AED)</strong> + <p class="m-0" t-esc="o.amount_untaxed_signed" t-options='{"widget": "monetary", "display_currency": o.company_currency_id}'/> + </div> + <div name="aed_vat_amount" class="col-auto"> + <strong>Tax Amount (AED)</strong> + <p class="m-0" t-esc="o.currency_id._convert(o.amount_tax, o.company_id.currency_id, o.company_id, o.date_invoice or datetime.date.today())" t-options='{"widget": "monetary", "display_currency": o.company_currency_id}'/> + </div> + <div name="aed_total" class="col-auto"> + <strong>Total (AED)</strong> + <p class="m-0" t-esc="o.amount_total_company_signed" t-options='{"widget": "monetary", "display_currency": o.company_currency_id}'/> + </div> + </div> + </xpath> + </template> +</odoo> diff --git a/addons/l10n_be/data/account_tax_template_data.xml b/addons/l10n_be/data/account_tax_template_data.xml index 7d3101e66ac..98e17c58ae2 100644 --- a/addons/l10n_be/data/account_tax_template_data.xml +++ b/addons/l10n_be/data/account_tax_template_data.xml @@ -3325,7 +3325,7 @@ (0,0, { 'factor_percent': 100, 'repartition_type': 'tax', - 'plus_report_line_ids': [ref('tax_report_line_62')], + 'minus_report_line_ids': [ref('tax_report_line_62')], }), ]"/> </record> diff --git a/addons/l10n_be_edi/security/security.xml b/addons/l10n_be_edi/security/security.xml new file mode 100644 index 00000000000..3fc21715a4c --- /dev/null +++ b/addons/l10n_be_edi/security/security.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + <!-- Note for later versions, the group Employee Manager should become contract manager --> + <record id="hr.group_hr_manager" model="res.groups"> + <field name="implied_ids" eval="[(4, ref('fleet.fleet_group_manager'))]"/> + </record> +</odoo> diff --git a/addons/l10n_ca/data/account_tax_data.xml b/addons/l10n_ca/data/account_tax_data.xml index d2eae78804d..347afdf8525 100644 --- a/addons/l10n_ca/data/account_tax_data.xml +++ b/addons/l10n_ca/data/account_tax_data.xml @@ -84,7 +84,6 @@ <field name="amount">100</field> <field name="amount_type">group</field> <field name="children_tax_ids" eval="[(6,0,[ref('pst_bc_sale_en'), ref('gstpst_sale_bc_gst_en')])]"/> - <field name="tag_ids" eval="[(6,0,[ref('tag_gstpst_bc_sale')])]"/> <field name="tax_group_id" ref="tax_group_fix"/> </record> diff --git a/addons/l10n_ch/views/res_bank_view.xml b/addons/l10n_ch/views/res_bank_view.xml index df3919186ab..90329ee4813 100644 --- a/addons/l10n_ch/views/res_bank_view.xml +++ b/addons/l10n_ch/views/res_bank_view.xml @@ -29,7 +29,7 @@ <field name="model">account.journal</field> <field name="inherit_id" ref="account.view_account_journal_form"/> <field name="arch" type="xml"> - <xpath expr="//field[@name='bank_acc_number']" position="after"> + <xpath expr="//field[@name='bank_account_id']" position="after"> <field name="l10n_ch_postal" attrs="{'invisible': [('type', 'not in', ['bank', 'cash'])]}"/> </xpath> </field> diff --git a/addons/l10n_fr_fec/wizard/account_fr_fec.py b/addons/l10n_fr_fec/wizard/account_fr_fec.py index 6d8b0bc62f0..34ca956a2c4 100644 --- a/addons/l10n_fr_fec/wizard/account_fr_fec.py +++ b/addons/l10n_fr_fec/wizard/account_fr_fec.py @@ -305,7 +305,7 @@ class AccountFrFec(models.TransientModel): END AS PieceRef, TO_CHAR(am.date, 'YYYYMMDD') AS PieceDate, - CASE WHEN aml.name IS NULL THEN '/' ELSE replace(replace(aml.name, '|', '/'), '\t', '') END AS EcritureLib, + CASE WHEN aml.name IS NULL OR aml.name = '' THEN '/' ELSE replace(replace(aml.name, '|', '/'), '\t', '') END AS EcritureLib, replace(CASE WHEN aml.debit = 0 THEN '0,00' ELSE to_char(aml.debit, '000000000000000D99') END, '.', ',') AS Debit, replace(CASE WHEN aml.credit = 0 THEN '0,00' ELSE to_char(aml.credit, '000000000000000D99') END, '.', ',') AS Credit, CASE WHEN rec.name IS NULL THEN '' ELSE rec.name END AS EcritureLet, @@ -388,7 +388,7 @@ class AccountFrFec(models.TransientModel): rows_length = len(rows) for i, row in enumerate(rows): if not i == rows_length - 1: - row.append(lineterminator) + row[-1] += lineterminator writer.writerow(row) fecvalue = fecfile.getvalue() diff --git a/addons/l10n_hn/data/account_chart_template_data.xml b/addons/l10n_hn/data/account_chart_template_data.xml index 4a7131526ce..bfeafbd48dd 100644 --- a/addons/l10n_hn/data/account_chart_template_data.xml +++ b/addons/l10n_hn/data/account_chart_template_data.xml @@ -10,8 +10,6 @@ <field name="description">ISV por Cobrar</field> <field name="amount" eval="15"/> <field name="amount_type">percent</field> - <field name="account_id" ref="cta110301"/> - <field name="refund_account_id" ref="cta110301"/> <field name="type_tax_use">purchase</field> <field name="price_include" eval="True"/> <field name="tax_group_id" ref="tax_group_iva_15"/> diff --git a/addons/l10n_in/models/account_invoice.py b/addons/l10n_in/models/account_invoice.py index 6ef37d2c584..5b59933d0e9 100644 --- a/addons/l10n_in/models/account_invoice.py +++ b/addons/l10n_in/models/account_invoice.py @@ -70,8 +70,8 @@ class AccountInvoice(models.Model): vals['quantity'] = invoice_tax_line.l10n_in_quantity return res - def _prepare_tax_line_vals(self, line, tax, tax_ids): - vals = super(AccountInvoice, self)._prepare_tax_line_vals(line, tax, tax_ids) + def _prepare_tax_line_vals(self, line, tax): + vals = super(AccountInvoice, self)._prepare_tax_line_vals(line, tax) vals['l10n_in_product_id'] = line.product_id.id vals['l10n_in_uom_id'] = line.uom_id.id vals['l10n_in_quantity'] = line.quantity diff --git a/addons/l10n_it/data/account.tax.group.csv b/addons/l10n_it/data/account.tax.group.csv index ec8fffb16bc..c8035a8f072 100644 --- a/addons/l10n_it/data/account.tax.group.csv +++ b/addons/l10n_it/data/account.tax.group.csv @@ -1,6 +1,7 @@ id,name tax_group_iva_2,IVA 2% tax_group_iva_4,IVA 4% +tax_group_iva_5,IVA 5% tax_group_iva_10,IVA 10% tax_group_iva_12,IVA 12% tax_group_iva_21,IVA 21% diff --git a/addons/l10n_it/data/account_tax_template.xml b/addons/l10n_it/data/account_tax_template.xml index 8c64ec29cb3..0adb0478db4 100644 --- a/addons/l10n_it/data/account_tax_template.xml +++ b/addons/l10n_it/data/account_tax_template.xml @@ -205,6 +205,74 @@ ]"/> </record> + <record id="5v" model="account.tax.template"> + <field name="description">5v</field> + <field name="chart_template_id" ref="l10n_it_chart_template_generic"/> + <field name="name">Iva al 5% (debito)</field> + <field name="sequence">5</field> + <field name="amount">5</field> + <field name="amount_type">percent</field> + <field name="type_tax_use">sale</field> + <field name="price_include">False</field> + <field name="tax_group_id" ref="tax_group_iva_5"/> + <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('2601'), + }), + ]"/> + <field name="refund_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('2601'), + }), + ]"/> + </record> + + <record id="5a" model="account.tax.template"> + <field name="description">5a</field> + <field name="chart_template_id" ref="l10n_it_chart_template_generic"/> + <field name="name">Iva al 5% (credito)</field> + <field name="sequence">6</field> + <field name="amount">5</field> + <field name="amount_type">percent</field> + <field name="type_tax_use">purchase</field> + <field name="price_include">False</field> + <field name="tax_group_id" ref="tax_group_iva_5"/> + <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('1601'), + }), + ]"/> + <field name="refund_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('1601'), + }), + ]"/> + </record> + <record id="10v" model="account.tax.template"> <field name="description">10v</field> <field name="chart_template_id" ref="l10n_it_chart_template_generic"/> diff --git a/addons/l10n_mn/__init__.py b/addons/l10n_mn/__init__.py new file mode 100644 index 00000000000..67dee8c60db --- /dev/null +++ b/addons/l10n_mn/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. diff --git a/addons/l10n_mn/__manifest__.py b/addons/l10n_mn/__manifest__.py new file mode 100644 index 00000000000..ba60ad4da0f --- /dev/null +++ b/addons/l10n_mn/__manifest__.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +{ + "name" : "Mongolia - Accounting", + "version" : "1.0", + 'category': 'Localization', + "description": """ +This is the module to manage the accounting chart for Mongolia. +=============================================================== + + * the Mongolia Official Chart of Accounts, + * the Tax Code Chart for Mongolia + * the main taxes used in Mongolia + +Financial requirement contributor: Baskhuu Lodoikhuu. BumanIT LLC +""", + "depends": ['account'], + 'data': [ + 'data/account_chart_template_data.xml', + 'data/account.account.tag.csv', + 'data/account.account.template.csv', + 'data/account.tax.group.csv', + 'data/account_tax_template_data.xml', + 'data/account_fiscal_position_template_data.xml', + 'data/account.chart.template.csv', + 'data/account_chart_template_configuration_data.xml', + ], +} diff --git a/addons/l10n_mn/data/account.account.tag.csv b/addons/l10n_mn/data/account.account.tag.csv new file mode 100644 index 00000000000..b8667ffc12d --- /dev/null +++ b/addons/l10n_mn/data/account.account.tag.csv @@ -0,0 +1,94 @@ +id,name,applicability +account.account_tag_operating,CF: Үндсэн: Бусад,accounts +account.account_tag_financing,CF: Хөрөнгө оруулалт: Бусад Урт хугацаат хөрөнгө,accounts +account.account_tag_investing,CF: Санхүү: Ногдол ашиг,accounts +account_tag_operating_product,CF: Үндсэн: Бараа үйлчилгээ,accounts +account_tag_operating_loyalty,CF: Үндсэн: Эрхийн шимтгэл хураамж,accounts +account_tag_operating_insurance,CF: Үндсэн: Нийгмийн Даатгал,accounts +account_tag_operating_insurance2,CF: Үндсэн: Бусад Даатгал,accounts +account_tag_operating_taxrefund,CF: Үндсэн: Татвар,accounts +account_tag_operating_funding,CF: Үндсэн: Татаас санхүүжилт,accounts +account_tag_operating_employee,CF: Үндсэн: Ажилчид,accounts +account_tag_operating_cost,CF: Үндсэн: Ашиглалтын зардал,accounts +account_tag_operating_transport,CF: Үндсэн: Түлш тээвэр сэлбэг,accounts +account_tag_operating_interest,CF: Үндсэн: Зээлийн хүү,accounts +account_tag_investing_fasset,CF: Хөрөнгө оруулалт: Үндсэн хөрөнгө,accounts +account_tag_investing_iasset,CF: Хөрөнгө оруулалт: Биет бус хөрөнгө,accounts +account_tag_investing_invest,CF: Хөрөнгө оруулалт: Хөрөнгө оруулалт,accounts +account_tag_investing_loan,CF: Хөрөнгө оруулалт: Зээл урьдчилгаа,accounts +account_tag_investing_interest,CF: Хөрөнгө оруулалт: Зээлийн хүү,accounts +account_tag_investing_dividends,CF: Хөрөнгө оруулалт: Ногдол ашиг,accounts +account_tag_financing_loan,"CF: Санхүү: Зээл, өрийн үнэт цаас",accounts +account_tag_financing_rental,CF: Санхүү: Санхүүгийн түрээс,accounts +account_tag_financing_stock,CF: Санхүү: Хувьцаа өмчийн бусад үнэт цаас,accounts +account_tag_financing_donation,CF: Санхүү: Хандив,accounts +account_tag_exchange,CF: Валютын ханшийн зөрүү,accounts +vat_report_tag2,ТТ3а: НӨАТ-аас чөлөөлөгдөх борлуулалтын орлого (2),taxes +vat_report_tag5,ТТ3a: Дотоодын зах зээлд борлуулсан барааны борлуулалтын орлого (5),taxes +vat_report_tag6,ТТ3a: Бусад барааны борлуулалтын орлого (6),taxes +vat_report_tag7,ТТ3a: Эрх борлуулсны орлого (7),taxes +vat_report_tag8,ТТ3a: Татан буугдах үед үлдсэн барааны орлого (8),taxes +vat_report_tag9,ТТ3a: Өрийн төлбөрт шилжүүлсэн барааны орлого (9),taxes +vat_report_tag11,ТТ3a: Наториатын үйлчилгээний орлого (11),taxes +vat_report_tag12,ТТ3a: Өрийн төлбөрт тооцсон ажил үйлчилгээний орлого (12),taxes +vat_report_tag13,"ТТ3a: Цахилгаан, дулаан, ус, шуудан, холбооны үйлчилгээ үзүүлсний орлого (13)",taxes +vat_report_tag14,"ТТ3a: Бараа түрээслүүлэх, эзэмшүүлэх, ашиглуулах үйлчилгээний орлого (14)",taxes +vat_report_tag15,"ТТ3a: Байр түрээслүүлэх, эзэмшүүлэх, ашиглуулах үйлчилгээний орлого (15)",taxes +vat_report_tag16,"ТТ3a: Үл хөдлөх, хөдлөх хөрөнгө түрээслүүлэх, эзэмшүүлэх ашиглуулах үйлчилгээий орлого (16)",taxes +vat_report_tag17,"ТТ3a: Шинэ бүтээл, загвар, зохиогчийн эрх, барааны тэмдэг худалдсаны орлого (17)",taxes +vat_report_tag18,"ТТ3a: Хонжворт сугалаа, төлбөрт таавар, бооцоот тоглоомын орлого (18)",taxes +vat_report_tag19,ТТ3a: Зуучлалын үйлчилгээний орлого (19),taxes +vat_report_tag20,"ТТ3a: Бусдаас авсан хүү, торгууль, алдангийн орлого (20)",taxes +vat_report_tag21,ТТ3a: Хөрөнгийн үнэлгээний үйлчилгээний орлого (21),taxes +vat_report_tag22,"ТТ3a: Төсвийн санхүүжилт, татаас, урамшууллын орлого (22)",taxes +vat_report_tag23,"ТТ3a: Өмгөөлөл, хууль зүйн үйлчилгээний орлого (23)",taxes +vat_report_tag24,"ТТ3a: Үсчин, гоо сайхан, засвар үйлчилгээ, угаалга, хими цэвэрлэгээний үйлчилгээний орлого (24)",taxes +vat_report_tag25,ТТ3a: Хуулийн 13-т зааснаас бусад үйлчилгээний орлого (25),taxes +vat_report_tag28,ТТ3a: Экспортонд гаргасан барааны борлуулалтын орлого (28),taxes +vat_report_tag29,ТТ3a: Экспортолсон үйлчилгээний борлуулалтын орлого (29),taxes +vat_report_tag33,"ТТ3а: Худалдан авсан нийт бараа, ажил үйлчилгээ (33)",taxes +vat_report_tag35,"ТТ3a: НӨАТ-тэй авсан Импортын бараа, ажил үйлчилгээний худалдан авалт (35)",taxes +vat_report_tag36,"ТТ3a: Дотоодын зах зээлээс НӨАТ-тэй авсан бараа, ажил, үйлчилгээ (36)",taxes +vat_report_tag37,"ТТ3a: Суутган төлөгчөөр бүртгүүлэх үед НӨАТ-тэй авсан бараа, үйлчилгээ (37)",taxes +vat_report_tag38,"ТТ3a: Мал аж ахуй, тариалан эрхлэгчээс НӨАТ-тэй авсан бараа, үйлчилгээ (38)",taxes +vat_report_tag41,"ТТ3а: Суудлын автомашин, түүний эд анги сэлбэгт төлсөн НӨАТ (41)",taxes +vat_report_tag42,"ТТ3а: Ажлын хэрэгцээнд зориулж авсан бараа, үйлчилгээнд төлсөн НӨАТ (42)",taxes +vat_report_tag43,"ТТ3а: Үндсэн хөрөнгө бэлтгэхэд зориулж импортоор авсан бараа, үйлчилгээнд төлсөн НӨАТ (43)",taxes +vat_report_tag44,"ТТ3а: Чөлөөлөгдөх үйлдвэрлэл, үйлчилгээнд зориулж авсан бараа, ажил, үйлчилгээнд төлсөн НӨАТ (44)",taxes +vat_report_tag45,"ТТ3а: Хайгуулын ажил болон ашиглалтын өмнөх үйл ажиллагаанд зориулж импортоор авсан бараа, ажил, үйлчилгээнд төлсөн НӨАТ (45)",taxes +vat_report_tag46,"ТТ3а: Импортоор оруулсан бусад хамааралгүй бараа, ажил, үйлчилгээнд төлсөн НӨАТ (46)",taxes +vat_report_tag48,ТТ3а: Cанхүүгийн түрээсээр дотоодын зах зээлд борлуулсны орлого (48),taxes +vat_report_tag49,"ТТ3а: Факторинг, форфайтинг хэлцлийн үйлчилгээний орлого (49)",taxes +vat_report_tag51,ТТ3а: Санхүүгийн түрээсийн зүйлийг худалдан авахад төлсөн түрээсийн төлбөр (51),taxes +vat_report_tag52,"ТТ3а: Факторинг, форфайтинг хэлцлийг авахад төлсөн төлбөр (52)",taxes +vat_report_tag58,"ТТ3а: Борлуулалтын орлогын буцаалт, хөнгөлөлт (58)",accounts +vat_report_tag59,"ТТ3а: Худалдан авалтын буцаалт, хөнгөлөлт (59)",accounts +tax_report_tag2,ТТ02: Татвараас чөлөөлөгдөх орлого (2),accounts +tax_report_tag5,ТТ02: Бусад орлогын дүн (5),accounts +tax_report_tag7,ТТ02: Үндсэн ажил үйлчилгээний орлого (7),accounts +tax_report_tag8,ТТ02: Туслах ажил үйлчилгээний орлого (8),accounts +tax_report_tag9,"ТТ02: Хувьцаа, үнэт цаас борлуулсны орлого (9)",accounts +tax_report_tag10,"ТТ02: Үнэ төлбөргүйгээр бусдаас авсан бараа, үйлчилгээ (10)",accounts +tax_report_tag11,ТТ02: Биет бус хөрөнгө борлуулсны орлого (11),accounts +tax_report_tag12,ТТ02: Техникийн зөвлөх үйлчилгээний орлого (12),accounts +tax_report_tag13,"ТТ02: Гэрээний хүү, торгууль, хохирлын нөхөн төлбөрийн орлого (13)",accounts +tax_report_tag14,ТТ02: Гадаад валютын ханшийн зөрүүгийн бодит орлого (14),accounts +tax_report_tag15,ТТ02: Хөдлөх болон үл хөдлөх эд хөрөнгийн түрээсийн орлого (15),accounts +tax_report_tag16,ТТ02: Хөдлөх эд хөрөнгө борлуулсны орлого (16),accounts +tax_report_tag17,ТТ02: Албан татвар ногдох бусад орлого (17),accounts +tax_report_tag22,ТТ02: Татвар ногдох орлогоос хасагдахгүй зардал (22),accounts +tax_report_tag23,ТТ02: Татвар ногдох орлогыг бууруулах (23),accounts +tax_report_tag25,ТТ02: Сайн дурын даатгалын хураамжийн хэтрэлт (25),accounts +tax_report_tag27,ТТ02: Өмнөх жилийн татварын алдагдлаас тайлант хугацаанд шилжүүлсэн дүн (27),accounts +tax_report_tag30,ТТ02: Татварын хөнгөлөлт (30),accounts +tax_report_tag32,"ТТ02: Эротик хэвлэл, бичлэг, тоглолт борлуулалтын орлого (32)",accounts +tax_report_tag34,"ТТ02: Таавар, бооцоот тоглоом, хонжворт сугалааны орлого (34)",accounts +tax_report_tag35,"ТТ02: Таавар, бооцоот тоглоом, хонжворт сугалааны орлого олоход зарцуулсан зардал (35)",accounts +tax_report_tag36,ТТ02: Хонжворт олгосон мөнгө болон барааны үнэ (36),accounts +tax_report_tag39,ТТ02: Хүүгийн орлого (39),accounts +tax_report_tag41,"ТТ02: Гадаад улсад олсон ногдол ашиг, хүүгийн орлого (41)",accounts +tax_report_tag43,ТТ02: Гадаад улсад олсон орлого (43),accounts +tax_report_tag46,ТТ02: Ногдол ашгийн орлого (46),accounts +tax_report_tag48,ТТ02: Эрхийн шимтгэлийн орлого (48),accounts +tax_report_tag50,ТТ02: Эрх борлуулсны орлого (50),accounts +tax_report_tag52,ТТ02: Үл хөдлөх хөрөнгө борлуулсны орлого (52),accounts diff --git a/addons/l10n_mn/data/account.account.template.csv b/addons/l10n_mn/data/account.account.template.csv new file mode 100644 index 00000000000..f64558c3e6b --- /dev/null +++ b/addons/l10n_mn/data/account.account.template.csv @@ -0,0 +1,285 @@ +id,code,name,user_type_id/id,tag_ids/id,reconcile,chart_template_id:id +account_template_1001_0101,10010101,Бэлэн мөнгө,account.data_account_type_liquidity,,0,mn_chart_1 +account_template_1003_0101,10030101,Жижиг мөнгөн сан,account.data_account_type_liquidity,,0,mn_chart_1 +account_template_1101_0101,11010101,Банкинд байгаа мөнгө,account.data_account_type_liquidity,,0,mn_chart_1 +account_template_1103_0101,11030101,Хугацаагүй хадгаламж,account.data_account_type_liquidity,,0,mn_chart_1 +account_template_1104_0101,11040101,Банкны төлбөрийн темринал,account.data_account_type_liquidity,,0,mn_chart_1 +account_template_1109_0101,11090101,Замд яваа мөнгө,account.data_account_type_current_assets,,1,mn_chart_1 +account_template_1110_0101,11100101,Бусад мөнгө түүнтэй адилтгах хөрөнгө,account.data_account_type_current_assets,,0,mn_chart_1 +account_template_1201_0101,12010101,Худалдааны авлага ААН,account.data_account_type_receivable,account_tag_operating_product,1,mn_chart_1 +account_template_1201_0201,12010201,Худалдааны авлага Хувь хүн,account.data_account_type_receivable,account_tag_operating_product,1,mn_chart_1 +account_template_1201_0301,12010301,Худалдааны авлага Холбоотой тал,account.data_account_type_receivable,account_tag_operating_product,1,mn_chart_1 +account_template_1202_0101,12020101,Авлагын бичиг,account.data_account_type_current_assets,account_tag_operating_product,0,mn_chart_1 +account_template_1203_0101,12030101,Найдваргүй авлагын хасагдуулга,account.data_account_type_current_assets,account_tag_operating_product,0,mn_chart_1 +account_template_1204_0101,12040101,Урьдчилж төлсөн ААНОАТатвар,account.data_account_type_current_assets,account_tag_operating_taxrefund,0,mn_chart_1 +account_template_1204_0201,12040201,Урьдчилж төлсөн ХХОАТатвар,account.data_account_type_current_assets,account_tag_operating_taxrefund,0,mn_chart_1 +account_template_1204_0301,12040301,Урьдчилж төлсөн НӨАТатвар,account.data_account_type_current_assets,account_tag_operating_taxrefund,0,mn_chart_1 +account_template_1204_0401,12040401,Урьдчилж төлсөн Гаалийн албан татвар,account.data_account_type_current_assets,account_tag_operating_taxrefund,0,mn_chart_1 +account_template_1204_0501,12040501,Урьдчилж төлсөн Үл хөдлөх хөрөнгийн албан татвар,account.data_account_type_current_assets,account_tag_operating_taxrefund,0,mn_chart_1 +account_template_1204_0601,12040601,Урьдчилж төлсөн Газрын төлбөр,account.data_account_type_current_assets,account_tag_operating_taxrefund,0,mn_chart_1 +account_template_1204_0701,12040701,Урьдчилж төлсөн АТӨЯХАТатвар,account.data_account_type_current_assets,account_tag_operating_taxrefund,0,mn_chart_1 +account_template_1204_0801,12040801,Урьдчилж төлсөн Агаарын бохирдлын татвар,account.data_account_type_current_assets,account_tag_operating_taxrefund,0,mn_chart_1 +account_template_1204_0901,12040901,Урьдчилж төлсөн Онцгой албан татвар,account.data_account_type_current_assets,account_tag_operating_taxrefund,0,mn_chart_1 +account_template_1204_1001,12041001,Урьдчилж төлсөн Байгалийн нөөц ашиглалтын төлбөр,account.data_account_type_current_assets,account_tag_operating_taxrefund,0,mn_chart_1 +account_template_1204_9901,12049901,Урьдчилж төлсөн Бусад татвар,account.data_account_type_current_assets,account_tag_operating_taxrefund,0,mn_chart_1 +account_template_1205_0101,12050101,"Урьдчилж төлсөн ЭМ, НДШ",account.data_account_type_current_assets,account_tag_operating_taxrefund,0,mn_chart_1 +account_template_1205_0201,12050201,Урьдчилж төлсөн Сайн дурын даатгалын хураамж,account.data_account_type_current_assets,account_tag_operating_insurance2,0,mn_chart_1 +account_template_1206_0101,12060101,"Цалингийн урьдчилгаа, богино хугацаат зээл",account.data_account_type_receivable,account_tag_operating_employee,1,mn_chart_1 +account_template_1206_0201,12060201,"Төлбөр, дутагдал",account.data_account_type_receivable,account_tag_operating_employee,1,mn_chart_1 +account_template_1206_0301,12060301,Худалдааны бус авлага ААН,account.data_account_type_receivable,account.account_tag_operating,1,mn_chart_1 +account_template_1206_0401,12060401,Худалдааны бус авлага Хувь хүн,account.data_account_type_receivable,account.account_tag_operating,1,mn_chart_1 +account_template_1206_0501,12060501,Худалдааны бус авлага Холбоотой тал,account.data_account_type_receivable,account.account_tag_operating,1,mn_chart_1 +account_template_1206_0601,12060601,Богино хугацаат зээлийн авлага ААН,account.data_account_type_receivable,account.account_tag_operating,1,mn_chart_1 +account_template_1206_0701,12060701,Богино хугацаат зээлийн авлага Хувь хүн,account.data_account_type_receivable,account.account_tag_operating,1,mn_chart_1 +account_template_1206_0801,12060801,Богино хугацаат зээлийн авлага Холбоотой тал,account.data_account_type_receivable,account.account_tag_operating,1,mn_chart_1 +account_template_1206_0901,12060901,Ногдол ашгийн авлага,account.data_account_type_receivable,account_tag_investing_dividends,1,mn_chart_1 +account_template_1206_1001,12061001,Барьцаалсан авлага,account.data_account_type_receivable,account.account_tag_operating,1,mn_chart_1 +account_template_1206_1101,12061101,Хүүний авлага,account.data_account_type_receivable,account_tag_operating_interest,1,mn_chart_1 +account_template_1206_1201,12061201,Үндсэн хөрөнгө худалдааны авлага,account.data_account_type_receivable,account_tag_investing_fasset,1,mn_chart_1 +account_template_1206_1301,12061301,Биет бус хөрөнгө худалдааны авлага,account.data_account_type_receivable,account_tag_investing_iasset,1,mn_chart_1 +account_template_1206_1401,12061401,Хөрөнгө оруулалт худалдааны авлага,account.data_account_type_receivable,account_tag_investing_invest,1,mn_chart_1 +account_template_1206_9901,12069901,Бусад худалдааны бус авлага,account.data_account_type_receivable,account.account_tag_operating,1,mn_chart_1 +account_template_1301_0101,13010101,Түргэн борлогдох үнэт цаас,account.data_account_type_current_assets,account.account_tag_financing,0,mn_chart_1 +account_template_1301_0201,13010201,Хугацаат хадгаламж,account.data_account_type_current_assets,account.account_tag_financing,0,mn_chart_1 +account_template_1302_0101,13020101,Бусад дэлгэрэнгүй орлого болох бодит үнэ цэнээр илэрхийлэх санхүүгийн хөрөнгө,account.data_account_type_current_assets,account.account_tag_operating,0,mn_chart_1 +account_template_1303_0101,13030101,Хорогдуулсан өртөгөөр илэрхийлэгдэх санхүүгийн өртөг,account.data_account_type_current_assets,account.account_tag_operating,0,mn_chart_1 +account_template_1401_0101,14010101,Агуулахад байгаа бараа материал,account.data_account_type_current_assets,,0,mn_chart_1 +account_template_1401_0401,14010401,Татан авалтын өртөгийн зөрүү,account.data_account_type_current_assets,,0,mn_chart_1 +account_template_1401_0701,14010701,Амьтан,account.data_account_type_current_assets,,0,mn_chart_1 +account_template_1401_0801,14010801,Мал,account.data_account_type_current_assets,,0,mn_chart_1 +account_template_1401_0901,14010901,Ургамал,account.data_account_type_current_assets,,0,mn_chart_1 +account_template_1402_0101,14020101,Агуулахад байгаа түүхий эд материал,account.data_account_type_current_assets,,0,mn_chart_1 +account_template_1403_0301,14030301,Агуулахад байгаа үндсэн хөрөнгийн хэсэг,account.data_account_type_current_assets,,0,mn_chart_1 +account_template_1403_0401,14030401,Агуулахад байгаа хангамжийн материал,account.data_account_type_current_assets,,0,mn_chart_1 +account_template_1403_0501,14030501,Ашиглалтанд байгаа хангамжийн материал,account.data_account_type_current_assets,,0,mn_chart_1 +account_template_1403_0502,14030502,Ашиглалтанд байгаа хангамжийн материалын элэгдэл,account.data_account_type_current_assets,,0,mn_chart_1 +account_template_1404_0101,14040101,Дуусаагүй үйлдвэрлэл - Шууд материал,account.data_account_type_current_assets,,0,mn_chart_1 +account_template_1404_0201,14040201,Дуусаагүй үйлдвэрлэл - Шууд хөдөлмөр,account.data_account_type_current_assets,,0,mn_chart_1 +account_template_1404_0301,14040301,Дуусаагүй үйлдвэрлэл - НЗ,account.data_account_type_current_assets,,0,mn_chart_1 +account_template_1404_0401,14040401,Хагас боловсруулсан бүтээгдэхүүн,account.data_account_type_current_assets,,0,mn_chart_1 +account_template_1405_0101,14050101,Агентад байгаа бараа,account.data_account_type_current_assets,,0,mn_chart_1 +account_template_1406_0101,14060101,Замд яваа бараа,account.data_account_type_current_assets,,0,mn_chart_1 +account_template_1407_0101,14070101,Ирж буй барааны өртөгийн түр данс,account.data_account_type_current_assets,,1,mn_chart_1 +account_template_1408_0101,14080101,Гарч буй барааны өртөгийн түр данс,account.data_account_type_current_assets,,1,mn_chart_1 +account_template_1501_0101,15010101,Урьдчилж төлсөн түрээс,account.data_account_type_prepayments,account_tag_operating_cost,0,mn_chart_1 +account_template_1501_0201,15010201,Урьдчилж төлсөн хураамж,account.data_account_type_prepayments,account_tag_operating_loyalty,0,mn_chart_1 +account_template_1501_0301,15010301,Урьдчилж төлсөн цалин,account.data_account_type_prepayments,account_tag_operating_employee,0,mn_chart_1 +account_template_1501_0901,15010901,Урьдчилж төлсөн бусад зардал,account.data_account_type_prepayments,account.account_tag_operating,0,mn_chart_1 +account_template_1502_0101,15020101,Бэлтгэн нийлүүлэгчид төлсөн урьдчилгаа,account.data_account_type_prepayments,account_tag_operating_product,0,mn_chart_1 +account_template_1502_0201,15020201,"Дараа тайлангийн урьдчилгаа, томилолт",account.data_account_type_prepayments,account_tag_operating_employee,0,mn_chart_1 +account_template_1502_0301,15020301,Үндсэн хөрөнгө худалдан авахад төлсөн урьдчилгаа,account.data_account_type_prepayments,account_tag_investing_fasset,0,mn_chart_1 +account_template_1502_0401,15020401,Биет бус хөрөнгө худалдан авахад төлсөн урьдчилгаа,account.data_account_type_prepayments,account_tag_investing_iasset,0,mn_chart_1 +account_template_1502_0501,15020501,Хөрөнгө оруулалт худалдан авахад төлсөн урьдчилгаа,account.data_account_type_prepayments,account_tag_investing_invest,0,mn_chart_1 +account_template_1502_0601,15020601,Хангамжийн зүйлс худалдан авахад төлсөн урьдчилгаа,account.data_account_type_prepayments,account_tag_operating_product,0,mn_chart_1 +account_template_1502_0901,15020901,Бусад худалдааны бус урьдчилгаа,account.data_account_type_prepayments,account.account_tag_operating,0,mn_chart_1 +account_template_1601_0101,16010101,Бусад эргэлтийн хөрөнгө,account.data_account_type_current_assets,account.account_tag_operating,0,mn_chart_1 +account_template_1701_0101,17010101,Борлуулах зорилгоор эзэмшиж буй эргэлтийн бус хөрөнгө,account.data_account_type_current_assets,account.account_tag_operating,0,mn_chart_1 +account_template_2001_0101,20010101,Биет хөрөнгө - Газрын сайржуулалт,account.data_account_type_fixed_assets,account_tag_investing_fasset,0,mn_chart_1 +account_template_2001_0201,20010201,Биет хөрөнгө - Газрын сайжруулалтын элэгдэл,account.data_account_type_fixed_assets,,0,mn_chart_1 +account_template_2002_0101,20020101,"Биет хөрөнгө - Барилга, байгууламж",account.data_account_type_fixed_assets,account_tag_investing_fasset,0,mn_chart_1 +account_template_2002_0201,20020201,"Биет хөрөнгө - Барилга, байгууламжын элэгдэл",account.data_account_type_fixed_assets,,0,mn_chart_1 +account_template_2003_0101,20030101,Биет хөрөнгө - Машин тоног төхөөрөмж,account.data_account_type_fixed_assets,account_tag_investing_fasset,0,mn_chart_1 +account_template_2003_0201,20030201,Биет хөрөнгө - Машин тоног төхөөрөмжийн элэгдэл,account.data_account_type_fixed_assets,,0,mn_chart_1 +account_template_2004_0101,20040101,Биет хөрөнгө - Тээврийн хэрэгсэл,account.data_account_type_fixed_assets,account_tag_investing_fasset,0,mn_chart_1 +account_template_2004_0201,20040201,Биет хөрөнгө - Тээврийн хэрэгслийн элэгдэл,account.data_account_type_fixed_assets,,0,mn_chart_1 +account_template_2005_0101,20050101,Биет хөрөнгө - Тавилга эд хогшил,account.data_account_type_fixed_assets,account_tag_investing_fasset,0,mn_chart_1 +account_template_2005_0201,20050201,Биет хөрөнгө - Тавилга эд хогшлын элэгдэл,account.data_account_type_fixed_assets,,0,mn_chart_1 +account_template_2006_0101,20060101,"Биет хөрөнгө - Компьютер, дагалдах хэрэгсэл",account.data_account_type_fixed_assets,account_tag_investing_fasset,0,mn_chart_1 +account_template_2006_0201,20060201,"Биет хөрөнгө - Компьютер, дагалдах хэрэгслийн элэгдэл",account.data_account_type_fixed_assets,,0,mn_chart_1 +account_template_2008_0101,20080101,Биет хөрөнгө - Дуусаагүй барилга,account.data_account_type_fixed_assets,account_tag_investing_fasset,0,mn_chart_1 +account_template_2008_0201,20080201,Биет хөрөнгө - Дуусаагүй барилгын элэгдэл,account.data_account_type_fixed_assets,,0,mn_chart_1 +account_template_2009_0101,20090101,Биет хөрөнгө - Бусад хөрөнгө,account.data_account_type_fixed_assets,account_tag_investing_fasset,0,mn_chart_1 +account_template_2009_0201,20090201,Биет хөрөнгө - Бусад хөрөнгийн элэгдэл,account.data_account_type_fixed_assets,,0,mn_chart_1 +account_template_2101_0101,21010101,Биет бус хөрөнгө - Зохиогчийн эрх,account.data_account_type_fixed_assets,account_tag_investing_fasset,0,mn_chart_1 +account_template_2101_0201,21010201,Биет бус хөрөнгө - Зохиогчийн эрхийн хорогдол,account.data_account_type_fixed_assets,,0,mn_chart_1 +account_template_2102_0101,21020101,Биет бус хөрөнгө - Програм хангамж,account.data_account_type_fixed_assets,account_tag_investing_fasset,0,mn_chart_1 +account_template_2102_0201,21020201,Биет бус хөрөнгө - Програм хангамжын хорогдол,account.data_account_type_fixed_assets,,0,mn_chart_1 +account_template_2103_0101,21030101,Биет бус хөрөнгө - Патент,account.data_account_type_fixed_assets,account_tag_investing_fasset,0,mn_chart_1 +account_template_2103_0201,21030201,Биет бус хөрөнгө - Патентын хорогдол,account.data_account_type_fixed_assets,,0,mn_chart_1 +account_template_2104_0101,21040101,Биет бус хөрөнгө - Барааны тэмдэг,account.data_account_type_fixed_assets,account_tag_investing_fasset,0,mn_chart_1 +account_template_2104_0201,21040201,Биет бус хөрөнгө - Барааны тэмдэгийн хорогдол,account.data_account_type_fixed_assets,,0,mn_chart_1 +account_template_2105_0101,21050101,Биет бус хөрөнгө - Тусгай зөвшөөрөл,account.data_account_type_fixed_assets,account_tag_investing_fasset,0,mn_chart_1 +account_template_2105_0201,21050201,Биет бус хөрөнгө - Тусгай зөвшөөрлийн хорогдол,account.data_account_type_fixed_assets,,0,mn_chart_1 +account_template_2109_0101,21090101,Биет бус хөрөнгө - Бусад хөрөнгө,account.data_account_type_fixed_assets,account_tag_investing_fasset,0,mn_chart_1 +account_template_2109_0201,21090201,Биет бус хөрөнгө - Бусад хөрөнгийн элэгдэл,account.data_account_type_fixed_assets,,0,mn_chart_1 +account_template_2401_0101,24010101,Биологийн хөрөнгө - Мал амьтад,account.data_account_type_fixed_assets,account_tag_investing_fasset,0,mn_chart_1 +account_template_2401_0201,24010201,Биологийн хөрөнгө - Мал амьтадын хорогдол,account.data_account_type_fixed_assets,,0,mn_chart_1 +account_template_2402_0101,24020101,Биологийн хөрөнгө - Ургамал,account.data_account_type_fixed_assets,account_tag_investing_fasset,0,mn_chart_1 +account_template_2402_0201,24020201,Биологийн хөрөнгө - Ургамалын хорогдол,account.data_account_type_fixed_assets,,0,mn_chart_1 +account_template_2201_0101,22010101,Ашиг алдагдлаарх бодит үнэ цэнээр илэрхийлэх санхүүгийн хөрөнгө,account.data_account_type_non_current_assets,account_tag_investing_invest,0,mn_chart_1 +account_template_2204_0101,22040101,"Хараат компани, хамтарсан үйлдвэрт оруулсан хөрөнгө оруулалт",account.data_account_type_non_current_assets,account_tag_investing_invest,0,mn_chart_1 +account_template_2205_0101,22050101,Охин компанид оруулсан хөрөнгө оруулалт,account.data_account_type_non_current_assets,account_tag_investing_invest,0,mn_chart_1 +account_template_2301_0101,23010101,Хайгуул ба үнэлгээний хөрөнгө,account.data_account_type_non_current_assets,account_tag_investing_invest,0,mn_chart_1 +account_template_2501_0101,25010101,Хойшлогдсон татварын хөрөнгө,account.data_account_type_non_current_assets,account.account_tag_financing,0,mn_chart_1 +account_template_2601_0101,26010101,Хөрөнгө оруулалтын зориулалттай үндсэн хөрөнгө,account.data_account_type_non_current_assets,account_tag_investing_invest,0,mn_chart_1 +account_template_2601_0201,26010201,Үндсэн хөрөнгийн үнэ цэнийн бууралт,account.data_account_type_non_current_assets,account_tag_investing_fasset,0,mn_chart_1 +account_template_2701_0101,27010101,Урт хугацаат зээлийн авлага ААН,account.data_account_type_non_current_assets,account_tag_investing_loan,0,mn_chart_1 +account_template_2701_0201,27010201,Урт хугацаат зээлийн авлага Хувь хүн,account.data_account_type_non_current_assets,account_tag_investing_loan,0,mn_chart_1 +account_template_2701_0301,27010301,Урт хугацаат зээлийн авлага Холбоотой тал,account.data_account_type_non_current_assets,account_tag_investing_loan,0,mn_chart_1 +account_template_2709_0101,27090101,Бусад эргэлтийн бус хөрөнгө,account.data_account_type_non_current_assets,account.account_tag_financing,0,mn_chart_1 +account_template_3101_0101,31010101,Худалдааны БН-д өгөх өглөг ААН,account.data_account_type_payable,account_tag_operating_product,1,mn_chart_1 +account_template_3101_0201,31010201,Худалдааны БН-д өгөх өглөг Хувь хүн,account.data_account_type_payable,account_tag_operating_product,1,mn_chart_1 +account_template_3101_0301,31010301,Худалдааны БН-д өгөх өглөг Холбоотой тал,account.data_account_type_payable,account_tag_operating_product,1,mn_chart_1 +account_template_3201_0101,32010101,Урьдчилж орсон худалдааны орлого,account.data_account_type_current_liabilities,account_tag_operating_product,1,mn_chart_1 +account_template_3202_0101,32020101,Урьдчилж орсон түрээсийн орлого,account.data_account_type_current_liabilities,account_tag_operating_product,1,mn_chart_1 +account_template_3203_0101,32030101,Урьдчилж орсон зээлийн хүүгийн орлого,account.data_account_type_current_liabilities,account_tag_operating_interest,1,mn_chart_1 +account_template_3301_0101,33010101,Цалингийн өглөг,account.data_account_type_payable,account_tag_operating_employee,1,mn_chart_1 +account_template_3401_0101,34010101,Татварын өглөг - ААНОАТатвар,account.data_account_type_current_liabilities,account_tag_operating_taxrefund,0,mn_chart_1 +account_template_3401_0201,34010201,Татварын өглөг - НӨАТатвар,account.data_account_type_current_liabilities,account_tag_operating_taxrefund,0,mn_chart_1 +account_template_3401_0301,34010301,Татварын өглөг - ХХОАТатвар,account.data_account_type_current_liabilities,account_tag_operating_taxrefund,0,mn_chart_1 +account_template_3401_0401,34010401,Татварын өглөг - Гаалийн албан татвар,account.data_account_type_current_liabilities,account_tag_operating_taxrefund,0,mn_chart_1 +account_template_3401_0501,34010501,Татварын өглөг - Үл хөдлөх хөрөнгийн албан татвар,account.data_account_type_current_liabilities,account_tag_operating_taxrefund,0,mn_chart_1 +account_template_3401_0601,34010601,Татварын өглөг - Газрын албан татвар,account.data_account_type_current_liabilities,account_tag_operating_taxrefund,0,mn_chart_1 +account_template_3401_0701,34010701,Татварын өглөг - АТӨЯХАТатвар,account.data_account_type_current_liabilities,account_tag_operating_taxrefund,0,mn_chart_1 +account_template_3401_0801,34010801,Татварын өглөг - Агаарын бохирдлын татвар,account.data_account_type_current_liabilities,account_tag_operating_taxrefund,0,mn_chart_1 +account_template_3401_0901,34010901,Татварын өглөг - Онцгой албан татвар,account.data_account_type_current_liabilities,account_tag_operating_taxrefund,0,mn_chart_1 +account_template_3401_1001,34011001,Татварын өглөг - Байгалийн нөөц ашиглалтын татвар,account.data_account_type_current_liabilities,account_tag_operating_taxrefund,0,mn_chart_1 +account_template_3401_1101,34011101,Татварын өглөг – НХАТатвар,account.data_account_type_current_liabilities,account_tag_operating_taxrefund,0,mn_chart_1 +account_template_3401_9901,34019901,Татварын өглөг - Бусад татвар,account.data_account_type_current_liabilities,account_tag_operating_taxrefund,0,mn_chart_1 +account_template_3402_0101,34020101,"ЭМ, НДШ-ийн өглөг",account.data_account_type_current_liabilities,account_tag_operating_insurance,0,mn_chart_1 +account_template_3402_0201,34020201,Сайн дурын даатгалын өглөг,account.data_account_type_current_liabilities,account_tag_operating_insurance2,0,mn_chart_1 +account_template_3501_0101,35010101,Банкны богино хугацаат зээлийн өглөг,account.data_account_type_payable,account_tag_operating_interest,1,mn_chart_1 +account_template_3501_0201,35010201,ББСБ-ын богино хугацаат зээлийн өглөг,account.data_account_type_payable,account_tag_operating_interest,1,mn_chart_1 +account_template_3501_0301,35010301,Хувь хүний богино хугацаат зээлийн өглөг,account.data_account_type_payable,account_tag_operating_interest,1,mn_chart_1 +account_template_3502_0101,35020101,Банкны богино хугацаат зээлийн хүү,account.data_account_type_payable,account_tag_operating_interest,1,mn_chart_1 +account_template_3502_0201,35020201,ББСБ-ын богино хугацаат зээлийн хүү,account.data_account_type_payable,account_tag_operating_interest,1,mn_chart_1 +account_template_3502_0301,35020301,Хувь хүний богино хугацаат зээлийн хүү,account.data_account_type_payable,account_tag_operating_interest,1,mn_chart_1 +account_template_3601_0101,36010101,Ногдол ашгийн өглөг,account.data_account_type_current_liabilities,account_tag_investing_dividends,0,mn_chart_1 +account_template_3701_0101,37010101,Нөөц өр төлбөр,account.data_account_type_current_liabilities,account_tag_exchange,0,mn_chart_1 +account_template_3801_0101,38010101,Даатгалын өглөг,account.data_account_type_payable,account_tag_operating_insurance2,1,mn_chart_1 +account_template_3802_0101,38020101,Гишүүнчлэлийн өглөг,account.data_account_type_payable,account_tag_operating_loyalty,1,mn_chart_1 +account_template_3803_0101,38030101,Ажилчдад өгөх өглөг,account.data_account_type_payable,account_tag_operating_employee,1,mn_chart_1 +account_template_3804_0101,38040101,Хувь хүмүүст өгөх өглөг,account.data_account_type_payable,account.account_tag_operating,1,mn_chart_1 +account_template_3805_0101,38050101,"Баталгаа, барьцааны өглөг",account.data_account_type_payable,account.account_tag_operating,1,mn_chart_1 +account_template_3806_0101,38060101,Худалдааны бус бэлтгэн нийлүүлэгчид өгөх өглөг - ААН,account.data_account_type_payable,account.account_tag_operating,1,mn_chart_1 +account_template_3806_0201,38060201,Худалдааны бус бэлтгэн нийлүүлэгчид өгөх өглөг - Хувь хүн,account.data_account_type_payable,account.account_tag_operating,1,mn_chart_1 +account_template_3806_0301,38060301,Худалдааны бус бэлтгэн нийлүүлэгчид өгөх өглөг - Холбоотой тал,account.data_account_type_payable,account.account_tag_operating,1,mn_chart_1 +account_template_3806_0401,38060401,Худалдан авсан үндсэн хөрөнгийн өглөг,account.data_account_type_payable,account_tag_investing_fasset,1,mn_chart_1 +account_template_3806_0501,38060501,Худалдан авсан хөрөнгө оруулалтын өглөг,account.data_account_type_payable,account_tag_investing_fasset,1,mn_chart_1 +account_template_3809_0101,38090101,Бусад богино хугацаат өр төлбөр,account.data_account_type_current_liabilities,account.account_tag_operating,0,mn_chart_1 +account_template_3901_0101,39010101,Борлуулах зорилгоор эзэмшиж буй эргэлтийн бус хөрөнгөнд хамаарах өр,account.data_account_type_current_liabilities,account_tag_exchange,0,mn_chart_1 +account_template_4001_0101,40010101,Банкны урт хугацаат зээл,account.data_account_type_non_current_liabilities,account_tag_investing_loan,0,mn_chart_1 +account_template_4001_0201,40010201,ББСБ-ын урт хугацаат зээл,account.data_account_type_non_current_liabilities,account_tag_investing_loan,0,mn_chart_1 +account_template_4001_0301,40010301,Хувь хүнээс авсан урт хугацаат зээл,account.data_account_type_non_current_liabilities,account_tag_investing_loan,0,mn_chart_1 +account_template_4001_0401,40010401,Холбоотой талын урт хугацаат зээл,account.data_account_type_non_current_liabilities,account_tag_investing_loan,0,mn_chart_1 +account_template_4002_0101,40020101,Урт хугацаат нөөц өр төлбөр,account.data_account_type_non_current_liabilities,account_tag_investing_loan,0,mn_chart_1 +account_template_4003_0101,40030101,Хойшлогдсон татварын өр,account.data_account_type_non_current_liabilities,account_tag_investing_loan,0,mn_chart_1 +account_template_4009_0101,40090101,Бусад урт хугацаат өр төлбөр,account.data_account_type_non_current_liabilities,account_tag_investing_loan,0,mn_chart_1 +account_template_4101_0101,41010101,Эздийн өмч - төрийн,account.data_account_type_equity,,0,mn_chart_1 +account_template_4102_0101,41020101,Эздийн өмч - хувийн,account.data_account_type_equity,,0,mn_chart_1 +account_template_4103_0101,41030101,Эздийн өмч - хувьцаат,account.data_account_type_equity,,0,mn_chart_1 +account_template_4201_0101,42010101,Халаасны хувьцаа,account.data_account_type_equity,,0,mn_chart_1 +account_template_4301_0101,43010101,Нэмж төлөгдсөн капитал,account.data_account_type_equity,,0,mn_chart_1 +account_template_4401_0101,44010101,Хөрөнгийн дахин үнэлгээний нэмэгдэл,account.data_account_type_equity,,0,mn_chart_1 +account_template_4501_0101,45010101,Гадаад валютын хөрвүүлэлтийн нөөц,account.data_account_type_equity,,0,mn_chart_1 +account_template_4509_0101,45090101,Эздийн өмчийн бусад хэсэг,account.data_account_type_equity,,0,mn_chart_1 +account_template_4601_0101,46010101,Тайлант үеийн хуримтлагдсан ашиг,account.data_account_type_equity,,0,mn_chart_1 +account_template_4601_0201,46010201,Өмнөх үеийн хуримтлагдсан ашиг,account.data_account_type_equity,,0,mn_chart_1 +account_template_5101_0101,51010101,Борлуулалтын орлого,account.data_account_type_revenue,"account_tag_operating_product,tax_report_tag7",0,mn_chart_1 +account_template_5101_0201,51010201,Борлуулалтын орлого (НӨАТ-с чөлөөлөгдөх),account.data_account_type_revenue,"account_tag_operating_product,tax_report_tag2",0,mn_chart_1 +account_template_5102_0101,51020101,Түрээсийн орлого,account.data_account_type_other_income,"account_tag_operating_product,tax_report_tag5",0,mn_chart_1 +account_template_5103_0101,51030101,Хүүний орлого,account.data_account_type_other_income,"account_tag_operating_interest,tax_report_tag39",0,mn_chart_1 +account_template_5104_0101,51040101,Ногдол ашгийн орлого,account.data_account_type_other_income,"account_tag_investing_dividends,tax_report_tag46",0,mn_chart_1 +account_template_5105_0101,51050101,Эрхийн шимтгэлийн орлого,account.data_account_type_other_income,"account_tag_operating_loyalty,tax_report_tag48",0,mn_chart_1 +account_template_5201_0101,52010101,Тооллогын зөрүүний олз,account.data_account_type_other_income,,0,mn_chart_1 +account_template_5201_0201,52010201,Тооллогын зөрүүний алдагдал,account.data_account_type_other_income,,0,mn_chart_1 +account_template_5201_0901,52010901,Бусад дэлгэрэнгүй орлого,account.data_account_type_other_income,"account.account_tag_operating,tax_report_tag8",0,mn_chart_1 +account_template_5301_0101,53010101,Валютын ханшийн зөрүүний хэрэгжсэн олз,account.data_account_type_other_income,,0,mn_chart_1 +account_template_5301_0201,53010201,Валютын ханшийн зөрүүний хэрэгжээгүй олз,account.data_account_type_other_income,,0,mn_chart_1 +account_template_5302_0101,53020101,Валютын ханшийн зөрүүний хэрэгжсэн гарз,account.data_account_type_other_income,,0,mn_chart_1 +account_template_5302_0201,53020201,Валютын ханшийн зөрүүний хэрэгжээгүй гарз,account.data_account_type_other_income,,0,mn_chart_1 +account_template_5401_0101,54010101,Үндсэн хөрөнгө борлуулсны олз,account.data_account_type_other_income,,0,mn_chart_1 +account_template_5401_0201,54010201,Үндсэн хөрөнгө борлуулсны гарз,account.data_account_type_other_income,,0,mn_chart_1 +account_template_5501_0101,55010101,Биет бус хөрөнгө борлуулсны олз,account.data_account_type_other_income,,0,mn_chart_1 +account_template_5501_0201,55010201,Биет бус хөрөнгө борлуулсны гарз,account.data_account_type_other_income,,0,mn_chart_1 +account_template_5601_0101,56010101,Хөрөнгө оруулалт борлуулсны олз,account.data_account_type_other_income,,0,mn_chart_1 +account_template_5601_0201,56010201,Хөрөнгө оруулалт борлуулсны гарз,account.data_account_type_other_income,,0,mn_chart_1 +account_template_5701_0101,57010101,Үндсэн бус үйл ажиллагааны бусад олз,account.data_account_type_other_income,,0,mn_chart_1 +account_template_5701_0201,57010201,Үндсэн бус үйл ажиллагааны бусад гарз,account.data_account_type_other_income,,0,mn_chart_1 +account_template_6001_0101,60010101,Борлуулалтын хөнгөлөлт,account.data_account_type_revenue,,0,mn_chart_1 +account_template_6002_0101,60020101,Борлуулалтын урамшуулал,account.data_account_type_revenue,,0,mn_chart_1 +account_template_6003_0101,60030101,Борлуулалтын буцаалт,account.data_account_type_revenue,,0,mn_chart_1 +account_template_6004_0101,60040101,Бэлнээр төлсөний хөнгөлөлт,account.data_account_type_revenue,,0,mn_chart_1 +account_template_6005_0101,60050101,Хугацаанаас өмнө төлсний хөнгөлөлт,account.data_account_type_revenue,,0,mn_chart_1 +account_template_6006_0101,60060101,Борлуулалтын үнийн бууралт,account.data_account_type_revenue,,0,mn_chart_1 +account_template_6101_0101,61010101,"Борлуулсан бараа, бүтээгдэхүүний өртөг",account.data_account_type_direct_costs,,0,mn_chart_1 +account_template_6102_0101,61020101,Борлуулсан үйлчилгээний өртөг,account.data_account_type_direct_costs,,0,mn_chart_1 +account_template_7001_0101,70010101,Удирд - Үндсэн цалингийн зардал,account.data_account_type_expenses,,0,mn_chart_1 +account_template_7001_0201,70010201,Удирд - Нэмэгдэл цалингийн зардал,account.data_account_type_expenses,,0,mn_chart_1 +account_template_7002_0101,70020101,Удирд - ЭМД-ийн зардал,account.data_account_type_expenses,,0,mn_chart_1 +account_template_7002_0201,70020201,Удирд - НДШ-ийн зардал,account.data_account_type_expenses,,0,mn_chart_1 +account_template_7002_0301,70020301,Удирд - Банкны шимтгэл хураамжын зардал,account.data_account_type_expenses,,0,mn_chart_1 +account_template_7002_0401,70020401,Удирд - Гадаад томилолтын зардал,account.data_account_type_expenses,,0,mn_chart_1 +account_template_7002_0402,70020402,Удирд - Дотоод томилолтын зардал,account.data_account_type_expenses,,0,mn_chart_1 +account_template_7002_0501,70020501,Удирд - Бичиг хэргийн зардал,account.data_account_type_expenses,,0,mn_chart_1 +account_template_7002_0601,70020601,Удирд - Шуудан холбооны зардал,account.data_account_type_expenses,,0,mn_chart_1 +account_template_7002_0602,70020602,Удирд - Суурин утасны зардал,account.data_account_type_expenses,,0,mn_chart_1 +account_template_7002_0603,70020603,Удирд - Үүрэн утасны зардал,account.data_account_type_expenses,,0,mn_chart_1 +account_template_7002_0604,70020604,Удирд - Интернетийн зардал,account.data_account_type_expenses,,0,mn_chart_1 +account_template_7002_0701,70020701,Удирд - Мэргэжлийн үйлчилгээний зардал,account.data_account_type_expenses,,0,mn_chart_1 +account_template_7002_0801,70020801,Удирд - Хүний нөөцийн зардал,account.data_account_type_expenses,,0,mn_chart_1 +account_template_7002_0901,70020901,Удирд - Сонин сэтгүүл захиалгын зардал,account.data_account_type_expenses,,0,mn_chart_1 +account_template_7002_1001,70021001,Удирд - Даатгалын зардал,account.data_account_type_expenses,,0,mn_chart_1 +account_template_7002_1101,70021101,Удирд - Ашиглалтын зардал,account.data_account_type_expenses,,0,mn_chart_1 +account_template_7002_1201,70021201,Удирд - Засварын зардал,account.data_account_type_expenses,,0,mn_chart_1 +account_template_7002_1301,70021301,Удирд - Элэгдэл хорогдлын зардал,account.data_account_type_expenses,,0,mn_chart_1 +account_template_7002_1401,70021401,Удирд - Түрээсийн зардал,account.data_account_type_expenses,,0,mn_chart_1 +account_template_7002_1501,70021501,Удирд - Харуул хамгаалалтын зардал,account.data_account_type_expenses,,0,mn_chart_1 +account_template_7002_1601,70021601,Удирд - Цэвэрлэгээ үйлчилгээний зардал,account.data_account_type_expenses,,0,mn_chart_1 +account_template_7002_1701,70021701,Удирд - Тээврийн зардал,account.data_account_type_expenses,,0,mn_chart_1 +account_template_7002_1801,70021801,Удирд - Шатахууны зардал,account.data_account_type_expenses,,0,mn_chart_1 +account_template_7002_1901,70021901,Удирд - Хүлээн авалтын зардал,account.data_account_type_expenses,,0,mn_chart_1 +account_template_7002_2001,70022001,Удирд - Зар сурталчилгааны зардал,account.data_account_type_expenses,,0,mn_chart_1 +account_template_7002_2101,70022101,Удирд - Төсөл хөгжлийн зардал,account.data_account_type_expenses,,0,mn_chart_1 +account_template_7002_2201,70022201,"Удирд - Судалгаа, туршилтын зардал",account.data_account_type_expenses,,0,mn_chart_1 +account_template_7002_2301,70022301,"Удирд - Хүргэлт, түгээлтийн зардал",account.data_account_type_expenses,,0,mn_chart_1 +account_template_7002_9901,70029901,Удирд - Үйл ажиллагааны бусад зардал,account.data_account_type_expenses,,0,mn_chart_1 +account_template_7101_0101,71010101,Борл - Үндсэн цалингийн зардал,account.data_account_type_expenses,,0,mn_chart_1 +account_template_7101_0201,71010201,Борл - Нэмэгдэл цалингийн зардал,account.data_account_type_expenses,,0,mn_chart_1 +account_template_7102_0101,71020101,Борл - ЭМД-ийн зардал,account.data_account_type_expenses,,0,mn_chart_1 +account_template_7102_0201,71020201,Борл - НДШ-ийн зардал,account.data_account_type_expenses,,0,mn_chart_1 +account_template_7102_0301,71020301,Борл - Банкны шимтгэл хураамжын зардал,account.data_account_type_expenses,,0,mn_chart_1 +account_template_7102_0401,71020401,Борл - Гадаад томилолтын зардал,account.data_account_type_expenses,,0,mn_chart_1 +account_template_7102_0402,71020402,Борл - Дотоод томилолтын зардал,account.data_account_type_expenses,,0,mn_chart_1 +account_template_7102_0501,71020501,Борл - Бичиг хэргийн зардал,account.data_account_type_expenses,,0,mn_chart_1 +account_template_7102_0601,71020601,Борл - Шуудан холбооны зардал,account.data_account_type_expenses,,0,mn_chart_1 +account_template_7102_0602,71020602,Борл - Суурин утасны зардал,account.data_account_type_expenses,,0,mn_chart_1 +account_template_7102_0603,71020603,Борл - Үүрэн утасны зардал,account.data_account_type_expenses,,0,mn_chart_1 +account_template_7102_0604,71020604,Борл - Интернетийн зардал,account.data_account_type_expenses,,0,mn_chart_1 +account_template_7102_0701,71020701,Борл - Мэргэжлийн үйлчилгээний зардал,account.data_account_type_expenses,,0,mn_chart_1 +account_template_7102_0801,71020801,Борл - Хүний нөөцийн зардал,account.data_account_type_expenses,,0,mn_chart_1 +account_template_7102_0901,71020901,Борл - Сонин сэтгүүл захиалгын зардал,account.data_account_type_expenses,,0,mn_chart_1 +account_template_7102_1001,71021001,Борл - Даатгалын зардал,account.data_account_type_expenses,,0,mn_chart_1 +account_template_7102_1101,71021101,Борл - Ашиглалтын зардал,account.data_account_type_expenses,,0,mn_chart_1 +account_template_7102_1201,71021201,Борл - Засварын зардал,account.data_account_type_expenses,,0,mn_chart_1 +account_template_7102_1301,71021301,Борл - Элэгдэл хорогдлын зардал,account.data_account_type_expenses,,0,mn_chart_1 +account_template_7102_1401,71021401,Борл - Түрээсийн зардал,account.data_account_type_expenses,,0,mn_chart_1 +account_template_7102_1501,71021501,Борл - Харуул хамгаалалтын зардал,account.data_account_type_expenses,,0,mn_chart_1 +account_template_7102_1601,71021601,Борл - Цэвэрлэгээ үйлчилгээний зардал,account.data_account_type_expenses,,0,mn_chart_1 +account_template_7102_1701,71021701,Борл - Тээврийн зардал,account.data_account_type_expenses,,0,mn_chart_1 +account_template_7102_1801,71021801,Борл - Шатахууны зардал,account.data_account_type_expenses,,0,mn_chart_1 +account_template_7102_1901,71021901,Борл - Хүлээн авалтын зардал,account.data_account_type_expenses,,0,mn_chart_1 +account_template_7102_2001,71022001,Борл - Зар сурталчилгааны зардал,account.data_account_type_expenses,,0,mn_chart_1 +account_template_7102_2101,71022101,Борл - Төсөл хөгжлийн зардал,account.data_account_type_expenses,,0,mn_chart_1 +account_template_7102_2201,71022201,"Борл - Судалгаа, туршилтын зардал",account.data_account_type_expenses,,0,mn_chart_1 +account_template_7102_2301,71022301,"Борл - Хүргэлт, түгээлтийн зардал",account.data_account_type_expenses,,0,mn_chart_1 +account_template_7102_9901,71029901,Борл - Үйл ажиллагааны бусад зардал,account.data_account_type_expenses,,0,mn_chart_1 +account_template_7201_0101,72010101,Эргэлтийн хөрөнгө санхүүжүүлэх зээлийн хүүгийн зардал,account.data_account_type_expenses,,0,mn_chart_1 +account_template_7202_0101,72020101,Үндсэн хөрөнгө санхүүжүүлэх зээлийн хүүгийн зардал,account.data_account_type_expenses,,0,mn_chart_1 +account_template_7203_0101,72030101,Төсөл санхүүжүүлэх зээлийн хүүгийн зардал,account.data_account_type_expenses,,0,mn_chart_1 +account_template_7301_0101,73010101,"Хайгуул, үнэлгээний зардал",account.data_account_type_expenses,,0,mn_chart_1 +account_template_7401_0101,74010101,"Алданги, торгуулийн зардал",account.data_account_type_expenses,,0,mn_chart_1 +account_template_7402_0101,74020101,"Хандив, тусламжийн зардал",account.data_account_type_expenses,,0,mn_chart_1 +account_template_7403_0101,74030101,Найдваргүй авлагын зардал,account.data_account_type_expenses,,0,mn_chart_1 +account_template_7404_0101,74040101,Болзошгүй алдагдлын санд төвлөрүүлсэн хөрөнгийн зардал,account.data_account_type_expenses,,0,mn_chart_1 +account_template_7405_0101,74050101,Франчайзын зардал,account.data_account_type_expenses,,0,mn_chart_1 +account_template_7406_0101,74060101,Банкны үйлчилгээний шимтгэлийн зардал,account.data_account_type_expenses,,0,mn_chart_1 +account_template_7407_0101,74070101,Хөрөнгө акталж устгасны зардал,account.data_account_type_expenses,,0,mn_chart_1 +account_template_7408_0101,74080101,Үйлдвэрийн ослын зардал,account.data_account_type_expenses,,0,mn_chart_1 +account_template_7409_0101,74090101,"Туслах үйлдвэрлэл, үйлчилгээний зардал",account.data_account_type_expenses,,0,mn_chart_1 +account_template_7410_0101,74100101,Үндсэн хөрөнгийн үнэ цэнийн бууралтын зардал,account.data_account_type_expenses,,0,mn_chart_1 +account_template_7499_0101,74990101,Бусад ерөнхий зардал,account.data_account_type_expenses,,0,mn_chart_1 +account_template_9101_0101,91010101,Тайлант үеийн орлогын албан татварын зардал,account.data_account_type_expenses,,0,mn_chart_1 +account_template_9102_0101,91020101,Хойшлогдсон татварын зардал,account.data_account_type_expenses,,0,mn_chart_1 +account_template_9103_0101,91030101,Татвар ногдохгүй бусад дэлгэрэнгүй орлого,account.data_account_type_expenses,,0,mn_chart_1 +account_template_9301_0101,93010101,Эхний үлдэгдлийн эх үүсвэр,account.data_unaffected_earnings,,0,mn_chart_1 diff --git a/addons/l10n_mn/data/account.chart.template.csv b/addons/l10n_mn/data/account.chart.template.csv new file mode 100644 index 00000000000..3c63ba77781 --- /dev/null +++ b/addons/l10n_mn/data/account.chart.template.csv @@ -0,0 +1,2 @@ +id,property_account_receivable_id/id,property_account_payable_id/id,property_account_expense_categ_id/id,property_account_income_categ_id/id,income_currency_exchange_account_id/id,expense_currency_exchange_account_id/id +mn_chart_1,account_template_1201_0201,account_template_3101_0201,account_template_7002_1101,account_template_5101_0201,account_template_5301_0201,account_template_5302_0201 diff --git a/addons/l10n_mn/data/account.tax.group.csv b/addons/l10n_mn/data/account.tax.group.csv new file mode 100644 index 00000000000..9e2a71ba648 --- /dev/null +++ b/addons/l10n_mn/data/account.tax.group.csv @@ -0,0 +1,7 @@ +id,name,sequence +account_tax_group1,НӨАТ ногдох бараа,10 +account_tax_group2,НӨАТ ногдох үйлчилгээ,10 +account_tax_group3,Экспортын борлуулалт,11 +account_tax_group4,"Худалдан авсан бараа, үйлчилгээ",12 +account_tax_group5,Санхүүгийн түрээсийн зүйл,13 +account_tax_group6,НӨАТ-с чөлөөлөгдөх,14 diff --git a/addons/l10n_mn/data/account_chart_template_configuration_data.xml b/addons/l10n_mn/data/account_chart_template_configuration_data.xml new file mode 100644 index 00000000000..2de735fa360 --- /dev/null +++ b/addons/l10n_mn/data/account_chart_template_configuration_data.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo noupdate="1"> + + <function model="account.chart.template" name="try_loading_for_current_company"> + <value eval="[ref('l10n_mn.mn_chart_1')]"/> + </function> + +</odoo> diff --git a/addons/l10n_mn/data/account_chart_template_data.xml b/addons/l10n_mn/data/account_chart_template_data.xml new file mode 100644 index 00000000000..bb64e86c95e --- /dev/null +++ b/addons/l10n_mn/data/account_chart_template_data.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + + <menuitem id="account_reports_mn_statements_menu" name="Mongolia" parent="account.menu_finance_reports" sequence="0" groups="account.group_account_user"/> + + <record id="mn_chart_1" model="account.chart.template"> + <field name="name">Mongolia</field> + <field name="bank_account_code_prefix">11</field> + <field name="cash_account_code_prefix">10</field> + <field name="transfer_account_code_prefix">1109</field> + <field name="code_digits">8</field> + <field name="currency_id" ref="base.MNT"/> + </record> + +</odoo> diff --git a/addons/l10n_mn/data/account_fiscal_position_template_data.xml b/addons/l10n_mn/data/account_fiscal_position_template_data.xml new file mode 100644 index 00000000000..94780439b51 --- /dev/null +++ b/addons/l10n_mn/data/account_fiscal_position_template_data.xml @@ -0,0 +1,399 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + <!-- = = = = = = = = = = = = = = = --> + <!-- Fiscal Position Templates --> + <!-- = = = = = = = = = = = = = = = --> + <record id="fiscal_position_vat0" model="account.fiscal.position.template"> + <field name="name">Дотоодын борлуулалт - Худалдан авалт</field> + <field name="chart_template_id" ref="mn_chart_1"/> + <field name="auto_apply" eval="True" /> + <field name="country_id" ref="base.mn"/> + </record> + + <record id="fiscal_position_vat2" model="account.fiscal.position.template"> + <field name="name">НӨАТ 0% (3000000)</field> + <field name="chart_template_id" ref="mn_chart_1"/> + </record> + + <record id="fiscal_position_vat3" model="account.fiscal.position.template"> + <field name="name">НӨАТ-гүй (2000000)</field> + <field name="chart_template_id" ref="mn_chart_1"/> + </record> + + <record id="fiscal_position_vat4" model="account.fiscal.position.template"> + <field name="name">Гадаад борлуулалт - Худалдан авалт</field> + <field name="chart_template_id" ref="mn_chart_1"/> + <field name="auto_apply" eval="True" /> + </record> + + <!-- = = = = = = = = = = = = = = = --> + <!-- Fiscal Position Tax Templates --> + <!-- = = = = = = = = = = = = = = = --> + + <record id="fp_tax_template_vat2_sale_vat1" model="account.fiscal.position.tax.template"> + <field name="position_id" ref="fiscal_position_vat2" /> + <field name="tax_src_id" ref="account_tax_sale_vat1" /> + <field name="tax_dest_id" ref="account_tax_sale_vat2" /> + </record> + + <record id="fp_tax_template_vat2_sale_vat4" model="account.fiscal.position.tax.template"> + <field name="position_id" ref="fiscal_position_vat2" /> + <field name="tax_src_id" ref="account_tax_sale_vat4" /> + <field name="tax_dest_id" ref="account_tax_sale_vat2" /> + </record> + + <record id="fp_tax_template_vat2_sale_vat5" model="account.fiscal.position.tax.template"> + <field name="position_id" ref="fiscal_position_vat2" /> + <field name="tax_src_id" ref="account_tax_sale_vat5" /> + <field name="tax_dest_id" ref="account_tax_sale_vat2" /> + </record> + + <record id="fp_tax_template_vat2_sale_vat6" model="account.fiscal.position.tax.template"> + <field name="position_id" ref="fiscal_position_vat2" /> + <field name="tax_src_id" ref="account_tax_sale_vat6" /> + <field name="tax_dest_id" ref="account_tax_sale_vat2" /> + </record> + + <record id="fp_tax_template_vat2_sale_vat7" model="account.fiscal.position.tax.template"> + <field name="position_id" ref="fiscal_position_vat2" /> + <field name="tax_src_id" ref="account_tax_sale_vat7" /> + <field name="tax_dest_id" ref="account_tax_sale_vat2" /> + </record> + + <record id="fp_tax_template_vat2_sale_vat8" model="account.fiscal.position.tax.template"> + <field name="position_id" ref="fiscal_position_vat2" /> + <field name="tax_src_id" ref="account_tax_sale_vat8" /> + <field name="tax_dest_id" ref="account_tax_sale_vat2" /> + </record> + + <record id="fp_tax_template_vat2_sale_vat9" model="account.fiscal.position.tax.template"> + <field name="position_id" ref="fiscal_position_vat2" /> + <field name="tax_src_id" ref="account_tax_sale_vat9" /> + <field name="tax_dest_id" ref="account_tax_sale_vat2" /> + </record> + + <record id="fp_tax_template_vat2_sale_vat10" model="account.fiscal.position.tax.template"> + <field name="position_id" ref="fiscal_position_vat2" /> + <field name="tax_src_id" ref="account_tax_sale_vat10" /> + <field name="tax_dest_id" ref="account_tax_sale_vat2" /> + </record> + + <record id="fp_tax_template_vat2_sale_vat11" model="account.fiscal.position.tax.template"> + <field name="position_id" ref="fiscal_position_vat2" /> + <field name="tax_src_id" ref="account_tax_sale_vat11" /> + <field name="tax_dest_id" ref="account_tax_sale_vat2" /> + </record> + + <record id="fp_tax_template_vat2_sale_vat12" model="account.fiscal.position.tax.template"> + <field name="position_id" ref="fiscal_position_vat2" /> + <field name="tax_src_id" ref="account_tax_sale_vat12" /> + <field name="tax_dest_id" ref="account_tax_sale_vat2" /> + </record> + + <record id="fp_tax_template_vat2_sale_vat13" model="account.fiscal.position.tax.template"> + <field name="position_id" ref="fiscal_position_vat2" /> + <field name="tax_src_id" ref="account_tax_sale_vat13" /> + <field name="tax_dest_id" ref="account_tax_sale_vat2" /> + </record> + + <record id="fp_tax_template_vat2_sale_vat14" model="account.fiscal.position.tax.template"> + <field name="position_id" ref="fiscal_position_vat2" /> + <field name="tax_src_id" ref="account_tax_sale_vat14" /> + <field name="tax_dest_id" ref="account_tax_sale_vat2" /> + </record> + + <record id="fp_tax_template_vat2_sale_vat15" model="account.fiscal.position.tax.template"> + <field name="position_id" ref="fiscal_position_vat2" /> + <field name="tax_src_id" ref="account_tax_sale_vat15" /> + <field name="tax_dest_id" ref="account_tax_sale_vat2" /> + </record> + + <record id="fp_tax_template_vat2_sale_vat16" model="account.fiscal.position.tax.template"> + <field name="position_id" ref="fiscal_position_vat2" /> + <field name="tax_src_id" ref="account_tax_sale_vat16" /> + <field name="tax_dest_id" ref="account_tax_sale_vat2" /> + </record> + + <record id="fp_tax_template_vat2_sale_vat17" model="account.fiscal.position.tax.template"> + <field name="position_id" ref="fiscal_position_vat2" /> + <field name="tax_src_id" ref="account_tax_sale_vat17" /> + <field name="tax_dest_id" ref="account_tax_sale_vat2" /> + </record> + + <record id="fp_tax_template_vat2_sale_vat18" model="account.fiscal.position.tax.template"> + <field name="position_id" ref="fiscal_position_vat2" /> + <field name="tax_src_id" ref="account_tax_sale_vat18" /> + <field name="tax_dest_id" ref="account_tax_sale_vat2" /> + </record> + + <record id="fp_tax_template_vat2_sale_vat19" model="account.fiscal.position.tax.template"> + <field name="position_id" ref="fiscal_position_vat2" /> + <field name="tax_src_id" ref="account_tax_sale_vat19" /> + <field name="tax_dest_id" ref="account_tax_sale_vat2" /> + </record> + + <record id="fp_tax_template_vat2_sale_vat20" model="account.fiscal.position.tax.template"> + <field name="position_id" ref="fiscal_position_vat2" /> + <field name="tax_src_id" ref="account_tax_sale_vat20" /> + <field name="tax_dest_id" ref="account_tax_sale_vat2" /> + </record> + + <record id="fp_tax_template_vat2_sale_vat21" model="account.fiscal.position.tax.template"> + <field name="position_id" ref="fiscal_position_vat2" /> + <field name="tax_src_id" ref="account_tax_sale_vat21" /> + <field name="tax_dest_id" ref="account_tax_sale_vat2" /> + </record> + + <record id="fp_tax_template_vat2_sale_vat22" model="account.fiscal.position.tax.template"> + <field name="position_id" ref="fiscal_position_vat2" /> + <field name="tax_src_id" ref="account_tax_sale_vat22" /> + <field name="tax_dest_id" ref="account_tax_sale_vat2" /> + </record> + + <record id="fp_tax_template_vat3_sale_vat1" model="account.fiscal.position.tax.template"> + <field name="position_id" ref="fiscal_position_vat3" /> + <field name="tax_src_id" ref="account_tax_sale_vat1" /> + <field name="tax_dest_id" ref="account_tax_sale_vat3" /> + </record> + + <record id="fp_tax_template_vat3_sale_vat4" model="account.fiscal.position.tax.template"> + <field name="position_id" ref="fiscal_position_vat3" /> + <field name="tax_src_id" ref="account_tax_sale_vat4" /> + <field name="tax_dest_id" ref="account_tax_sale_vat3" /> + </record> + + <record id="fp_tax_template_vat3_sale_vat5" model="account.fiscal.position.tax.template"> + <field name="position_id" ref="fiscal_position_vat3" /> + <field name="tax_src_id" ref="account_tax_sale_vat5" /> + <field name="tax_dest_id" ref="account_tax_sale_vat3" /> + </record> + + <record id="fp_tax_template_vat3_sale_vat6" model="account.fiscal.position.tax.template"> + <field name="position_id" ref="fiscal_position_vat3" /> + <field name="tax_src_id" ref="account_tax_sale_vat6" /> + <field name="tax_dest_id" ref="account_tax_sale_vat3" /> + </record> + + <record id="fp_tax_template_vat3_sale_vat7" model="account.fiscal.position.tax.template"> + <field name="position_id" ref="fiscal_position_vat3" /> + <field name="tax_src_id" ref="account_tax_sale_vat7" /> + <field name="tax_dest_id" ref="account_tax_sale_vat3" /> + </record> + + <record id="fp_tax_template_vat3_sale_vat8" model="account.fiscal.position.tax.template"> + <field name="position_id" ref="fiscal_position_vat3" /> + <field name="tax_src_id" ref="account_tax_sale_vat8" /> + <field name="tax_dest_id" ref="account_tax_sale_vat3" /> + </record> + + <record id="fp_tax_template_vat3_sale_vat9" model="account.fiscal.position.tax.template"> + <field name="position_id" ref="fiscal_position_vat3" /> + <field name="tax_src_id" ref="account_tax_sale_vat9" /> + <field name="tax_dest_id" ref="account_tax_sale_vat3" /> + </record> + + <record id="fp_tax_template_vat3_sale_vat10" model="account.fiscal.position.tax.template"> + <field name="position_id" ref="fiscal_position_vat3" /> + <field name="tax_src_id" ref="account_tax_sale_vat10" /> + <field name="tax_dest_id" ref="account_tax_sale_vat3" /> + </record> + + <record id="fp_tax_template_vat3_sale_vat11" model="account.fiscal.position.tax.template"> + <field name="position_id" ref="fiscal_position_vat3" /> + <field name="tax_src_id" ref="account_tax_sale_vat11" /> + <field name="tax_dest_id" ref="account_tax_sale_vat3" /> + </record> + + <record id="fp_tax_template_vat3_sale_vat12" model="account.fiscal.position.tax.template"> + <field name="position_id" ref="fiscal_position_vat3" /> + <field name="tax_src_id" ref="account_tax_sale_vat12" /> + <field name="tax_dest_id" ref="account_tax_sale_vat3" /> + </record> + + <record id="fp_tax_template_vat3_sale_vat13" model="account.fiscal.position.tax.template"> + <field name="position_id" ref="fiscal_position_vat3" /> + <field name="tax_src_id" ref="account_tax_sale_vat13" /> + <field name="tax_dest_id" ref="account_tax_sale_vat3" /> + </record> + + <record id="fp_tax_template_vat3_sale_vat14" model="account.fiscal.position.tax.template"> + <field name="position_id" ref="fiscal_position_vat3" /> + <field name="tax_src_id" ref="account_tax_sale_vat14" /> + <field name="tax_dest_id" ref="account_tax_sale_vat3" /> + </record> + + <record id="fp_tax_template_vat3_sale_vat15" model="account.fiscal.position.tax.template"> + <field name="position_id" ref="fiscal_position_vat3" /> + <field name="tax_dest_id" ref="account_tax_sale_vat15" /> + <field name="tax_src_id" ref="account_tax_sale_vat3" /> + </record> + + <record id="fp_tax_template_vat3_sale_vat16" model="account.fiscal.position.tax.template"> + <field name="position_id" ref="fiscal_position_vat3" /> + <field name="tax_src_id" ref="account_tax_sale_vat16" /> + <field name="tax_dest_id" ref="account_tax_sale_vat3" /> + </record> + + <record id="fp_tax_template_vat3_sale_vat17" model="account.fiscal.position.tax.template"> + <field name="position_id" ref="fiscal_position_vat3" /> + <field name="tax_src_id" ref="account_tax_sale_vat17" /> + <field name="tax_dest_id" ref="account_tax_sale_vat3" /> + </record> + + <record id="fp_tax_template_vat3_sale_vat18" model="account.fiscal.position.tax.template"> + <field name="position_id" ref="fiscal_position_vat3" /> + <field name="tax_src_id" ref="account_tax_sale_vat18" /> + <field name="tax_dest_id" ref="account_tax_sale_vat3" /> + </record> + + <record id="fp_tax_template_vat3_sale_vat19" model="account.fiscal.position.tax.template"> + <field name="position_id" ref="fiscal_position_vat3" /> + <field name="tax_src_id" ref="account_tax_sale_vat19" /> + <field name="tax_dest_id" ref="account_tax_sale_vat3" /> + </record> + + <record id="fp_tax_template_vat3_sale_vat20" model="account.fiscal.position.tax.template"> + <field name="position_id" ref="fiscal_position_vat3" /> + <field name="tax_src_id" ref="account_tax_sale_vat20" /> + <field name="tax_dest_id" ref="account_tax_sale_vat3" /> + </record> + + <record id="fp_tax_template_vat3_sale_vat21" model="account.fiscal.position.tax.template"> + <field name="position_id" ref="fiscal_position_vat3" /> + <field name="tax_src_id" ref="account_tax_sale_vat21" /> + <field name="tax_dest_id" ref="account_tax_sale_vat3" /> + </record> + + <record id="fp_tax_template_vat3_sale_vat22" model="account.fiscal.position.tax.template"> + <field name="position_id" ref="fiscal_position_vat3" /> + <field name="tax_src_id" ref="account_tax_sale_vat22" /> + <field name="tax_dest_id" ref="account_tax_sale_vat3" /> + </record> + + <record id="fp_tax_template_vat3_sale_vat1" model="account.fiscal.position.tax.template"> + <field name="position_id" ref="fiscal_position_vat4" /> + <field name="tax_src_id" ref="account_tax_sale_vat1" /> + <field name="tax_dest_id" ref="account_tax_sale_vat23" /> + </record> + + <record id="fp_tax_template_vat3_sale_vat4" model="account.fiscal.position.tax.template"> + <field name="position_id" ref="fiscal_position_vat4" /> + <field name="tax_src_id" ref="account_tax_sale_vat4" /> + <field name="tax_dest_id" ref="account_tax_sale_vat23" /> + </record> + + <record id="fp_tax_template_vat3_sale_vat5" model="account.fiscal.position.tax.template"> + <field name="position_id" ref="fiscal_position_vat4" /> + <field name="tax_src_id" ref="account_tax_sale_vat5" /> + <field name="tax_dest_id" ref="account_tax_sale_vat23" /> + </record> + + <record id="fp_tax_template_vat3_sale_vat6" model="account.fiscal.position.tax.template"> + <field name="position_id" ref="fiscal_position_vat4" /> + <field name="tax_src_id" ref="account_tax_sale_vat6" /> + <field name="tax_dest_id" ref="account_tax_sale_vat23" /> + </record> + + <record id="fp_tax_template_vat3_sale_vat7" model="account.fiscal.position.tax.template"> + <field name="position_id" ref="fiscal_position_vat4" /> + <field name="tax_src_id" ref="account_tax_sale_vat7" /> + <field name="tax_dest_id" ref="account_tax_sale_vat23" /> + </record> + + <record id="fp_tax_template_vat3_sale_vat8" model="account.fiscal.position.tax.template"> + <field name="position_id" ref="fiscal_position_vat4" /> + <field name="tax_src_id" ref="account_tax_sale_vat8" /> + <field name="tax_dest_id" ref="account_tax_sale_vat24" /> + </record> + + <record id="fp_tax_template_vat3_sale_vat9" model="account.fiscal.position.tax.template"> + <field name="position_id" ref="fiscal_position_vat4" /> + <field name="tax_src_id" ref="account_tax_sale_vat9" /> + <field name="tax_dest_id" ref="account_tax_sale_vat24" /> + </record> + + <record id="fp_tax_template_vat3_sale_vat10" model="account.fiscal.position.tax.template"> + <field name="position_id" ref="fiscal_position_vat4" /> + <field name="tax_src_id" ref="account_tax_sale_vat10" /> + <field name="tax_dest_id" ref="account_tax_sale_vat24" /> + </record> + + <record id="fp_tax_template_vat3_sale_vat11" model="account.fiscal.position.tax.template"> + <field name="position_id" ref="fiscal_position_vat4" /> + <field name="tax_src_id" ref="account_tax_sale_vat11" /> + <field name="tax_dest_id" ref="account_tax_sale_vat24" /> + </record> + + <record id="fp_tax_template_vat3_sale_vat12" model="account.fiscal.position.tax.template"> + <field name="position_id" ref="fiscal_position_vat4" /> + <field name="tax_src_id" ref="account_tax_sale_vat12" /> + <field name="tax_dest_id" ref="account_tax_sale_vat24" /> + </record> + + <record id="fp_tax_template_vat3_sale_vat13" model="account.fiscal.position.tax.template"> + <field name="position_id" ref="fiscal_position_vat4" /> + <field name="tax_src_id" ref="account_tax_sale_vat13" /> + <field name="tax_dest_id" ref="account_tax_sale_vat24" /> + </record> + + <record id="fp_tax_template_vat3_sale_vat14" model="account.fiscal.position.tax.template"> + <field name="position_id" ref="fiscal_position_vat4" /> + <field name="tax_src_id" ref="account_tax_sale_vat14" /> + <field name="tax_dest_id" ref="account_tax_sale_vat24" /> + </record> + + <record id="fp_tax_template_vat3_sale_vat15" model="account.fiscal.position.tax.template"> + <field name="position_id" ref="fiscal_position_vat4" /> + <field name="tax_dest_id" ref="account_tax_sale_vat15" /> + <field name="tax_src_id" ref="account_tax_sale_vat24" /> + </record> + + <record id="fp_tax_template_vat3_sale_vat16" model="account.fiscal.position.tax.template"> + <field name="position_id" ref="fiscal_position_vat4" /> + <field name="tax_src_id" ref="account_tax_sale_vat16" /> + <field name="tax_dest_id" ref="account_tax_sale_vat24" /> + </record> + + <record id="fp_tax_template_vat3_sale_vat17" model="account.fiscal.position.tax.template"> + <field name="position_id" ref="fiscal_position_vat4" /> + <field name="tax_src_id" ref="account_tax_sale_vat17" /> + <field name="tax_dest_id" ref="account_tax_sale_vat24" /> + </record> + + <record id="fp_tax_template_vat3_sale_vat18" model="account.fiscal.position.tax.template"> + <field name="position_id" ref="fiscal_position_vat4" /> + <field name="tax_src_id" ref="account_tax_sale_vat18" /> + <field name="tax_dest_id" ref="account_tax_sale_vat24" /> + </record> + + <record id="fp_tax_template_vat3_sale_vat19" model="account.fiscal.position.tax.template"> + <field name="position_id" ref="fiscal_position_vat4" /> + <field name="tax_src_id" ref="account_tax_sale_vat19" /> + <field name="tax_dest_id" ref="account_tax_sale_vat24" /> + </record> + + <record id="fp_tax_template_vat3_sale_vat20" model="account.fiscal.position.tax.template"> + <field name="position_id" ref="fiscal_position_vat4" /> + <field name="tax_src_id" ref="account_tax_sale_vat20" /> + <field name="tax_dest_id" ref="account_tax_sale_vat24" /> + </record> + + <record id="fp_tax_template_vat3_sale_vat21" model="account.fiscal.position.tax.template"> + <field name="position_id" ref="fiscal_position_vat4" /> + <field name="tax_src_id" ref="account_tax_sale_vat21" /> + <field name="tax_dest_id" ref="account_tax_sale_vat24" /> + </record> + + <record id="fp_tax_template_vat3_sale_vat22" model="account.fiscal.position.tax.template"> + <field name="position_id" ref="fiscal_position_vat4" /> + <field name="tax_src_id" ref="account_tax_sale_vat22" /> + <field name="tax_dest_id" ref="account_tax_sale_vat24" /> + </record> + + <record id="fp_tax_template_vat3_purchase_vat1" model="account.fiscal.position.tax.template"> + <field name="position_id" ref="fiscal_position_vat4" /> + <field name="tax_src_id" ref="account_tax_purchase_vat1" /> + <field name="tax_dest_id" ref="account_tax_purchase_vat2" /> + </record> + +</odoo> diff --git a/addons/l10n_mn/data/account_tax_template_data.xml b/addons/l10n_mn/data/account_tax_template_data.xml new file mode 100644 index 00000000000..8f5d16d9b75 --- /dev/null +++ b/addons/l10n_mn/data/account_tax_template_data.xml @@ -0,0 +1,1476 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + + <record id="account_tax_sale_vat1" model="account.tax.template"> + <field name="name">НӨАТ бараа</field> + <field name="amount">10.0</field> + <field name="price_include" eval="1"/> + <field name="sequence">1</field> + <field name="amount_type">percent</field> + <field name="type_tax_use">sale</field> + <field name="description">10%</field> + <field name="tax_group_id" ref="account_tax_group1"/> + <field name="chart_template_id" ref="mn_chart_1"/> + <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'tag_ids': [ref('vat_report_tag5')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('account_template_3401_0201'), + 'tag_ids': [ref('vat_report_tag5')], + }), + ]"/> + <field name="refund_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'tag_ids': [ref('vat_report_tag5')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('account_template_3401_0201'), + 'tag_ids': [ref('vat_report_tag5')], + }), + ]"/> + </record> + + <record id="account_tax_purchase_vat1" model="account.tax.template"> + <field name="name">НӨАТ-тэй худалдан авалт</field> + <field name="amount">10.0</field> + <field name="price_include" eval="1"/> + <field name="sequence">1</field> + <field name="amount_type">percent</field> + <field name="type_tax_use">purchase</field> + <field name="description">10%</field> + <field name="tax_group_id" ref="account_tax_group4"/> + <field name="chart_template_id" ref="mn_chart_1"/> + <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'tag_ids': [ref('vat_report_tag36'),ref('vat_report_tag33')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('account_template_1204_0301'), + 'tag_ids': [ref('vat_report_tag36')], + }), + ]"/> + <field name="refund_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'tag_ids': [ref('vat_report_tag36'),ref('vat_report_tag33')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('account_template_1204_0301'), + 'tag_ids': [ref('vat_report_tag36')], + }), + ]"/> + </record> + + <record id="account_tax_purchase_vat2" model="account.tax.template"> + <field name="name">НӨАТ-гүй худалдан авалт</field> + <field name="amount">0.0</field> + <field name="price_include" eval="1"/> + <field name="sequence">10</field> + <field name="amount_type">percent</field> + <field name="type_tax_use">purchase</field> + <field name="description">0%</field> + <field name="tax_group_id" ref="account_tax_group4"/> + <field name="chart_template_id" ref="mn_chart_1"/> + <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'tag_ids': [ref('vat_report_tag33')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('account_template_1204_0301'), + }), + ]"/> + <field name="refund_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'tag_ids': [ref('vat_report_tag33')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('account_template_1204_0301'), + }), + ]"/> + </record> + + <record id="account_tax_sale_vat2" model="account.tax.template"> + <field name="name">НӨАТ 0% борлуулалт</field> + <field name="amount">0.0</field> + <field name="price_include" eval="1"/> + <field name="sequence">10</field> + <field name="amount_type">percent</field> + <field name="type_tax_use">sale</field> + <field name="description">0%</field> + <field name="tax_group_id" ref="account_tax_group6"/> + <field name="chart_template_id" ref="mn_chart_1"/> + <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'tag_ids': [ref('vat_report_tag2')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('account_template_3401_0201'), + }), + ]"/> + <field name="refund_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'tag_ids': [ref('vat_report_tag2')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('account_template_3401_0201'), + }), + ]"/> + </record> + + <record id="account_tax_sale_vat3" model="account.tax.template"> + <field name="name">НӨАТ чөлөөлөх борлуулалт</field> + <field name="amount">0.0</field> + <field name="price_include" eval="1"/> + <field name="sequence">10</field> + <field name="amount_type">percent</field> + <field name="type_tax_use">sale</field> + <field name="description">0%</field> + <field name="tax_group_id" ref="account_tax_group6"/> + <field name="chart_template_id" ref="mn_chart_1"/> + <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'tag_ids': [ref('vat_report_tag2')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('account_template_3401_0201'), + }), + ]"/> + <field name="refund_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'tag_ids': [ref('vat_report_tag2')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('account_template_3401_0201'), + }), + ]"/> + </record> + + <record id="account_tax_sale_vat4" model="account.tax.template"> + <field name="name">НӨАТ бусад барааны борлуулалт</field> + <field name="amount">10.0</field> + <field name="price_include" eval="1"/> + <field name="sequence">20</field> + <field name="amount_type">percent</field> + <field name="type_tax_use">sale</field> + <field name="description">10%</field> + <field name="tax_group_id" ref="account_tax_group1"/> + <field name="chart_template_id" ref="mn_chart_1"/> + <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'tag_ids': [ref('vat_report_tag6')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('account_template_3401_0201'), + 'tag_ids': [ref('vat_report_tag6')], + }), + ]"/> + <field name="refund_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'tag_ids': [ref('vat_report_tag6')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('account_template_3401_0201'), + 'tag_ids': [ref('vat_report_tag6')], + }), + ]"/> + </record> + + <record id="account_tax_sale_vat5" model="account.tax.template"> + <field name="name">НӨАТ эрх борлуулалт</field> + <field name="amount">10.0</field> + <field name="price_include" eval="1"/> + <field name="sequence">20</field> + <field name="amount_type">percent</field> + <field name="type_tax_use">sale</field> + <field name="description">10%</field> + <field name="tax_group_id" ref="account_tax_group1"/> + <field name="chart_template_id" ref="mn_chart_1"/> + <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'tag_ids': [ref('vat_report_tag7')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('account_template_3401_0201'), + 'tag_ids': [ref('vat_report_tag7')], + }), + ]"/> + <field name="refund_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'tag_ids': [ref('vat_report_tag7')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('account_template_3401_0201'), + 'tag_ids': [ref('vat_report_tag7')], + }), + ]"/> + </record> + + <record id="account_tax_sale_vat6" model="account.tax.template"> + <field name="name">НӨАТ татан буугдах үеийн борлуулалт</field> + <field name="amount">10.0</field> + <field name="price_include" eval="1"/> + <field name="sequence">20</field> + <field name="amount_type">percent</field> + <field name="type_tax_use">sale</field> + <field name="description">10%</field> + <field name="tax_group_id" ref="account_tax_group1"/> + <field name="chart_template_id" ref="mn_chart_1"/> + <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'tag_ids': [ref('vat_report_tag8')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('account_template_3401_0201'), + 'tag_ids': [ref('vat_report_tag8')], + }), + ]"/> + <field name="refund_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'tag_ids': [ref('vat_report_tag8')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('account_template_3401_0201'), + 'tag_ids': [ref('vat_report_tag8')], + }), + ]"/> + </record> + + <record id="account_tax_sale_vat7" model="account.tax.template"> + <field name="name">НӨАТ өрийн төлбөрт өгсөн бараа</field> + <field name="amount">10.0</field> + <field name="price_include" eval="1"/> + <field name="sequence">20</field> + <field name="amount_type">percent</field> + <field name="type_tax_use">sale</field> + <field name="description">10%</field> + <field name="tax_group_id" ref="account_tax_group1"/> + <field name="chart_template_id" ref="mn_chart_1"/> + <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'tag_ids': [ref('vat_report_tag9')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('account_template_3401_0201'), + 'tag_ids': [ref('vat_report_tag9')], + }), + ]"/> + <field name="refund_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'tag_ids': [ref('vat_report_tag9')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('account_template_3401_0201'), + 'tag_ids': [ref('vat_report_tag9')], + }), + ]"/> + </record> + + <record id="account_tax_sale_vat8" model="account.tax.template"> + <field name="name">НӨАТ нотариат үйлчилгээ</field> + <field name="amount">10.0</field> + <field name="price_include" eval="1"/> + <field name="sequence">20</field> + <field name="amount_type">percent</field> + <field name="type_tax_use">sale</field> + <field name="description">10%</field> + <field name="tax_group_id" ref="account_tax_group2"/> + <field name="chart_template_id" ref="mn_chart_1"/> + <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'tag_ids': [ref('vat_report_tag11')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('account_template_3401_0201'), + 'tag_ids': [ref('vat_report_tag11')], + }), + ]"/> + <field name="refund_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'tag_ids': [ref('vat_report_tag11')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('account_template_3401_0201'), + 'tag_ids': [ref('vat_report_tag11')], + }), + ]"/> + </record> + + <record id="account_tax_sale_vat9" model="account.tax.template"> + <field name="name">НӨАТ өрийн төлбөрт өгсөн үйлчилгээ</field> + <field name="amount">10.0</field> + <field name="price_include" eval="1"/> + <field name="sequence">20</field> + <field name="amount_type">percent</field> + <field name="type_tax_use">sale</field> + <field name="description">10%</field> + <field name="tax_group_id" ref="account_tax_group2"/> + <field name="chart_template_id" ref="mn_chart_1"/> + <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'tag_ids': [ref('vat_report_tag12')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('account_template_3401_0201'), + 'tag_ids': [ref('vat_report_tag12')], + }), + ]"/> + <field name="refund_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'tag_ids': [ref('vat_report_tag12')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('account_template_3401_0201'), + 'tag_ids': [ref('vat_report_tag12')], + }), + ]"/> + </record> + + <record id="account_tax_sale_vat10" model="account.tax.template"> + <field name="name">НӨАТ цахилгаан, дулаан, ус, шуудан, холбоо</field> + <field name="amount">10.0</field> + <field name="price_include" eval="1"/> + <field name="sequence">20</field> + <field name="amount_type">percent</field> + <field name="type_tax_use">sale</field> + <field name="description">10%</field> + <field name="tax_group_id" ref="account_tax_group2"/> + <field name="chart_template_id" ref="mn_chart_1"/> + <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'tag_ids': [ref('vat_report_tag13')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('account_template_3401_0201'), + 'tag_ids': [ref('vat_report_tag13')], + }), + ]"/> + <field name="refund_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'tag_ids': [ref('vat_report_tag13')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('account_template_3401_0201'), + 'tag_ids': [ref('vat_report_tag13')], + }), + ]"/> + </record> + + <record id="account_tax_sale_vat11" model="account.tax.template"> + <field name="name">НӨАТ бараа түрээслүүлэх, эзэмшүүлэх</field> + <field name="amount">10.0</field> + <field name="price_include" eval="1"/> + <field name="sequence">20</field> + <field name="amount_type">percent</field> + <field name="type_tax_use">sale</field> + <field name="description">10%</field> + <field name="tax_group_id" ref="account_tax_group2"/> + <field name="chart_template_id" ref="mn_chart_1"/> + <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'tag_ids': [ref('vat_report_tag14')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('account_template_3401_0201'), + 'tag_ids': [ref('vat_report_tag14')], + }), + ]"/> + <field name="refund_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'tag_ids': [ref('vat_report_tag14')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('account_template_3401_0201'), + 'tag_ids': [ref('vat_report_tag14')], + }), + ]"/> + </record> + + <record id="account_tax_sale_vat12" model="account.tax.template"> + <field name="name">НӨАТ байр түрээслүүлэх, эзэмшүүлэх</field> + <field name="amount">10.0</field> + <field name="price_include" eval="1"/> + <field name="sequence">20</field> + <field name="amount_type">percent</field> + <field name="type_tax_use">sale</field> + <field name="description">10%</field> + <field name="tax_group_id" ref="account_tax_group2"/> + <field name="chart_template_id" ref="mn_chart_1"/> + <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'tag_ids': [ref('vat_report_tag15')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('account_template_3401_0201'), + 'tag_ids': [ref('vat_report_tag15')], + }), + ]"/> + <field name="refund_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'tag_ids': [ref('vat_report_tag15')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('account_template_3401_0201'), + 'tag_ids': [ref('vat_report_tag15')], + }), + ]"/> + </record> + + <record id="account_tax_sale_vat13" model="account.tax.template"> + <field name="name">НӨАТ хөрөнгө түрээслүүлэх, эзэмшүүлэх</field> + <field name="amount">10.0</field> + <field name="price_include" eval="1"/> + <field name="sequence">20</field> + <field name="amount_type">percent</field> + <field name="type_tax_use">sale</field> + <field name="description">10%</field> + <field name="tax_group_id" ref="account_tax_group2"/> + <field name="chart_template_id" ref="mn_chart_1"/> + <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'tag_ids': [ref('vat_report_tag16')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('account_template_3401_0201'), + 'tag_ids': [ref('vat_report_tag16')], + }), + ]"/> + <field name="refund_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'tag_ids': [ref('vat_report_tag16')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('account_template_3401_0201'), + 'tag_ids': [ref('vat_report_tag16')], + }), + ]"/> + </record> + + <record id="account_tax_sale_vat14" model="account.tax.template"> + <field name="name">НӨАТ бүтээл, загвар, зохиогчийн эрх</field> + <field name="amount">10.0</field> + <field name="price_include" eval="1"/> + <field name="sequence">20</field> + <field name="amount_type">percent</field> + <field name="type_tax_use">sale</field> + <field name="description">10%</field> + <field name="tax_group_id" ref="account_tax_group2"/> + <field name="chart_template_id" ref="mn_chart_1"/> + <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'tag_ids': [ref('vat_report_tag17')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('account_template_3401_0201'), + 'tag_ids': [ref('vat_report_tag17')], + }), + ]"/> + <field name="refund_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'tag_ids': [ref('vat_report_tag17')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('account_template_3401_0201'), + 'tag_ids': [ref('vat_report_tag17')], + }), + ]"/> + </record> + + <record id="account_tax_sale_vat15" model="account.tax.template"> + <field name="name">НӨАТ хонжворт сугалаа, таавар, бооцоо</field> + <field name="amount">10.0</field> + <field name="price_include" eval="1"/> + <field name="sequence">20</field> + <field name="amount_type">percent</field> + <field name="type_tax_use">sale</field> + <field name="description">10%</field> + <field name="tax_group_id" ref="account_tax_group2"/> + <field name="chart_template_id" ref="mn_chart_1"/> + <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'tag_ids': [ref('vat_report_tag18')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('account_template_3401_0201'), + 'tag_ids': [ref('vat_report_tag18')], + }), + ]"/> + <field name="refund_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'tag_ids': [ref('vat_report_tag18')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('account_template_3401_0201'), + 'tag_ids': [ref('vat_report_tag18')], + }), + ]"/> + </record> + + <record id="account_tax_sale_vat16" model="account.tax.template"> + <field name="name">НӨАТ зуучлалын үйлчилгээ</field> + <field name="amount">10.0</field> + <field name="price_include" eval="1"/> + <field name="sequence">20</field> + <field name="amount_type">percent</field> + <field name="type_tax_use">sale</field> + <field name="description">10%</field> + <field name="tax_group_id" ref="account_tax_group2"/> + <field name="chart_template_id" ref="mn_chart_1"/> + <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'tag_ids': [ref('vat_report_tag19')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('account_template_3401_0201'), + 'tag_ids': [ref('vat_report_tag19')], + }), + ]"/> + <field name="refund_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'tag_ids': [ref('vat_report_tag19')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('account_template_3401_0201'), + 'tag_ids': [ref('vat_report_tag19')], + }), + ]"/> + </record> + + <record id="account_tax_sale_vat17" model="account.tax.template"> + <field name="name">НӨАТ авсан хүү торгууль, алданги</field> + <field name="amount">10.0</field> + <field name="price_include" eval="1"/> + <field name="sequence">20</field> + <field name="amount_type">percent</field> + <field name="type_tax_use">sale</field> + <field name="description">10%</field> + <field name="tax_group_id" ref="account_tax_group2"/> + <field name="chart_template_id" ref="mn_chart_1"/> + <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'tag_ids': [ref('vat_report_tag20')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('account_template_3401_0201'), + 'tag_ids': [ref('vat_report_tag20')], + }), + ]"/> + <field name="refund_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'tag_ids': [ref('vat_report_tag20')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('account_template_3401_0201'), + 'tag_ids': [ref('vat_report_tag20')], + }), + ]"/> + </record> + + <record id="account_tax_sale_vat18" model="account.tax.template"> + <field name="name">НӨАТ хөрөнгийн үнэлгээний үйлчилгээ</field> + <field name="amount">10.0</field> + <field name="price_include" eval="1"/> + <field name="sequence">20</field> + <field name="amount_type">percent</field> + <field name="type_tax_use">sale</field> + <field name="description">10%</field> + <field name="tax_group_id" ref="account_tax_group2"/> + <field name="chart_template_id" ref="mn_chart_1"/> + <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'tag_ids': [ref('vat_report_tag21')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('account_template_3401_0201'), + 'tag_ids': [ref('vat_report_tag21')], + }), + ]"/> + <field name="refund_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'tag_ids': [ref('vat_report_tag21')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('account_template_3401_0201'), + 'tag_ids': [ref('vat_report_tag21')], + }), + ]"/> + </record> + + <record id="account_tax_sale_vat19" model="account.tax.template"> + <field name="name">НӨАТ төсвийн санхүүжилт, татаас</field> + <field name="amount">10.0</field> + <field name="price_include" eval="1"/> + <field name="sequence">20</field> + <field name="amount_type">percent</field> + <field name="type_tax_use">sale</field> + <field name="description">10%</field> + <field name="tax_group_id" ref="account_tax_group2"/> + <field name="chart_template_id" ref="mn_chart_1"/> + <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'tag_ids': [ref('vat_report_tag22')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('account_template_3401_0201'), + 'tag_ids': [ref('vat_report_tag22')], + }), + ]"/> + <field name="refund_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'tag_ids': [ref('vat_report_tag22')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('account_template_3401_0201'), + 'tag_ids': [ref('vat_report_tag22')], + }), + ]"/> + </record> + + <record id="account_tax_sale_vat20" model="account.tax.template"> + <field name="name">НӨАТ хууль зүйн үйлчилгээ</field> + <field name="amount">10.0</field> + <field name="price_include" eval="1"/> + <field name="sequence">20</field> + <field name="amount_type">percent</field> + <field name="type_tax_use">sale</field> + <field name="description">10%</field> + <field name="tax_group_id" ref="account_tax_group2"/> + <field name="chart_template_id" ref="mn_chart_1"/> + <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'tag_ids': [ref('vat_report_tag23')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('account_template_3401_0201'), + 'tag_ids': [ref('vat_report_tag23')], + }), + ]"/> + <field name="refund_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'tag_ids': [ref('vat_report_tag23')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('account_template_3401_0201'), + 'tag_ids': [ref('vat_report_tag23')], + }), + ]"/> + </record> + + <record id="account_tax_sale_vat21" model="account.tax.template"> + <field name="name">НӨАТ үсчин, гоо сайхан, угаалга, цэвэрлэгээ</field> + <field name="amount">10.0</field> + <field name="price_include" eval="1"/> + <field name="sequence">20</field> + <field name="amount_type">percent</field> + <field name="type_tax_use">sale</field> + <field name="description">10%</field> + <field name="tax_group_id" ref="account_tax_group2"/> + <field name="chart_template_id" ref="mn_chart_1"/> + <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'tag_ids': [ref('vat_report_tag24')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('account_template_3401_0201'), + 'tag_ids': [ref('vat_report_tag24')], + }), + ]"/> + <field name="refund_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'tag_ids': [ref('vat_report_tag24')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('account_template_3401_0201'), + 'tag_ids': [ref('vat_report_tag24')], + }), + ]"/> + </record> + + <record id="account_tax_sale_vat22" model="account.tax.template"> + <field name="name">НӨАТ бусад үйлчилгээ</field> + <field name="amount">10.0</field> + <field name="price_include" eval="1"/> + <field name="sequence">20</field> + <field name="amount_type">percent</field> + <field name="type_tax_use">sale</field> + <field name="description">10%</field> + <field name="tax_group_id" ref="account_tax_group2"/> + <field name="chart_template_id" ref="mn_chart_1"/> + <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'tag_ids': [ref('vat_report_tag25')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('account_template_3401_0201'), + 'tag_ids': [ref('vat_report_tag25')], + }), + ]"/> + <field name="refund_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'tag_ids': [ref('vat_report_tag25')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('account_template_3401_0201'), + 'tag_ids': [ref('vat_report_tag25')], + }), + ]"/> + </record> + + <record id="account_tax_sale_vat23" model="account.tax.template"> + <field name="name">НӨАТ 0% экспорт бараа</field> + <field name="amount">0.0</field> + <field name="price_include" eval="1"/> + <field name="sequence">30</field> + <field name="amount_type">percent</field> + <field name="type_tax_use">sale</field> + <field name="description">0%</field> + <field name="tax_group_id" ref="account_tax_group3"/> + <field name="chart_template_id" ref="mn_chart_1"/> + <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'tag_ids': [ref('vat_report_tag28')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('account_template_3401_0201'), + }), + ]"/> + <field name="refund_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'tag_ids': [ref('vat_report_tag28')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('account_template_3401_0201'), + }), + ]"/> + </record> + + <record id="account_tax_sale_vat24" model="account.tax.template"> + <field name="name">НӨАТ 0% экспорт үйлчилгээ</field> + <field name="amount">0.0</field> + <field name="price_include" eval="1"/> + <field name="sequence">30</field> + <field name="amount_type">percent</field> + <field name="type_tax_use">sale</field> + <field name="description">0%</field> + <field name="tax_group_id" ref="account_tax_group3"/> + <field name="chart_template_id" ref="mn_chart_1"/> + <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'tag_ids': [ref('vat_report_tag29')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('account_template_3401_0201'), + }), + ]"/> + <field name="refund_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'tag_ids': [ref('vat_report_tag29')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('account_template_3401_0201'), + }), + ]"/> + </record> + + <record id="account_tax_purchase_vat3" model="account.tax.template"> + <field name="name">НӨАТ импортоор авсан бараа, үйлчилгээ</field> + <field name="amount">10.0</field> + <field name="price_include" eval="1"/> + <field name="sequence">40</field> + <field name="amount_type">percent</field> + <field name="type_tax_use">purchase</field> + <field name="description">10%</field> + <field name="tax_group_id" ref="account_tax_group4"/> + <field name="chart_template_id" ref="mn_chart_1"/> + <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'tag_ids': [ref('vat_report_tag33'),ref('vat_report_tag35')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('account_template_1204_0301'), + 'tag_ids': [ref('vat_report_tag35')], + }), + ]"/> + <field name="refund_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'tag_ids': [ref('vat_report_tag33'),ref('vat_report_tag35')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('account_template_1204_0301'), + 'tag_ids': [ref('vat_report_tag35')], + }), + ]"/> + </record> + + <record id="account_tax_purchase_vat4" model="account.tax.template"> + <field name="name">НӨАТ төлөгчөөр бүртгүүлэх үед худалдан авсан</field> + <field name="amount">10.0</field> + <field name="price_include" eval="1"/> + <field name="sequence">40</field> + <field name="amount_type">percent</field> + <field name="type_tax_use">purchase</field> + <field name="description">10%</field> + <field name="tax_group_id" ref="account_tax_group4"/> + <field name="chart_template_id" ref="mn_chart_1"/> + <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'tag_ids': [ref('vat_report_tag33'),ref('vat_report_tag37')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('account_template_1204_0301'), + 'tag_ids': [ref('vat_report_tag37')], + }), + ]"/> + <field name="refund_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'tag_ids': [ref('vat_report_tag33'),ref('vat_report_tag37')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('account_template_1204_0301'), + 'tag_ids': [ref('vat_report_tag37')], + }), + ]"/> + </record> + + <record id="account_tax_purchase_vat5" model="account.tax.template"> + <field name="name">НӨАТ мал аж ахуй эрхлэгчээс худалдан авсан</field> + <field name="amount">10.0</field> + <field name="price_include" eval="1"/> + <field name="sequence">40</field> + <field name="amount_type">percent</field> + <field name="type_tax_use">purchase</field> + <field name="description">10%</field> + <field name="tax_group_id" ref="account_tax_group4"/> + <field name="chart_template_id" ref="mn_chart_1"/> + <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'tag_ids': [ref('vat_report_tag33'),ref('vat_report_tag38')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('account_template_1204_0301'), + 'tag_ids': [ref('vat_report_tag38')], + }), + ]"/> + <field name="refund_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'tag_ids': [ref('vat_report_tag33'),ref('vat_report_tag38')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('account_template_1204_0301'), + 'tag_ids': [ref('vat_report_tag38')], + }), + ]"/> + </record> + + <record id="account_tax_purchase_vat6" model="account.tax.template"> + <field name="name">НӨАТ автомашин, эд анги худалдан авалт</field> + <field name="amount">10.0</field> + <field name="price_include" eval="1"/> + <field name="sequence">40</field> + <field name="amount_type">percent</field> + <field name="type_tax_use">purchase</field> + <field name="description">10%</field> + <field name="tax_group_id" ref="account_tax_group4"/> + <field name="chart_template_id" ref="mn_chart_1"/> + <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'tag_ids': [ref('vat_report_tag33')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('account_template_1204_0301'), + 'tag_ids': [ref('vat_report_tag41')], + }), + ]"/> + <field name="refund_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'tag_ids': [ref('vat_report_tag33')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('account_template_1204_0301'), + 'tag_ids': [ref('vat_report_tag41')], + }), + ]"/> + </record> + + <record id="account_tax_purchase_vat7" model="account.tax.template"> + <field name="name">НӨАТ ажлын хэрэгцээнд авсан бараа</field> + <field name="amount">10.0</field> + <field name="price_include" eval="1"/> + <field name="sequence">40</field> + <field name="amount_type">percent</field> + <field name="type_tax_use">purchase</field> + <field name="description">10%</field> + <field name="tax_group_id" ref="account_tax_group4"/> + <field name="chart_template_id" ref="mn_chart_1"/> + <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'tag_ids': [ref('vat_report_tag33')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('account_template_1204_0301'), + 'tag_ids': [ref('vat_report_tag42')], + }), + ]"/> + <field name="refund_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'tag_ids': [ref('vat_report_tag33')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('account_template_1204_0301'), + 'tag_ids': [ref('vat_report_tag42')], + }), + ]"/> + </record> + + <record id="account_tax_purchase_vat8" model="account.tax.template"> + <field name="name">НӨАТ импортоор авсан үндсэн хөрөнгө</field> + <field name="amount">10.0</field> + <field name="price_include" eval="1"/> + <field name="sequence">40</field> + <field name="amount_type">percent</field> + <field name="type_tax_use">purchase</field> + <field name="description">10%</field> + <field name="tax_group_id" ref="account_tax_group4"/> + <field name="chart_template_id" ref="mn_chart_1"/> + <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'tag_ids': [ref('vat_report_tag33')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('account_template_1204_0301'), + 'tag_ids': [ref('vat_report_tag43')], + }), + ]"/> + <field name="refund_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'tag_ids': [ref('vat_report_tag33')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('account_template_1204_0301'), + 'tag_ids': [ref('vat_report_tag43')], + }), + ]"/> + </record> + + <record id="account_tax_purchase_vat9" model="account.tax.template"> + <field name="name">НӨАТ чөлөөлөгдөх үйлдвэрлэлд зориулсан бараа, үйлчилгээ</field> + <field name="amount">10.0</field> + <field name="price_include" eval="1"/> + <field name="sequence">40</field> + <field name="amount_type">percent</field> + <field name="type_tax_use">purchase</field> + <field name="description">10%</field> + <field name="tax_group_id" ref="account_tax_group4"/> + <field name="chart_template_id" ref="mn_chart_1"/> + <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'tag_ids': [ref('vat_report_tag33')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('account_template_1204_0301'), + 'tag_ids': [ref('vat_report_tag44')], + }), + ]"/> + <field name="refund_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'tag_ids': [ref('vat_report_tag33')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('account_template_1204_0301'), + 'tag_ids': [ref('vat_report_tag44')], + }), + ]"/> + </record> + + <record id="account_tax_purchase_vat10" model="account.tax.template"> + <field name="name">НӨАТ хайгуулд зориулсан импортын бараа, үйлчилгээ</field> + <field name="amount">10.0</field> + <field name="price_include" eval="1"/> + <field name="sequence">40</field> + <field name="amount_type">percent</field> + <field name="type_tax_use">purchase</field> + <field name="description">10%</field> + <field name="tax_group_id" ref="account_tax_group4"/> + <field name="chart_template_id" ref="mn_chart_1"/> + <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'tag_ids': [ref('vat_report_tag33')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('account_template_1204_0301'), + 'tag_ids': [ref('vat_report_tag45')], + }), + ]"/> + <field name="refund_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'tag_ids': [ref('vat_report_tag33')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('account_template_1204_0301'), + 'tag_ids': [ref('vat_report_tag45')], + }), + ]"/> + </record> + + <record id="account_tax_purchase_vat11" model="account.tax.template"> + <field name="name">НӨАТ импортын бусад хамааралгүй бараа, үйлчилгээ</field> + <field name="amount">10.0</field> + <field name="price_include" eval="1"/> + <field name="sequence">40</field> + <field name="amount_type">percent</field> + <field name="type_tax_use">purchase</field> + <field name="description">10%</field> + <field name="tax_group_id" ref="account_tax_group4"/> + <field name="chart_template_id" ref="mn_chart_1"/> + <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'tag_ids': [ref('vat_report_tag33')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('account_template_1204_0301'), + 'tag_ids': [ref('vat_report_tag46')], + }), + ]"/> + <field name="refund_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'tag_ids': [ref('vat_report_tag33')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('account_template_1204_0301'), + 'tag_ids': [ref('vat_report_tag46')], + }), + ]"/> + </record> + + <record id="account_tax_sale_vat25" model="account.tax.template"> + <field name="name">НӨАТ дотоодод зарсан санхүүгийн түрээс</field> + <field name="amount">10.0</field> + <field name="price_include" eval="1"/> + <field name="sequence">50</field> + <field name="amount_type">percent</field> + <field name="type_tax_use">sale</field> + <field name="description">10%</field> + <field name="tax_group_id" ref="account_tax_group5"/> + <field name="chart_template_id" ref="mn_chart_1"/> + <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'tag_ids': [ref('vat_report_tag48')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('account_template_3401_0201'), + 'tag_ids': [ref('vat_report_tag48')], + }), + ]"/> + <field name="refund_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'tag_ids': [ref('vat_report_tag48')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('account_template_3401_0201'), + 'tag_ids': [ref('vat_report_tag48')], + }), + ]"/> + </record> + + <record id="account_tax_sale_vat26" model="account.tax.template"> + <field name="name">НӨАТ Факторинг, форфайтинг хэлцлийн үйлчилгээ</field> + <field name="amount">10.0</field> + <field name="price_include" eval="1"/> + <field name="sequence">50</field> + <field name="amount_type">percent</field> + <field name="type_tax_use">sale</field> + <field name="description">10%</field> + <field name="tax_group_id" ref="account_tax_group5"/> + <field name="chart_template_id" ref="mn_chart_1"/> + <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'tag_ids': [ref('vat_report_tag49')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('account_template_3401_0201'), + 'tag_ids': [ref('vat_report_tag49')], + }), + ]"/> + <field name="refund_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'tag_ids': [ref('vat_report_tag49')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('account_template_3401_0201'), + 'tag_ids': [ref('vat_report_tag49')], + }), + ]"/> + </record> + + <record id="account_tax_purchase_vat12" model="account.tax.template"> + <field name="name">НӨАТ Санхүүгийн түрээсийн худалдан авалт</field> + <field name="amount">10.0</field> + <field name="price_include" eval="1"/> + <field name="sequence">50</field> + <field name="amount_type">percent</field> + <field name="type_tax_use">purchase</field> + <field name="description">10%</field> + <field name="tax_group_id" ref="account_tax_group5"/> + <field name="chart_template_id" ref="mn_chart_1"/> + <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'tag_ids': [ref('vat_report_tag51')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('account_template_1204_0301'), + 'tag_ids': [ref('vat_report_tag51')], + }), + ]"/> + <field name="refund_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'tag_ids': [ref('vat_report_tag51')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('account_template_1204_0301'), + 'tag_ids': [ref('vat_report_tag51')], + }), + ]"/> + </record> + + <record id="account_tax_purchase_vat13" model="account.tax.template"> + <field name="name">НӨАТ Факторинг, форфайтинг худалдан авалт</field> + <field name="amount">10.0</field> + <field name="price_include" eval="1"/> + <field name="sequence">50</field> + <field name="amount_type">percent</field> + <field name="type_tax_use">purchase</field> + <field name="description">10%</field> + <field name="tax_group_id" ref="account_tax_group5"/> + <field name="chart_template_id" ref="mn_chart_1"/> + <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'tag_ids': [ref('vat_report_tag52')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('account_template_1204_0301'), + 'tag_ids': [ref('vat_report_tag52')], + }), + ]"/> + <field name="refund_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'tag_ids': [ref('vat_report_tag52')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('account_template_1204_0301'), + 'tag_ids': [ref('vat_report_tag52')], + }), + ]"/> + </record> + +</odoo> diff --git a/addons/l10n_mn/static/description/icon.png b/addons/l10n_mn/static/description/icon.png Binary files differnew file mode 100644 index 00000000000..dae4b68b81e --- /dev/null +++ b/addons/l10n_mn/static/description/icon.png diff --git a/addons/l10n_mx/data/account_tag_data.xml b/addons/l10n_mx/data/account_tag_data.xml index 244c9b65ff3..1e8b23dacda 100644 --- a/addons/l10n_mx/data/account_tag_data.xml +++ b/addons/l10n_mx/data/account_tag_data.xml @@ -479,25 +479,25 @@ <field name='name'>119.01 IVA pendiente de pago</field> <field name='color'>4</field> <field name='applicability'>accounts</field> - <field name='nature'>A</field> + <field name='nature'>D</field> </record> <record id='account_tag_119_02' model='account.account.tag'> <field name='name'>119.02 IVA de importación pendiente de pago</field> <field name='color'>4</field> <field name='applicability'>accounts</field> - <field name='nature'>A</field> + <field name='nature'>D</field> </record> <record id='account_tag_119_03' model='account.account.tag'> <field name='name'>119.03 IEPS pendiente de pago</field> <field name='color'>4</field> <field name='applicability'>accounts</field> - <field name='nature'>A</field> + <field name='nature'>D</field> </record> <record id='account_tag_119_04' model='account.account.tag'> <field name='name'>119.04 IEPS pendiente de pago en importación</field> <field name='color'>4</field> <field name='applicability'>accounts</field> - <field name='nature'>A</field> + <field name='nature'>D</field> </record> <record id='account_tag_120_01' model='account.account.tag'> <field name='name'>120.01 Anticipo a proveedores nacional</field> @@ -917,7 +917,7 @@ <field name='name'>180.01 Crédito mercantil</field> <field name='color'>4</field> <field name='applicability'>accounts</field> - <field name='nature'>A</field> + <field name='nature'>D</field> </record> <record id='account_tag_181_01' model='account.account.tag'> <field name='name'>181.01 Gastos de instalación</field> @@ -2423,7 +2423,7 @@ <field name='name'>503 Devoluciones, descuentos o bonificaciones sobre compras</field> <field name='color'>1</field> <field name='applicability'>accounts</field> - <field name='nature'>D</field> + <field name='nature'>A</field> </record> <record id='account_tag_503_01' model='account.account.tag'> <field name='name'>503.01 Devoluciones, descuentos o bonificaciones sobre compras</field> @@ -2585,13 +2585,13 @@ <field name='name'>505.01 Costo por venta de activo fijo</field> <field name='color'>4</field> <field name='applicability'>accounts</field> - <field name='nature'>A</field> + <field name='nature'>D</field> </record> <record id='account_tag_505_02' model='account.account.tag'> <field name='name'>505.02 Costo por baja de activo fijo</field> <field name='color'>4</field> <field name='applicability'>accounts</field> - <field name='nature'>A</field> + <field name='nature'>D</field> </record> <record id='account_tag_601_01' model='account.account.tag'> <field name='name'>601.01 Sueldos y salarios</field> @@ -4781,7 +4781,7 @@ <field name='name'>607.01 Participación de los trabajadores en las utilidades</field> <field name='color'>4</field> <field name='applicability'>accounts</field> - <field name='nature'>A</field> + <field name='nature'>D</field> </record> <record id='account_tag_608_01' model='account.account.tag'> <field name='name'>608.01 Participación en resultados de subsidiarias</field> @@ -4799,7 +4799,7 @@ <field name='name'>610.01 Participación de los trabajadores en las utilidades diferida</field> <field name='color'>4</field> <field name='applicability'>accounts</field> - <field name='nature'>A</field> + <field name='nature'>D</field> </record> <record id='account_tag_611_01' model='account.account.tag'> <field name='name'>611.01 Impuesto Sobre la renta</field> @@ -4817,7 +4817,7 @@ <field name='name'>612.01 Gastos no deducibles para CUFIN</field> <field name='color'>4</field> <field name='applicability'>accounts</field> - <field name='nature'>A</field> + <field name='nature'>D</field> </record> <record id='account_tag_613_01' model='account.account.tag'> <field name='name'>613.01 Depreciación de edificios</field> diff --git a/addons/l10n_za/__init__.py b/addons/l10n_za/__init__.py new file mode 100644 index 00000000000..e69de29bb2d --- /dev/null +++ b/addons/l10n_za/__init__.py diff --git a/addons/l10n_za/__manifest__.py b/addons/l10n_za/__manifest__.py new file mode 100644 index 00000000000..692ffecfc47 --- /dev/null +++ b/addons/l10n_za/__manifest__.py @@ -0,0 +1,26 @@ +# -*- encoding: utf-8 -*- + +# Copyright (C) 2017 Paradigm Digital (<http://www.paradigmdigital.co.za>). + +{ + 'name': 'South Africa - Accounting', + 'version': '1.0', + 'category': 'Localization', + 'description': """ +This is the latest basic South African localisation necessary to run Odoo in ZA: +================================================================================ + - a generic chart of accounts + - SARS VAT Ready Structure""", + 'author': 'Paradigm Digital', + 'website': 'https://www.paradigmdigital.co.za', + 'depends': ['account', 'base_vat'], + 'data': [ + 'data/account_tax_report_data.xml', + 'data/account.tax.group.csv', + 'data/account_chart_template_data.xml', + 'data/account.account.template.csv', + 'data/account_tax_template_data.xml', + 'data/account_chart_template_post_data.xml', + 'data/account_chart_template_configure_data.xml', + ], +} diff --git a/addons/l10n_za/data/account.account.tag.csv b/addons/l10n_za/data/account.account.tag.csv new file mode 100644 index 00000000000..9b8832a53eb --- /dev/null +++ b/addons/l10n_za/data/account.account.tag.csv @@ -0,0 +1,17 @@ +id,name,applicability +tag_ST1,ST1,taxes +tag_ST1A,ST1A,taxes +tag_ST2,ST2,taxes +tag_ST2A,ST2A,taxes +tag_ST3,ST3,taxes +tag_ST5,ST5,taxes +tag_ST7,ST7,taxes +tag_ST10,ST10,taxes +tag_ST12,ST12,taxes +tag_PT14,PT14,taxes +tag_PT14A,PT14A,taxes +tag_PT15,PT15,taxes +tag_PT15A,PT15A,taxes +tag_PT16,PT16,taxes +tag_PT17,PT17,taxes +tag_PT18,PT18,taxes
\ No newline at end of file diff --git a/addons/l10n_za/data/account.account.template.csv b/addons/l10n_za/data/account.account.template.csv new file mode 100644 index 00000000000..404eb24a86e --- /dev/null +++ b/addons/l10n_za/data/account.account.template.csv @@ -0,0 +1,111 @@ +id,code,name,user_type_id/id,reconcile,chart_template_id/id +100020,100020,Stock Valuation Account,account.data_account_type_current_assets,True,default_chart_template +100030,100030,Stock Work In Progress,account.data_account_type_current_assets,False,default_chart_template +100040,100040,Stock Finished Goods,account.data_account_type_current_assets,False,default_chart_template +100050,100050,Stock Delivered Control Account,account.data_account_type_current_assets,True,default_chart_template +100060,100060,Purchase Tax Control Account,account.data_account_type_current_assets,False,default_chart_template +100070,100070,Other Current Assets,account.data_account_type_current_assets,False,default_chart_template +110010,110010,Debtors Control Account,account.data_account_type_receivable,True,default_chart_template +110020,110020,Sundry Debtors,account.data_account_type_receivable,True,default_chart_template +124010,124010,Credit Card Merchant Account,account.data_account_type_liquidity,False,default_chart_template +126010,126010,Cash In Hand,account.data_account_type_liquidity,False,default_chart_template +130010,130010,Prepayments,account.data_account_type_prepayments,False,default_chart_template +140010,140010,Software,account.data_account_type_fixed_assets,False,default_chart_template +140020,140020,Patents & Trademarks,account.data_account_type_fixed_assets,False,default_chart_template +140030,140030,Fixtures & Fittings,account.data_account_type_fixed_assets,False,default_chart_template +140040,140040,Land & Buildings,account.data_account_type_fixed_assets,False,default_chart_template +140050,140050,Motor Vehicles,account.data_account_type_fixed_assets,False,default_chart_template +140060,140060,Office Equipment (incl computer equipment),account.data_account_type_fixed_assets,False,default_chart_template +140070,140070,Plant & Machinery,account.data_account_type_fixed_assets,False,default_chart_template +150010,150010,Non-current assets,account.data_account_type_non_current_assets,False,default_chart_template +200010,200010,Stock Received Control Account,account.data_account_type_current_liabilities,True,default_chart_template +200020,200020,Sundry Creditors,account.data_account_type_current_liabilities,False,default_chart_template +200030,200030,Other Creditors,account.data_account_type_current_liabilities,False,default_chart_template +200040,200040,Accruals,account.data_account_type_current_liabilities,False,default_chart_template +200050,200050,Bad debt provision,account.data_account_type_current_liabilities,False,default_chart_template +200060,200060,Sales Tax Control Account,account.data_account_type_current_liabilities,False,default_chart_template +200070,200070,Manual Adjustments & VAT,account.data_account_type_current_liabilities,False,default_chart_template +200080,200080,Loans,account.data_account_type_current_liabilities,False,default_chart_template +200090,200090,Hire Purchase,account.data_account_type_current_liabilities,False,default_chart_template +200100,200100,Mortgages,account.data_account_type_current_liabilities,False,default_chart_template +210010,210010,Company Credit Card,account.data_account_type_credit_card,False,default_chart_template +220010,220010,Creditors Control Account,account.data_account_type_payable,True,default_chart_template +220020,220020,SARS - VAT Account,account.data_account_type_payable,True,default_chart_template +220030,220030,P.A.Y.E. & UIF,account.data_account_type_payable,True,default_chart_template +220040,220040,Net Wages,account.data_account_type_payable,True,default_chart_template +220050,220050,Pension Fund,account.data_account_type_payable,True,default_chart_template +220060,220060,Corporation Tax,account.data_account_type_payable,True,default_chart_template +300010,300010,Called up share capital,account.data_account_type_equity,False,default_chart_template +300020,300020,Share premium account,account.data_account_type_equity,False,default_chart_template +300030,300030,Revaluation reserve,account.data_account_type_equity,False,default_chart_template +300040,300040,Other reserves,account.data_account_type_equity,False,default_chart_template +300050,300050,Capital,account.data_account_type_equity,False,default_chart_template +300060,300060,Dividends,account.data_account_type_equity,False,default_chart_template +300070,300070,Drawings,account.data_account_type_equity,False,default_chart_template +400010,400010,Undistributed Profits/Losses,account.data_unaffected_earnings,False,default_chart_template +500010,500010,Sales category 1,account.data_account_type_revenue,False,default_chart_template +500020,500020,Sales category 2,account.data_account_type_revenue,False,default_chart_template +500030,500030,Sales category 3,account.data_account_type_revenue,False,default_chart_template +500040,500040,Sales category 4,account.data_account_type_revenue,False,default_chart_template +500050,500050,Bank Interest received,account.data_account_type_revenue,False,default_chart_template +500060,500060,Investment Interest received,account.data_account_type_revenue,False,default_chart_template +500070,500070,Profits/Losses on disposals of assets,account.data_account_type_revenue,False,default_chart_template +500080,500080,Rental Income,account.data_account_type_revenue,False,default_chart_template +510010,510010,Other Income,account.data_account_type_other_income,False,default_chart_template +600010,600010,Cost of sales 1,account.data_account_type_direct_costs,False,default_chart_template +600020,600020,Cost of sales 2,account.data_account_type_direct_costs,False,default_chart_template +600030,600030,Cost of sales 3,account.data_account_type_direct_costs,False,default_chart_template +600040,600040,Cost of sales 4,account.data_account_type_direct_costs,False,default_chart_template +610010,610010,Marketing,account.data_account_type_expenses,False,default_chart_template +610020,610020,Exhibitions and events,account.data_account_type_expenses,False,default_chart_template +610030,610030,PR,account.data_account_type_expenses,False,default_chart_template +610040,610040,Distribution vehicles,account.data_account_type_expenses,False,default_chart_template +610050,610050,Distribution salaries and wages,account.data_account_type_expenses,False,default_chart_template +610060,610060,Shipping,account.data_account_type_expenses,False,default_chart_template +610070,610070,Directors pension,account.data_account_type_expenses,False,default_chart_template +610080,610080,Directors remuneration,account.data_account_type_expenses,False,default_chart_template +610090,610090,Gross Salaries,account.data_account_type_expenses,False,default_chart_template +610100,610100,Employers SDL & UIF,account.data_account_type_expenses,False,default_chart_template +610110,610110,Subcontractors payments,account.data_account_type_expenses,False,default_chart_template +610120,610120,Rent and rates,account.data_account_type_expenses,False,default_chart_template +610130,610130,Light / heat and power,account.data_account_type_expenses,False,default_chart_template +610140,610140,Repairs and maintenance,account.data_account_type_expenses,False,default_chart_template +610150,610150,Car hire,account.data_account_type_expenses,False,default_chart_template +610160,610160,Car fuel,account.data_account_type_expenses,False,default_chart_template +610170,610170,Car maintenance,account.data_account_type_expenses,False,default_chart_template +610180,610180,Telephone,account.data_account_type_expenses,False,default_chart_template +610190,610190,Internet & hosting,account.data_account_type_expenses,False,default_chart_template +610200,610200,Mobiles,account.data_account_type_expenses,False,default_chart_template +610210,610210,Stationery,account.data_account_type_expenses,False,default_chart_template +610220,610220,Office consumables,account.data_account_type_expenses,False,default_chart_template +610230,610230,Postage and Carriage,account.data_account_type_expenses,False,default_chart_template +610240,610240,Books,account.data_account_type_expenses,False,default_chart_template +610250,610250,Network costs,account.data_account_type_expenses,False,default_chart_template +610260,610260,Software expenses,account.data_account_type_expenses,False,default_chart_template +610270,610270,Other computer costs,account.data_account_type_expenses,False,default_chart_template +610280,610280,Recruitment fees,account.data_account_type_expenses,False,default_chart_template +610290,610290,Other admin expenses,account.data_account_type_expenses,False,default_chart_template +610300,610300,Accounting,account.data_account_type_expenses,False,default_chart_template +610310,610310,Auditing,account.data_account_type_expenses,False,default_chart_template +610320,610320,Consultancy,account.data_account_type_expenses,False,default_chart_template +610330,610330,Legal and professional charges,account.data_account_type_expenses,False,default_chart_template +610340,610340,Exchange gains/losses,account.data_account_type_expenses,False,default_chart_template +610350,610350,Other sundry expenses,account.data_account_type_expenses,False,default_chart_template +610360,610360,Bad debts,account.data_account_type_expenses,False,default_chart_template +610370,610370,Interest paid,account.data_account_type_expenses,False,default_chart_template +610380,610380,Bank Charges,account.data_account_type_expenses,False,default_chart_template +610390,610390,Donations,account.data_account_type_expenses,False,default_chart_template +610400,610400,Entertaining,account.data_account_type_expenses,False,default_chart_template +610410,610410,Insurance,account.data_account_type_expenses,False,default_chart_template +610420,610420,Travel and subsistence,account.data_account_type_expenses,False,default_chart_template +610430,610430,Corporation tax expense,account.data_account_type_expenses,False,default_chart_template +610440,610440,Foreign Exchange Gains/Losses,account.data_account_type_expenses,False,default_chart_template +610450,610450,Price Differences Control Account,account.data_account_type_expenses,False,default_chart_template +610460,610460,Cash Register Gains/Losses,account.data_account_type_expenses,False,default_chart_template +620010,620010,Software Depreciation,account.data_account_type_depreciation,False,default_chart_template +620020,620020,Patents & Trademarks Depreciation,account.data_account_type_depreciation,False,default_chart_template +620030,620030,Fixtures and fittings Depreciation,account.data_account_type_depreciation,False,default_chart_template +620040,620040,Land and buildings Depreciation,account.data_account_type_depreciation,False,default_chart_template +620050,620050,Motor vehicles Depreciation,account.data_account_type_depreciation,False,default_chart_template +620060,620060,Office equipment (inc computer equipment) Depreciation,account.data_account_type_depreciation,False,default_chart_template +620070,620070,Plant and machinery Depreciation,account.data_account_type_depreciation,False,default_chart_template
\ No newline at end of file diff --git a/addons/l10n_za/data/account.tax.group.csv b/addons/l10n_za/data/account.tax.group.csv new file mode 100644 index 00000000000..ec35d11456c --- /dev/null +++ b/addons/l10n_za/data/account.tax.group.csv @@ -0,0 +1,3 @@ +id,name +tax_group_0,VAT 0% +tax_group_1,VAT 15% diff --git a/addons/l10n_za/data/account_chart_template_configure_data.xml b/addons/l10n_za/data/account_chart_template_configure_data.xml new file mode 100644 index 00000000000..6a3db99a6d5 --- /dev/null +++ b/addons/l10n_za/data/account_chart_template_configure_data.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo noupdate="1"> + + <function model="account.chart.template" name="try_loading_for_current_company"> + <value eval="[ref('l10n_za.default_chart_template')]"/> + </function> + +</odoo> diff --git a/addons/l10n_za/data/account_chart_template_data.xml b/addons/l10n_za/data/account_chart_template_data.xml new file mode 100644 index 00000000000..652646a5d18 --- /dev/null +++ b/addons/l10n_za/data/account_chart_template_data.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + + <menuitem id="account_reports_za_statements_menu" name="South Africa" parent="account.menu_finance_reports" sequence="0" groups="account.group_account_user"/> + + <record id="default_chart_template" model="account.chart.template"> + <field name="name">South African Tax and Account Chart Template (by Paradigm Digital)</field> + <field name="bank_account_code_prefix">1200</field> + <field name="cash_account_code_prefix">1250</field> + <field name="transfer_account_code_prefix">1010</field> + <field name="code_digits">6</field> + <field name="currency_id" ref="base.ZAR"/> + </record> + +</odoo> diff --git a/addons/l10n_za/data/account_chart_template_post_data.xml b/addons/l10n_za/data/account_chart_template_post_data.xml new file mode 100644 index 00000000000..1d1fa398173 --- /dev/null +++ b/addons/l10n_za/data/account_chart_template_post_data.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + + <record id="default_chart_template" model="account.chart.template"> + <field name="property_account_receivable_id" ref="110010"/> + <field name="property_account_payable_id" ref="220010"/> + <field name="property_account_expense_categ_id" ref="600010"/> + <field name="property_account_income_categ_id" ref="500010"/> + <field name="property_stock_account_input_categ_id" ref="200010"/> + <field name="property_stock_account_output_categ_id" ref="100050"/> + <field name="property_stock_valuation_account_id" ref="100020"/> + <field name="income_currency_exchange_account_id" ref="610440"/> + <field name="expense_currency_exchange_account_id" ref="610440"/> + <field name="use_anglo_saxon" eval="True"/> + </record> + +</odoo> diff --git a/addons/l10n_za/data/account_tax_report_data.xml b/addons/l10n_za/data/account_tax_report_data.xml new file mode 100644 index 00000000000..e0a53422a55 --- /dev/null +++ b/addons/l10n_za/data/account_tax_report_data.xml @@ -0,0 +1,248 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + + <record id="total_vat_payable" model="account.tax.report.line"> + <field name="name">[20] VAT PAYABLE/REFUNDABLE (Total A - Total B)</field> + <field name="formula"> (VAT4 + VAT4A + (SEC6 * 60/100 + SEC7) + VAT11 + VAT12) - (VAT14 + VAT14A + VAT15 + VAT16 + VAT17 + VAT18)</field> + <field name="country_id" ref="base.za"/> + <field name="sequence" eval="1"/> + </record> + + <record id="total_output_tax" model="account.tax.report.line"> + <field name="name">[13] Total A: TOTAL OUTPUT TAX (4 + 4A + 9 + 11 + 12)</field> + <field name="sequence" eval="1"/> + <field name="formula">VAT4 + VAT4A + (SEC6 * 60/100 + SEC7) + VAT11 + VAT12</field> + <field name="parent_id" ref='total_vat_payable'/> + <field name="country_id" ref="base.za"/> + </record> + + <record id="total_input_tax" model="account.tax.report.line"> + <field name="name">[19] Total B: TOTAL INPUT TAX (14 + 14A + 15 + 16 + 17 + 18)</field> + <field name="sequence" eval="2"/> + <field name="formula">VAT14 + VAT14A + VAT15 + VAT16 + VAT17 + VAT18</field> + <field name="parent_id" ref='total_vat_payable'/> + <field name="country_id" ref="base.za"/> + </record> + + <record id="standard_rate_exclude_capital_goods_service" model="account.tax.report.line"> + <field name="name">[1] Standard Rate (Excluding Capital goods and/or services and accomodation)</field> + <field name="tag_name">[1] Standard Rate (Excluding Capital goods and/or services and accomodation)</field> + <field name="code">VAT1</field> + <field name="sequence" eval="1"/> + <field name="parent_id" ref='total_output_tax'/> + <field name="country_id" ref="base.za"/> + </record> + + <record id="vat_on_standard_rate_exclude_capital_goods_service" model="account.tax.report.line"> + <field name="name">[4] x 15/ (100 + 15)</field> + <field name="tag_name">[4] x 15/ (100 + 15)</field> + <field name="code">VAT4</field> + <field name="sequence" eval="2"/> + <field name="parent_id" ref='total_output_tax'/> + <field name="country_id" ref="base.za"/> + </record> + + <record id="standard_rate_only_capital_goods_service" model="account.tax.report.line"> + <field name="name">[1A] Standard Rate (Only Capital goods and/or services)</field> + <field name="tag_name">[1A] Standard Rate (Only Capital goods and/or services)</field> + <field name="code">VAT1A</field> + <field name="sequence" eval="3"/> + <field name="parent_id" ref='total_output_tax'/> + <field name="country_id" ref="base.za"/> + </record> + + <record id="vat_on_standard_rate_only_capital_goods_service" model="account.tax.report.line"> + <field name="name">[4A] x 15/ (100 + 15)</field> + <field name="tag_name">[4A] x 15/ (100 + 15)</field> + <field name="code">VAT4A</field> + <field name="sequence" eval="5"/> + <field name="parent_id" ref='total_output_tax'/> + <field name="country_id" ref="base.za"/> + </record> + + <record id="zero_rate_exclude_goods_exported" model="account.tax.report.line"> + <field name="name">[2] Zero Rate (excluding goods exported)</field> + <field name="tag_name">[2] Zero Rate (excluding goods exported)</field> + <field name="code">VAT2</field> + <field name="sequence" eval="6"/> + <field name="parent_id" ref='total_output_tax'/> + <field name="country_id" ref="base.za"/> + </record> + + <record id="zero_rate_only_goods_exported" model="account.tax.report.line"> + <field name="name">[2A] Zero Rate (Only goods exported)</field> + <field name="tag_name">[2A] Zero Rate (Only goods exported)</field> + <field name="code">VAT2A</field> + <field name="sequence" eval="7"/> + <field name="parent_id" ref='total_output_tax'/> + <field name="country_id" ref="base.za"/> + </record> + + <record id="exempt_and_non_supplies" model="account.tax.report.line"> + <field name="name">[3] Exempt and Non supplies</field> + <field name="tag_name">[3] Exempt and Non supplies</field> + <field name="code">VAT3</field> + <field name="sequence" eval="8"/> + <field name="parent_id" ref='total_output_tax'/> + <field name="country_id" ref="base.za"/> + </record> + + <record id="accomodation_exceeding_28_days" model="account.tax.report.line"> + <field name="name">[5] Accomodation exceeding 28 days</field> + <field name="tag_name">[5] Accomodation exceeding 28 days</field> + <field name="code">VAT5</field> + <field name="sequence" eval="9"/> + <field name="parent_id" ref='total_output_tax'/> + <field name="country_id" ref="base.za"/> + </record> + + <record id="accomodation_exceeding_28_days_60_percent" model="account.tax.report.line"> + <field name="name">[6] x 60%</field> + <field name="sequence" eval="11"/> + <field name="formula"> VAT5 * 60 / 100</field> + <field name="parent_id" ref='total_output_tax'/> + <field name="country_id" ref="base.za"/> + </record> + + <record id="accomodation_under_28_days" model="account.tax.report.line"> + <field name="name">[7] Accomodation under 28 days</field> + <field name="tag_name">[7] Accomodation under 28 days</field> + <field name="code">VAT7</field> + <field name="sequence" eval="12"/> + <field name="parent_id" ref='total_output_tax'/> + <field name="country_id" ref="base.za"/> + </record> + + <record id="accomodation_28_days" model="account.tax.report.line"> + <field name="name">[8] Total (6 + 7)</field> + <field name="sequence" eval="14"/> + <field name="formula">(VAT5 * 60 / 100) + VAT7</field> + <field name="parent_id" ref='total_output_tax'/> + <field name="country_id" ref="base.za"/> + </record> + + <record id="vat_on_accomodation_28_days" model="account.tax.report.line"> + <field name="name">[9] x 15 / (100 ??) </field> + <field name="sequence" eval="15"/> + <field name="formula"> SEC6 * 60/100 + SEC7 </field> + <field name="parent_id" ref='total_output_tax'/> + <field name="country_id" ref="base.za"/> + </record> + + <record id="vat_on_accomodation_exceeding_28_days" model="account.tax.report.line"> + <field name="name">VAT on Accomodation exceeding 28 days</field> + <field name="tag_name">VAT on Accomodation exceeding 28 days</field> + <field name="code">SEC6</field> + <field name="sequence" eval="1"/> + <field name="parent_id" ref='vat_on_accomodation_28_days'/> + <field name="country_id" ref="base.za"/> + </record> + + <record id="vat_on_accomodation_under_28_days" model="account.tax.report.line"> + <field name="name">VAT on Accomodation under 28 days</field> + <field name="tag_name">VAT on Accomodation under 28 days</field> + <field name="code">SEC7</field> + <field name="sequence" eval="2"/> + <field name="parent_id" ref='vat_on_accomodation_28_days'/> + <field name="country_id" ref="base.za"/> + </record> + + <record id="vat_plus_base_of_second_hand_goods_export" model="account.tax.report.line"> + <field name="name">[10] Change in use and export of second-hand goods</field> + <field name="sequence" eval="17"/> + <field name="formula"> VAT10a + VAT11</field> + <field name="parent_id" ref='total_output_tax'/> + <field name="country_id" ref="base.za"/> + </record> + + <record id="second_hand_goods_export" model="account.tax.report.line"> + <field name="name">[10] Base Amount: Change in use and export of second-hand goods</field> + <field name="tag_name">[10] Change in use and export of second-hand goods</field> + <field name="code">VAT10a</field> + <field name="sequence" eval="1"/> + <field name="parent_id" ref='vat_plus_base_of_second_hand_goods_export'/> + <field name="country_id" ref="base.za"/> + </record> + + <record id="vat_on_second_hand_goods_export" model="account.tax.report.line"> + <field name="name">[11] x 15 / (100 + 15)</field> + <field name="tag_name">[11] x 15 / (100 + 15)</field> + <field name="code">VAT11</field> + <field name="sequence" eval="18"/> + <field name="parent_id" ref='total_output_tax'/> + <field name="country_id" ref="base.za"/> + </record> + + <record id="other_imported_services" model="account.tax.report.line"> + <field name="name">[12] Other and imported services</field> + <field name="tag_name">[12] Other and imported services</field> + <field name="code">VAT12</field> + <field name="sequence" eval="19"/> + <field name="parent_id" ref='total_output_tax'/> + <field name="country_id" ref="base.za"/> + </record> + + <record id="capital_goods_services_supplied" model="account.tax.report.line"> + <field name="name">[14] Capital Goods and/or services supplied to you</field> + <field name="tag_name">[14] Capital Goods and/or services supplied to you</field> + <field name="code">VAT14</field> + <field name="sequence" eval="21"/> + <field name="parent_id" ref='total_input_tax'/> + <field name="country_id" ref="base.za"/> + </record> + + <record id="capital_goods_imported" model="account.tax.report.line"> + <field name="name">[14A] Capital Goods imported by you</field> + <field name="tag_name">[14A] Capital Goods imported by you</field> + <field name="code">VAT14A</field> + <field name="sequence" eval="22"/> + <field name="parent_id" ref='total_input_tax'/> + <field name="country_id" ref="base.za"/> + </record> + + <record id="other_goods_services_supplied" model="account.tax.report.line"> + <field name="name">[15] Other goods and/or services supplied to you (not Capital Goods)</field> + <field name="tag_name">[15] Other goods and/or services supplied to you (not Capital Goods)</field> + <field name="code">VAT15</field> + <field name="sequence" eval="23"/> + <field name="parent_id" ref='total_input_tax'/> + <field name="country_id" ref="base.za"/> + </record> + + <record id="other_goods_imported" model="account.tax.report.line"> + <field name="name">[15A] Other goods imported by you (not Capital Goods)</field> + <field name="tag_name">[15A] Other goods imported by you (not Capital Goods)</field> + <field name="code">VAT15A</field> + <field name="sequence" eval="24"/> + <field name="parent_id" ref='total_input_tax'/> + <field name="country_id" ref="base.za"/> + </record> + + <record id="change_in_use" model="account.tax.report.line"> + <field name="name">[16] Change in Use</field> + <field name="tag_name">[16] Change in Use</field> + <field name="code">VAT16</field> + <field name="sequence" eval="25"/> + <field name="parent_id" ref='total_input_tax'/> + <field name="country_id" ref="base.za"/> + </record> + + <record id="bad_debts" model="account.tax.report.line"> + <field name="name">[17] Bad Debts</field> + <field name="tag_name">[17] Bad Debts</field> + <field name="code">VAT17</field> + <field name="sequence" eval="26"/> + <field name="parent_id" ref='total_input_tax'/> + <field name="country_id" ref="base.za"/> + </record> + + <record id="others" model="account.tax.report.line"> + <field name="name">[18] Other</field> + <field name="tag_name">[18] Other</field> + <field name="code">VAT18</field> + <field name="sequence" eval="27"/> + <field name="parent_id" ref='total_input_tax'/> + <field name="country_id" ref="base.za"/> + </record> + +</odoo> diff --git a/addons/l10n_za/data/account_tax_template_data.xml b/addons/l10n_za/data/account_tax_template_data.xml new file mode 100644 index 00000000000..60c86758117 --- /dev/null +++ b/addons/l10n_za/data/account_tax_template_data.xml @@ -0,0 +1,558 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + + <record id="ST1" model="account.tax.template"> + <field name="description">15%</field> + <field name="chart_template_id" ref="default_chart_template"/> + <field name="type_tax_use">sale</field> + <field name="name">Standard Rate</field> + <field name="amount_type">percent</field> + <field name="amount">15</field> + <field name="tax_group_id" ref="tax_group_1"/> + <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'plus_report_line_ids': [ref('standard_rate_exclude_capital_goods_service')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('200060'), + 'plus_report_line_ids': [ref('standard_rate_exclude_capital_goods_service'), ref('vat_on_standard_rate_exclude_capital_goods_service')], + }), + ]"/> + <field name="refund_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'minus_report_line_ids': [ref('standard_rate_exclude_capital_goods_service')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('200060'), + 'minus_report_line_ids': [ref('standard_rate_exclude_capital_goods_service'), ref('vat_on_standard_rate_exclude_capital_goods_service')], + }), + ]"/> + </record> + + <record id="ST1A" model="account.tax.template"> + <field name="description">15%</field> + <field name="chart_template_id" ref="default_chart_template"/> + <field name="type_tax_use">sale</field> + <field name="name">Standard Rate (Capital Goods)</field> + <field name="amount_type">percent</field> + <field name="amount">15</field> + <field name="tax_group_id" ref="tax_group_1"/> + <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'plus_report_line_ids': [ref('standard_rate_only_capital_goods_service')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('200060'), + 'plus_report_line_ids': [ref('standard_rate_only_capital_goods_service'), ref('vat_on_standard_rate_only_capital_goods_service')], + }), + ]"/> + <field name="refund_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'minus_report_line_ids': [ref('standard_rate_only_capital_goods_service')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('200060'), + 'minus_report_line_ids': [ref('standard_rate_only_capital_goods_service'), ref('vat_on_standard_rate_only_capital_goods_service')], + }), + ]"/> + </record> + + <record id="ST2" model="account.tax.template"> + <field name="description">0%</field> + <field name="chart_template_id" ref="default_chart_template"/> + <field name="type_tax_use">sale</field> + <field name="name">Zero Rate</field> + <field name="amount_type">percent</field> + <field name="amount">0</field> + <field name="tax_group_id" ref="tax_group_0"/> + <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'plus_report_line_ids': [ref('zero_rate_exclude_goods_exported')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('200060'), + }), + ]"/> + <field name="refund_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'minus_report_line_ids': [ref('zero_rate_exclude_goods_exported')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('200060'), + }), + ]"/> + </record> + + <record id="ST2A" model="account.tax.template"> + <field name="description">0%</field> + <field name="chart_template_id" ref="default_chart_template"/> + <field name="type_tax_use">sale</field> + <field name="name">Zero Rate Exports</field> + <field name="amount_type">percent</field> + <field name="amount">0</field> + <field name="tax_group_id" ref="tax_group_0"/> + <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'plus_report_line_ids': [ref('zero_rate_only_goods_exported')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('200060'), + }), + ]"/> + <field name="refund_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'minus_report_line_ids': [ref('zero_rate_only_goods_exported')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('200060'), + }), + ]"/> + </record> + + <record id="ST3" model="account.tax.template"> + <field name="description">0%</field> + <field name="chart_template_id" ref="default_chart_template"/> + <field name="type_tax_use">sale</field> + <field name="name">Exempt and Non-Supplies</field> + <field name="amount_type">percent</field> + <field name="amount">0</field> + <field name="tax_group_id" ref="tax_group_0"/> + <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'plus_report_line_ids': [ref('exempt_and_non_supplies')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('200060'), + }), + ]"/> + <field name="refund_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'minus_report_line_ids': [ref('exempt_and_non_supplies')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('200060'), + }), + ]"/> + </record> + + <record id="ST5" model="account.tax.template"> + <field name="description">15%</field> + <field name="chart_template_id" ref="default_chart_template"/> + <field name="type_tax_use">sale</field> + <field name="name">Accommodation (28+ days)</field> + <field name="amount_type">percent</field> + <field name="amount">15</field> + <field name="tax_group_id" ref="tax_group_1"/> + <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'plus_report_line_ids': [ref('accomodation_exceeding_28_days')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('200060'), + 'plus_report_line_ids': [ref('vat_on_accomodation_exceeding_28_days')], + }), + ]"/> + <field name="refund_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'minus_report_line_ids': [ref('accomodation_exceeding_28_days')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('200060'), + 'minus_report_line_ids': [ref('vat_on_accomodation_exceeding_28_days')], + }), + ]"/> + </record> + + <record id="ST7" model="account.tax.template"> + <field name="description">15%</field> + <field name="chart_template_id" ref="default_chart_template"/> + <field name="type_tax_use">sale</field> + <field name="name">Accommodation (Under 28 days)</field> + <field name="amount_type">percent</field> + <field name="amount">15</field> + <field name="tax_group_id" ref="tax_group_1"/> + <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'plus_report_line_ids': [ref('accomodation_under_28_days')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('200060'), + 'plus_report_line_ids': [ref('vat_on_accomodation_under_28_days')], + }), + ]"/> + <field name="refund_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'minus_report_line_ids': [ref('accomodation_under_28_days')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('200060'), + 'minus_report_line_ids': [ref('vat_on_accomodation_under_28_days')], + }), + ]"/> + </record> + + <record id="ST10" model="account.tax.template"> + <field name="description">15%</field> + <field name="chart_template_id" ref="default_chart_template"/> + <field name="type_tax_use">sale</field> + <field name="name">Export of Second-hand Goods/ Change in Use</field> + <field name="amount_type">percent</field> + <field name="amount">15</field> + <field name="tax_group_id" ref="tax_group_1"/> + <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'plus_report_line_ids': [ref('second_hand_goods_export')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('200060'), + 'plus_report_line_ids': [ref('vat_on_second_hand_goods_export')], + }), + ]"/> + <field name="refund_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + 'minus_report_line_ids': [ref('second_hand_goods_export')], + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('200060'), + 'minus_report_line_ids': [ref('vat_on_second_hand_goods_export')], + }), + ]"/> + </record> + + <record id="ST12" model="account.tax.template"> + <field name="description">15%</field> + <field name="chart_template_id" ref="default_chart_template"/> + <field name="type_tax_use">sale</field> + <field name="name">VAT Adjustments and Manual VAT</field> + <field name="amount_type">percent</field> + <field name="amount">15</field> + <field name="tax_group_id" ref="tax_group_1"/> + <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('200060'), + 'plus_report_line_ids': [ref('other_imported_services')], + }), + ]"/> + <field name="refund_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('200060'), + 'minus_report_line_ids': [ref('other_imported_services')], + }), + ]"/> + </record> + + <record id="PT15" model="account.tax.template"> + <field name="description">15%</field> + <field name="chart_template_id" ref="default_chart_template"/> + <field name="type_tax_use">purchase</field> + <field name="name">Standard Rate</field> + <field name="amount_type">percent</field> + <field name="amount">15</field> + <field name="tax_group_id" ref="tax_group_1"/> + <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('100060'), + 'plus_report_line_ids': [ref('other_goods_services_supplied')], + }), + ]"/> + <field name="refund_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('100060'), + 'minus_report_line_ids': [ref('other_goods_services_supplied')], + }), + ]"/> + </record> + + <record id="PT14" model="account.tax.template"> + <field name="description">15%</field> + <field name="chart_template_id" ref="default_chart_template"/> + <field name="type_tax_use">purchase</field> + <field name="name">Standard Rate (Capital Goods)</field> + <field name="amount_type">percent</field> + <field name="amount">15</field> + <field name="tax_group_id" ref="tax_group_1"/> + <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('100060'), + 'plus_report_line_ids': [ref('capital_goods_services_supplied')], + }), + ]"/> + <field name="refund_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('100060'), + 'minus_report_line_ids': [ref('capital_goods_services_supplied')], + }), + ]"/> + </record> + + <record id="PT14A" model="account.tax.template"> + <field name="description">15%</field> + <field name="chart_template_id" ref="default_chart_template"/> + <field name="type_tax_use">purchase</field> + <field name="name">Capital Goods Imported</field> + <field name="amount_type">percent</field> + <field name="amount">15</field> + <field name="tax_group_id" ref="tax_group_1"/> + <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('100060'), + 'plus_report_line_ids': [ref('capital_goods_imported')], + }), + ]"/> + <field name="refund_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('100060'), + 'minus_report_line_ids': [ref('capital_goods_imported')], + }), + ]"/> + </record> + + <record id="PT15A" model="account.tax.template"> + <field name="description">15%</field> + <field name="chart_template_id" ref="default_chart_template"/> + <field name="type_tax_use">purchase</field> + <field name="name">Goods and Services Imported</field> + <field name="amount_type">percent</field> + <field name="amount">15</field> + <field name="tax_group_id" ref="tax_group_1"/> + <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('100060'), + 'plus_report_line_ids': [ref('other_goods_imported')], + }), + ]"/> + <field name="refund_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('100060'), + 'minus_report_line_ids': [ref('other_goods_imported')], + }), + ]"/> + </record> + + <record id="PT16" model="account.tax.template"> + <field name="description">15%</field> + <field name="chart_template_id" ref="default_chart_template"/> + <field name="type_tax_use">purchase</field> + <field name="name">Change in Use</field> + <field name="amount_type">percent</field> + <field name="amount">15</field> + <field name="tax_group_id" ref="tax_group_1"/> + <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('100060'), + 'plus_report_line_ids': [ref('change_in_use')], + }), + ]"/> + <field name="refund_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('100060'), + 'minus_report_line_ids': [ref('change_in_use')], + }), + ]"/> + </record> + + <record id="PT17" model="account.tax.template"> + <field name="description">15%</field> + <field name="chart_template_id" ref="default_chart_template"/> + <field name="type_tax_use">purchase</field> + <field name="name">Bad Debts</field> + <field name="amount_type">percent</field> + <field name="amount">15</field> + <field name="tax_group_id" ref="tax_group_1"/> + <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('100060'), + 'plus_report_line_ids': [ref('bad_debts')], + }), + ]"/> + <field name="refund_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('100060'), + 'minus_report_line_ids': [ref('bad_debts')], + }), + ]"/> + </record> + + <record id="PT18" model="account.tax.template"> + <field name="description">15%</field> + <field name="chart_template_id" ref="default_chart_template"/> + <field name="type_tax_use">purchase</field> + <field name="name">Other Adjustments</field> + <field name="amount_type">percent</field> + <field name="amount">15</field> + <field name="tax_group_id" ref="tax_group_1"/> + <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('100060'), + 'plus_report_line_ids': [ref('others')], + }), + ]"/> + <field name="refund_repartition_line_ids" eval="[(5, 0, 0), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'base', + }), + (0,0, { + 'factor_percent': 100, + 'repartition_type': 'tax', + 'account_id': ref('100060'), + 'minus_report_line_ids': [ref('others')], + }), + ]"/> + </record> + +</odoo> diff --git a/addons/l10n_za/static/description/icon.png b/addons/l10n_za/static/description/icon.png Binary files differnew file mode 100644 index 00000000000..1f82ab5459c --- /dev/null +++ b/addons/l10n_za/static/description/icon.png diff --git a/addons/lunch/controllers/main.py b/addons/lunch/controllers/main.py index ac645c2edc4..2097f86659a 100644 --- a/addons/lunch/controllers/main.py +++ b/addons/lunch/controllers/main.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import http, fields +from odoo import _, http, fields +from odoo.exceptions import AccessError from odoo.http import request from odoo.osv import expression from odoo.tools import float_round, float_repr @@ -10,6 +11,7 @@ from odoo.tools import float_round, float_repr class LunchController(http.Controller): @http.route('/lunch/infos', type='json', auth='user') def infos(self, user_id=None): + self._check_user_impersonification(user_id) user = request.env['res.users'].browse(user_id) if user_id else request.env.user infos = self._make_infos(user, order=False) @@ -35,6 +37,7 @@ class LunchController(http.Controller): @http.route('/lunch/trash', type='json', auth='user') def trash(self, user_id=None): + self._check_user_impersonification(user_id) user = request.env['res.users'].browse(user_id) if user_id else request.env.user lines = self._get_current_lines(user.id) @@ -43,6 +46,7 @@ class LunchController(http.Controller): @http.route('/lunch/pay', type='json', auth='user') def pay(self, user_id=None): + self._check_user_impersonification(user_id) user = request.env['res.users'].browse(user_id) if user_id else request.env.user lines = self._get_current_lines(user.id) @@ -60,13 +64,15 @@ class LunchController(http.Controller): @http.route('/lunch/user_location_set', type='json', auth='user') def set_user_location(self, location_id, user_id=None): + self._check_user_impersonification(user_id) user = request.env['res.users'].browse(user_id) if user_id else request.env.user - user.last_lunch_location_id = request.env['lunch.location'].browse(location_id) + user.sudo().last_lunch_location_id = request.env['lunch.location'].browse(location_id) return True @http.route('/lunch/user_location_get', type='json', auth='user') def get_user_location(self, user_id=None): + self._check_user_impersonification(user_id) user = request.env['res.users'].browse(user_id) if user_id else request.env.user return user.last_lunch_location_id.id @@ -87,7 +93,9 @@ class LunchController(http.Controller): 'currency': {'symbol': currency.symbol, 'position': currency.position}, }) - user_location = user.last_lunch_location_id if user.last_lunch_location_id else request.env['lunch.location'].search([], limit=1) + if not user.last_lunch_location_id: + user.last_lunch_location_id = request.env['lunch.location'].search([], limit=1) + user_location = user.last_lunch_location_id alert_domain = expression.AND([ [('available_today', '=', True)], @@ -101,6 +109,10 @@ class LunchController(http.Controller): return res + def _check_user_impersonification(self, user_id=None): + if (user_id and request.env.uid != user_id and not request.env.user.has_group('lunch.group_lunch_manager')): + raise AccessError(_('You are trying to impersonate another user, but this can only be done by a lunch manager')) + def _get_current_lines(self, user_id): return request.env['lunch.order'].search([('user_id', '=', user_id), ('date', '=', fields.Date.today()), ('state', '!=', 'cancelled')]) diff --git a/addons/lunch/models/lunch_order.py b/addons/lunch/models/lunch_order.py index 52e4917e58d..fdf5a1a5559 100644 --- a/addons/lunch/models/lunch_order.py +++ b/addons/lunch/models/lunch_order.py @@ -42,6 +42,10 @@ class LunchOrder(models.Model): display_toppings = fields.Text('Toppings', compute='_compute_display_toppings', store=True) + def init(self): + self._cr.execute("""CREATE INDEX IF NOT EXISTS lunch_order_user_product_date ON %s (user_id, product_id, date)""" + % self._table) + @api.depends('topping_ids_1', 'topping_ids_2', 'topping_ids_3', 'product_id', 'quantity') def _compute_total_price(self): for line in self: @@ -69,8 +73,8 @@ class LunchOrder(models.Model): 'To add some money to your wallet, please contact your lunch manager.')) def action_order(self): - self._check_wallet() self.write({'state': 'ordered'}) + self._check_wallet() def action_confirm(self): self.write({'state': 'confirmed'}) diff --git a/addons/lunch/report/lunch_product_report.py b/addons/lunch/report/lunch_product_report.py index 7c9fcfcb000..2615cb827ee 100644 --- a/addons/lunch/report/lunch_product_report.py +++ b/addons/lunch/report/lunch_product_report.py @@ -81,13 +81,12 @@ class LunchProductReport(models.Model): users.id AS user_id, fav.user_id IS NOT NULL AS is_favorite, product.new_until >= current_date AS is_new, - max(l_order.date) AS last_order_date + orders.last_order_date FROM lunch_product product - RIGHT JOIN res_users users ON users.company_id = product.company_id -- multi company - LEFT JOIN lunch_product_favorite_user_rel fav ON fav.product_id = product.id AND fav.user_id = users.id - JOIN res_groups_users_rel groups ON groups.uid = users.id -- only generate for internal users - JOIN lunch_order l_order ON l_order.product_id = product.id AND l_order.user_id = users.id AND l_order.date <= current_date + INNER JOIN res_users users ON users.company_id = product.company_id -- multi company + INNER JOIN res_groups_users_rel groups ON groups.uid = users.id -- only generate for internal users + LEFT JOIN LATERAL (select max(date) AS last_order_date FROM lunch_order where user_id=users.id and product_id=product.id) AS orders ON TRUE + LEFT JOIN LATERAL (select user_id FROM lunch_product_favorite_user_rel where user_id=users.id and product_id=product.id) AS fav ON TRUE WHERE users.active AND product.active AND groups.gid = %%s --only take into account active products and users - GROUP BY product.id, product.name, product.category_id, product.description, product.price, product.supplier_id, product.company_id, product.active, users.id, is_favorite, is_new ); """ % self._table, (self.env.ref('base.group_user').id,)) diff --git a/addons/lunch/static/src/js/lunch_kanban_controller.js b/addons/lunch/static/src/js/lunch_kanban_controller.js index 80a39adae96..f1acf81b301 100644 --- a/addons/lunch/static/src/js/lunch_kanban_controller.js +++ b/addons/lunch/static/src/js/lunch_kanban_controller.js @@ -77,13 +77,14 @@ var LunchKanbanController = KanbanController.extend({ */ _renderLunchKanbanWidget: function () { var self = this; - if (this.widget) { - this.widget.destroy(); - } + var oldWidget = this.widget; this.widgetData.wallet = parseFloat(this.widgetData.wallet).toFixed(2); this.widget = new LunchKanbanWidget(this, _.extend(this.widgetData, {edit: this.editMode})); return this.widget.appendTo(document.createDocumentFragment()).then(function () { self.$('.o_lunch_kanban').prepend(self.widget.$el); + if (oldWidget) { + oldWidget.destroy(); + } }); }, _showPaymentDialog: function (title) { diff --git a/addons/lunch/static/tests/lunch_kanban_tests.js b/addons/lunch/static/tests/lunch_kanban_tests.js index 32cac593dcc..8bc4879f9e5 100644 --- a/addons/lunch/static/tests/lunch_kanban_tests.js +++ b/addons/lunch/static/tests/lunch_kanban_tests.js @@ -128,6 +128,53 @@ QUnit.module('LunchKanbanView', { kanban.destroy(); }); + QUnit.test('no flickering at reload', async function (assert) { + assert.expect(2); + + const self = this; + let infosProm = Promise.resolve(); + const kanban = await createLunchKanbanView({ + View: LunchKanbanView, + model: 'product', + data: this.data, + arch: ` + <kanban> + <templates> + <t t-name="kanban-box"> + <div><field name="name"/></div> + </t> + </templates> + </kanban> + `, + mockRPC: function (route, args) { + if (route === '/lunch/user_location_get') { + return Promise.resolve(self.data['lunch.location'].records[0].id); + } + if (route === '/lunch/infos') { + return Promise.resolve(self.regularInfos); + } + var result = this._super.apply(this, arguments); + if (args.method === 'xmlid_to_res_id') { + // delay the rendering of the lunch widget + return infosProm.then(_.constant(result)); + } + return result; + }, + }); + + infosProm = testUtils.makeTestPromise(); + kanban.reload(); + + assert.strictEqual(kanban.$('.o_lunch_widget').length, 1, + "old widget should still be present"); + + await infosProm.resolve(); + + assert.strictEqual(kanban.$('.o_lunch_widget').length, 1); + + kanban.destroy(); + }); + QUnit.module('LunchKanbanWidget', function () { QUnit.test('empty cart', async function (assert) { @@ -814,7 +861,6 @@ QUnit.module('LunchKanbanView', { kanban.destroy(); }); - }); }); diff --git a/addons/lunch/views/res_config_settings.xml b/addons/lunch/views/res_config_settings.xml index bd99a708b4c..fc73ba99151 100644 --- a/addons/lunch/views/res_config_settings.xml +++ b/addons/lunch/views/res_config_settings.xml @@ -14,6 +14,7 @@ title="None"> <div class="o_setting_right_pane"> <span class="o_form_label">Lunch Overdraft</span> + <span class="fa fa-lg fa-building-o" title="Values set here are company-specific." aria-label="Values set here are company-specific." groups="base.group_multi_company" role="img"/> <div class="text-muted"> Maximum overdraft that your employees can reach </div> diff --git a/addons/lunch/wizard/lunch_line_wizard.py b/addons/lunch/wizard/lunch_line_wizard.py index e933c1e6008..4a704215443 100644 --- a/addons/lunch/wizard/lunch_line_wizard.py +++ b/addons/lunch/wizard/lunch_line_wizard.py @@ -86,7 +86,7 @@ class LunchOrderWizard(models.TransientModel): sum((wizard.topping_ids_1 | wizard.topping_ids_2 | wizard.topping_ids_3).mapped('price'))) def _get_matching_lines(self): - domain = [('product_id', '=', self.product_id.id), ('date', '=', fields.Date.today()), ('note', '=', self._get_note())] + domain = [('user_id', '=', self.user_id.id), ('product_id', '=', self.product_id.id), ('date', '=', fields.Date.today()), ('note', '=', self._get_note())] lines = self.env['lunch.order'].search(domain) return lines.filtered(lambda line: (line.topping_ids_1 | line.topping_ids_2 | line.topping_ids_3) == self.topping_ids_1) diff --git a/addons/mail/models/mail_activity.py b/addons/mail/models/mail_activity.py index 3dee568ca10..1f04378e601 100644 --- a/addons/mail/models/mail_activity.py +++ b/addons/mail/models/mail_activity.py @@ -149,7 +149,7 @@ class MailActivity(models.Model): ('today', 'Today'), ('planned', 'Planned')], 'State', compute='_compute_state') - recommended_activity_type_id = fields.Many2one('mail.activity.type', string="Recommended Activity Type", readonly=True) + recommended_activity_type_id = fields.Many2one('mail.activity.type', string="Recommended Activity Type") previous_activity_type_id = fields.Many2one('mail.activity.type', string='Previous Activity Type', readonly=True) has_recommended_activities = fields.Boolean( 'Next activities available', @@ -302,7 +302,7 @@ class MailActivity(models.Model): target_record = self.env[activity.res_model].browse(activity.res_id) if hasattr(target_record, 'company_id') and ( target_record.company_id != target_user.company_id and ( - len(target_user.company_ids) > 1)): + len(target_user.sudo().company_ids) > 1)): return # in that case we skip the check, assuming it would fail because of the company model.browse(activity.res_id).check_access_rule('read') except exceptions.AccessError: diff --git a/addons/mail/models/mail_message.py b/addons/mail/models/mail_message.py index 2fab423412b..84607013996 100644 --- a/addons/mail/models/mail_message.py +++ b/addons/mail/models/mail_message.py @@ -4,6 +4,7 @@ import logging import re +from binascii import Error as binascii_error from operator import itemgetter from email.utils import formataddr from openerp.http import request @@ -998,7 +999,7 @@ class Message(models.Model): for record in self: model = model or record.model res_id = res_id or record.res_id - if 'message_ids' in self.env[model]: + if issubclass(self.pool[model], self.pool['mail.thread']): self.env[model].invalidate_cache(fnames=[ 'message_ids', 'message_unread', @@ -1032,15 +1033,20 @@ class Message(models.Model): key = match.group(2) if not data_to_url.get(key): name = match.group(4) if match.group(4) else 'image%s' % len(data_to_url) - attachment = Attachments.create({ - 'name': name, - 'datas': match.group(2), - 'res_model': values.get('model'), - 'res_id': values.get('res_id'), - }) - attachment.generate_access_token() - values['attachment_ids'].append((4, attachment.id)) - data_to_url[key] = ['/web/image/%s?access_token=%s' % (attachment.id, attachment.access_token), name] + try: + attachment = Attachments.create({ + 'name': name, + 'datas': match.group(2), + 'res_model': values.get('model'), + 'res_id': values.get('res_id'), + }) + except binascii_error: + _logger.warning("Impossible to create an attachment out of badly formated base64 embedded image. Image has been removed.") + return match.group(3) # group(3) is the url ending single/double quote matched by the regexp + else: + attachment.generate_access_token() + values['attachment_ids'].append((4, attachment.id)) + data_to_url[key] = ['/web/image/%s?access_token=%s' % (attachment.id, attachment.access_token), name] return '%s%s alt="%s"' % (data_to_url[key][0], match.group(3), data_to_url[key][1]) values['body'] = _image_dataurl.sub(base64_to_boundary, tools.ustr(values['body'])) diff --git a/addons/mail/static/src/js/chatter.js b/addons/mail/static/src/js/chatter.js index f81bcb2d0e2..6bb11d0a090 100644 --- a/addons/mail/static/src/js/chatter.js +++ b/addons/mail/static/src/js/chatter.js @@ -139,7 +139,7 @@ var Chatter = Widget.extend({ if (this.fields.activity) { this.fields.activity.$el.detach(); } - if (this.fields.thread) { + if (this.fields.thread && this.fields.thread.$el ) { this.fields.thread.$el.detach(); } @@ -404,7 +404,7 @@ var Chatter = Widget.extend({ self.fields.followers.$el.insertBefore(self.$('.o_chatter_button_attachment')); } } - if (self.fields.thread) { + if (self.fields.thread && self.fields.thread.$el) { self.fields.thread.$el.appendTo(self.$el); } }).then(always).guardedCatch(always); diff --git a/addons/mail/static/src/js/models/messages/message.js b/addons/mail/static/src/js/models/messages/message.js index 73c4ece9025..b5b2e068cbf 100644 --- a/addons/mail/static/src/js/models/messages/message.js +++ b/addons/mail/static/src/js/models/messages/message.js @@ -579,7 +579,8 @@ var Message = AbstractMessage.extend(Mixins.EventDispatcherMixin, ServicesMixin * @return {boolean} */ _isOdoobotAuthor: function () { - return this._serverAuthorID === this.call('mail_service', 'getOdoobotID'); + return this._serverAuthorID && + this._serverAuthorID[0] === this.call('mail_service', 'getOdoobotID')[0]; }, /** * State whether the message is transient or not diff --git a/addons/mail/static/src/js/services/mail_manager.js b/addons/mail/static/src/js/services/mail_manager.js index 27a7183ec88..dedeacca013 100644 --- a/addons/mail/static/src/js/services/mail_manager.js +++ b/addons/mail/static/src/js/services/mail_manager.js @@ -46,7 +46,7 @@ var PREVIEW_MSG_MAX_SIZE = 350; // optimal for native english speakers var MailManager = AbstractService.extend({ dependencies: ['ajax', 'bus_service', 'local_storage'], - _ODOOBOT_ID: "ODOOBOT", // default authorID for transient messages + _ODOOBOT_ID: ["ODOOBOT", "ODOOBOT"], // authorID for transient messages /** * @override @@ -258,7 +258,7 @@ var MailManager = AbstractService.extend({ /** * Get the OdooBot ID, which is the default authorID for transient messages * - * @returns {string} + * @returns {Array<string>} */ getOdoobotID: function () { return this._ODOOBOT_ID; diff --git a/addons/mail/static/src/js/services/mail_notification_manager.js b/addons/mail/static/src/js/services/mail_notification_manager.js index 75754f375d8..caf0c3c4e12 100644 --- a/addons/mail/static/src/js/services/mail_notification_manager.js +++ b/addons/mail/static/src/js/services/mail_notification_manager.js @@ -471,12 +471,11 @@ MailManager.include({ * * @private * @param {Object} data - * @param {string} data.author_id */ _handlePartnerTransientMessageNotification: function (data) { var lastMessage = _.last(this._messages); data.id = (lastMessage ? lastMessage.getID() : 0) + 0.01; - data.author_id = data.author_id || this.getOdoobotID(); + data.author_id = this.getOdoobotID(); this.addMessage(data); }, /** diff --git a/addons/mail/static/src/js/services/mail_status_manager.js b/addons/mail/static/src/js/services/mail_status_manager.js index 4a2df2af94f..7891461c9af 100644 --- a/addons/mail/static/src/js/services/mail_status_manager.js +++ b/addons/mail/static/src/js/services/mail_status_manager.js @@ -30,6 +30,9 @@ MailManager.include({ getImStatus: function (data) { var self = this; var partnerID = data.partnerID; + if (partnerID === this.getOdoobotID()[0]) { + return 'bot'; + } if (!this._imStatus[partnerID]) { // Add to list to call it in next bus update or _fetchMissingImStatus this._imStatus[partnerID] = undefined; @@ -84,9 +87,10 @@ MailManager.include({ return Promise.resolve(); } return this._rpc({ - model: 'res.partner', - method: 'read', - args: [partnerIDs, ['id', 'im_status']], + route: '/longpolling/im_status', + params: { partner_ids: partnerIDs }, + }, { + shadow: true, }).then( function (results) { self.updateImStatus(results); }); diff --git a/addons/mail/static/tests/chatter_tests.js b/addons/mail/static/tests/chatter_tests.js index 91b9a28661e..29df0733930 100644 --- a/addons/mail/static/tests/chatter_tests.js +++ b/addons/mail/static/tests/chatter_tests.js @@ -1034,6 +1034,8 @@ QUnit.test('chatter: receive notif when document is open', async function (asser assert.strictEqual(thread.getUnreadCounter(), 1, "document thread should now have one unread message"); + // do not destroy form too early. wait rendering to avoid race condition in service call. + await testUtils.nextTick(); form.destroy(); }); diff --git a/addons/mail/static/tests/mail_status_manager_tests.js b/addons/mail/static/tests/mail_status_manager_tests.js index be769e8d0b0..f21cefdec9e 100644 --- a/addons/mail/static/tests/mail_status_manager_tests.js +++ b/addons/mail/static/tests/mail_status_manager_tests.js @@ -39,7 +39,7 @@ QUnit.test('simple set im_status', function (assert) { }); QUnit.test('multi get_im_status', async function (assert) { - assert.expect(9); + assert.expect(8); var readCount = 0; var parent = testUtils.createParent({ //data: this.data, @@ -48,9 +48,8 @@ QUnit.test('multi get_im_status', async function (assert) { if (route === '/mail/init_messaging') { return this._super.apply(this, arguments); } - if (args.method === 'read' && args.model === 'res.partner') { - assert.deepEqual(args.args[0], [2,3]); - assert.deepEqual(args.args[1], ['id', 'im_status']); + if (route === '/longpolling/im_status') { + assert.deepEqual(args.partner_ids, [2,3]); readCount++; return Promise.resolve([ {id: 2, im_status: 'away'}, @@ -85,7 +84,7 @@ QUnit.test('multi get_im_status', async function (assert) { }); QUnit.test('update loop', async function (assert) { - assert.expect(12); + assert.expect(10); var readCount = 0; var parent = testUtils.createParent({ services: this.services, @@ -93,9 +92,8 @@ QUnit.test('update loop', async function (assert) { if (route === '/mail/init_messaging') { return this._super.apply(this, arguments); } - if (args.method === 'read' && args.model === 'res.partner') { - assert.deepEqual(args.args[0], [1, 2]); - assert.deepEqual(args.args[1], ['id', 'im_status']); + if (route === '/longpolling/im_status') { + assert.deepEqual(args.partner_ids, [1, 2]); readCount++; return Promise.resolve([ {"id": 1, "im_status": "online"}, diff --git a/addons/mail/wizard/mail_compose_message.py b/addons/mail/wizard/mail_compose_message.py index ba65f787257..def7b58033b 100644 --- a/addons/mail/wizard/mail_compose_message.py +++ b/addons/mail/wizard/mail_compose_message.py @@ -410,6 +410,7 @@ class MailComposer(models.TransientModel): values = self.generate_email_for_composer(template_id, [res_id])[res_id] # transform attachments into attachment_ids; not attached to the document because this will # be done further in the posting process, allowing to clean database if email not send + attachment_ids = [] Attachment = self.env['ir.attachment'] for attach_fname, attach_datas in values.pop('attachments', []): data_attach = { @@ -419,7 +420,9 @@ class MailComposer(models.TransientModel): 'res_id': 0, 'type': 'binary', # override default_type from context, possibly meant for another model! } - values.setdefault('attachment_ids', list()).append(Attachment.create(data_attach).id) + attachment_ids.append(Attachment.create(data_attach).id) + if values.get('attachment_ids', []) or attachment_ids: + values['attachment_ids'] = [(5,)] + values.get('attachment_ids', []) + attachment_ids else: default_values = self.with_context(default_composition_mode=composition_mode, default_model=model, default_res_id=res_id).default_get(['composition_mode', 'model', 'res_id', 'parent_id', 'partner_ids', 'subject', 'body', 'email_from', 'reply_to', 'attachment_ids', 'mail_server_id']) values = dict((key, default_values[key]) for key in ['subject', 'body', 'partner_ids', 'email_from', 'reply_to', 'attachment_ids', 'mail_server_id'] if key in default_values) diff --git a/addons/mass_mailing/models/mass_mailing.py b/addons/mass_mailing/models/mass_mailing.py index b4cdf4109fc..243fc33af0a 100644 --- a/addons/mass_mailing/models/mass_mailing.py +++ b/addons/mass_mailing/models/mass_mailing.py @@ -460,7 +460,7 @@ class MassMailing(models.Model): return res active = fields.Boolean(default=True) - subject = fields.Char('Subject', help='Subject of emails to send', required=True) + subject = fields.Char('Subject', help='Subject of emails to send', required=True, translate=True) email_from = fields.Char(string='From', required=True, default=lambda self: self.env['mail.message']._get_default_from()) sent_date = fields.Datetime(string='Sent Date', oldname='date', copy=False) diff --git a/addons/mass_mailing/wizard/test_mailing.py b/addons/mass_mailing/wizard/test_mailing.py index 8389ce269ef..7f2459d4a07 100644 --- a/addons/mass_mailing/wizard/test_mailing.py +++ b/addons/mass_mailing/wizard/test_mailing.py @@ -27,7 +27,7 @@ class TestMassMailing(models.TransientModel): 'email_from': mailing.email_from, 'reply_to': mailing.reply_to, 'email_to': test_mail, - 'subject': mailing.name, + 'subject': mailing.subject, 'body_html': mass_mail_layout.render({'body': body}, engine='ir.qweb', minimal_qcontext=True), 'notification': True, 'mailing_id': mailing.id, diff --git a/addons/mrp/models/mrp_production.py b/addons/mrp/models/mrp_production.py index 1705297358a..f8b6544a61b 100644 --- a/addons/mrp/models/mrp_production.py +++ b/addons/mrp/models/mrp_production.py @@ -492,6 +492,8 @@ class MrpProduction(models.Model): } def _generate_finished_moves(self): + if self.product_id in self.bom_id.byproduct_ids.mapped('product_id'): + raise UserError(_("You cannot have %s as the finished product and in the Byproducts") % self.product_id.name) moves_values = [self._get_finished_move_value(self.product_id.id, self.product_qty, self.product_uom_id.id)] for byproduct in self.bom_id.byproduct_ids: product_uom_factor = self.product_uom_id._compute_quantity(self.product_qty, self.bom_id.product_uom_id) @@ -835,6 +837,8 @@ class MrpProduction(models.Model): order._cal_price(moves_to_do) moves_to_finish = order.move_finished_ids.filtered(lambda x: x.state not in ('done', 'cancel')) moves_to_finish = moves_to_finish._action_done() + order.workorder_ids.mapped('raw_workorder_line_ids').unlink() + order.workorder_ids.mapped('finished_workorder_line_ids').unlink() order.action_assign() consume_move_lines = moves_to_do.mapped('move_line_ids') for moveline in moves_to_finish.mapped('move_line_ids'): diff --git a/addons/mrp/models/stock_warehouse.py b/addons/mrp/models/stock_warehouse.py index 8b6e0676bb6..e7fee7f0def 100644 --- a/addons/mrp/models/stock_warehouse.py +++ b/addons/mrp/models/stock_warehouse.py @@ -249,7 +249,7 @@ class StockWarehouse(models.Model): @api.multi def _get_all_routes(self): - routes = super(StockWarehouse, self).get_all_routes_for_wh() + routes = super(StockWarehouse, self)._get_all_routes() routes |= self.filtered(lambda self: self.manufacture_to_resupply and self.manufacture_pull_id and self.manufacture_pull_id.route_id).mapped('manufacture_pull_id').mapped('route_id') return routes diff --git a/addons/mrp/tests/test_workorder_operation.py b/addons/mrp/tests/test_workorder_operation.py index b214fd3ad76..f1953f0d2c0 100644 --- a/addons/mrp/tests/test_workorder_operation.py +++ b/addons/mrp/tests/test_workorder_operation.py @@ -863,6 +863,29 @@ class TestWorkOrderProcess(TestMrpCommon): self.assertAlmostEqual(workorder.date_planned_start, date_start, delta=timedelta(seconds=1), msg="Workorder should be planned tomorrow.") self.assertAlmostEqual(workorder.date_planned_finished, date_start + timedelta(hours=1), delta=timedelta(seconds=1), msg="Workorder should be done one hour later.") + def test_change_production_1(self): + """Change the quantity to produce on the MO while workorders are already planned.""" + dining_table = self.env.ref("mrp.product_product_computer_desk") + dining_table.tracking = 'lot' + production_table_form = Form(self.env['mrp.production']) + production_table_form.product_id = dining_table + production_table_form.bom_id = self.env.ref("mrp.mrp_bom_desk") + production_table_form.product_qty = 1.0 + production_table_form.product_uom_id = dining_table.uom_id + production_table = production_table_form.save() + production_table.action_confirm() + + # Create work order + production_table.button_plan() + + context = {'active_id': production_table.id, 'active_model': 'mrp.production'} + change_qty_form = Form(self.env['change.production.qty'].with_context(context)) + change_qty_form.product_qty = 2.00 + change_qty = change_qty_form.save() + change_qty.change_prod_qty() + + self.assertEqual(production_table.workorder_ids[0].qty_producing, 2, "Quantity to produce not updated") + def test_planning_0(self): """ Test alternative conditions 1. alternative relation is directionnal diff --git a/addons/mrp/wizard/change_production_qty.py b/addons/mrp/wizard/change_production_qty.py index 2757d9ec0bb..f4bb7746c37 100644 --- a/addons/mrp/wizard/change_production_qty.py +++ b/addons/mrp/wizard/change_production_qty.py @@ -106,7 +106,7 @@ class ChangeProductionQty(models.TransientModel): (moves_finished + moves_raw).write({'workorder_id': wo.id}) if wo.state not in ('done', 'cancel'): line_values = wo._update_workorder_lines() - self._workorder_line_ids().create(line_values['to_create']) + wo._workorder_line_ids().create(line_values['to_create']) if line_values['to_delete']: line_values['to_delete'].unlink() for line, vals in line_values['to_update'].items(): diff --git a/addons/payment/controllers/portal.py b/addons/payment/controllers/portal.py index a4da9dbbb34..90c0065b643 100644 --- a/addons/payment/controllers/portal.py +++ b/addons/payment/controllers/portal.py @@ -2,6 +2,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. import logging +import psycopg2 from odoo import http, _ from odoo.http import request @@ -40,7 +41,7 @@ class PaymentProcessing(http.Controller): # I prefer to let the controller chose when to access to payment.transaction using sudo return request.session.get("__payment_tx_ids__", []) - @http.route(['/payment/process'], type="http", auth="public", website=True) + @http.route(['/payment/process'], type="http", auth="public", website=True, sitemap=False) def payment_status_page(self, **kwargs): # When the customer is redirect to this website page, # we retrieve the payment transaction list from his session @@ -93,11 +94,15 @@ class PaymentProcessing(http.Controller): tx_to_process = payment_transaction_ids.filtered(lambda x: x.state == 'done' and x.is_processed is False) try: tx_to_process._post_process_after_done() + except psycopg2.OperationalError as e: + request.env.cr.rollback() + result['success'] = False + result['error'] = "tx_process_retry" except Exception as e: request.env.cr.rollback() result['success'] = False result['error'] = str(e) - _logger.error("Error while processing transaction(s) %s, exception \"%s\"", tx_to_process.ids, str(e)) + _logger.exception("Error while processing transaction(s) %s, exception \"%s\"", tx_to_process.ids, str(e)) return result @@ -122,7 +127,7 @@ class WebsitePayment(http.Controller): } return request.render("payment.pay_methods", values) - @http.route(['/website_payment/pay'], type='http', auth='public', website=True) + @http.route(['/website_payment/pay'], type='http', auth='public', website=True, sitemap=False) def pay(self, reference='', order_id=None, amount=False, currency_id=None, acquirer_id=None, **kw): env = request.env user = env.user.sudo() @@ -262,7 +267,7 @@ class WebsitePayment(http.Controller): except Exception as e: return request.redirect('/payment/process') - @http.route(['/website_payment/confirm'], type='http', auth='public', website=True) + @http.route(['/website_payment/confirm'], type='http', auth='public', website=True, sitemap=False) def confirm(self, **kw): tx_id = int(kw.get('tx_id', 0)) if tx_id: diff --git a/addons/payment/models/payment_acquirer.py b/addons/payment/models/payment_acquirer.py index 21320cc04b3..f79252c622f 100644 --- a/addons/payment/models/payment_acquirer.py +++ b/addons/payment/models/payment_acquirer.py @@ -917,7 +917,7 @@ class PaymentTransaction(models.Model): # custom create custom_method_name = '%s_create' % acquirer.provider - if hasattr(acquirer, custom_method_name): + if hasattr(self, custom_method_name): values.update(getattr(self, custom_method_name)(values)) if not values.get('reference'): diff --git a/addons/payment/static/src/js/payment_processing.js b/addons/payment/static/src/js/payment_processing.js index 4bad7fcb4ef..ec1028188ec 100644 --- a/addons/payment/static/src/js/payment_processing.js +++ b/addons/payment/static/src/js/payment_processing.js @@ -44,6 +44,8 @@ odoo.define('payment.processing', function (require) { } else { switch(data.error) { + case "tx_process_retry": + break; case "no_tx_found": self.displayContent("payment.no_tx_found", {}); break; @@ -53,14 +55,8 @@ odoo.define('payment.processing', function (require) { } } self.startPolling(); - }).then(function() { - if ($.blockUI) { - $.unblockUI(); - } + }).guardedCatch(function() { - if ($.blockUI) { - $.unblockUI(); - } self.displayContent("payment.rpc_error", {}); self.startPolling(); }); @@ -110,6 +106,7 @@ odoo.define('payment.processing', function (require) { }, displayContent: function (xmlid, render_values) { var html = core.qweb.render(xmlid, render_values); + $.unblockUI(); this.$el.find('.o_payment_processing_content').html(html); }, displayLoading: function () { diff --git a/addons/payment_authorize/models/authorize_request.py b/addons/payment_authorize/models/authorize_request.py index 79d0a7589c5..922d9df1bf1 100644 --- a/addons/payment_authorize/models/authorize_request.py +++ b/addons/payment_authorize/models/authorize_request.py @@ -226,6 +226,12 @@ class AuthorizeAPI(): etree.SubElement(customer, "email").text = partner.email or '' response = self._authorize_request(root) res = dict() + if not response.find('customerProfileId'): + _logger.warning( + 'Unable to create customer payment profile, data missing from transaction. Transaction_id: %s - Partner_id: %s' + % (transaction_id, partner) + ) + return res res['profile_id'] = response.find('customerProfileId').text res['payment_profile_id'] = response.find('customerPaymentProfileIdList/numericString').text root_profile = self._base_tree('getCustomerPaymentProfileRequest') diff --git a/addons/payment_authorize/models/payment.py b/addons/payment_authorize/models/payment.py index 5a6ed0bf8e1..946f031484d 100644 --- a/addons/payment_authorize/models/payment.py +++ b/addons/payment_authorize/models/payment.py @@ -228,17 +228,18 @@ class TxAuthorize(models.Model): (self.type == 'form_save' or self.acquirer_id.save_token == 'always'): transaction = AuthorizeAPI(self.acquirer_id) res = transaction.create_customer_profile_from_tx(self.partner_id, self.acquirer_reference) - token_id = self.env['payment.token'].create({ - 'authorize_profile': res.get('profile_id'), - 'name': res.get('name'), - 'acquirer_ref': res.get('payment_profile_id'), - 'acquirer_id': self.acquirer_id.id, - 'partner_id': self.partner_id.id, - }) - self.payment_token_id = token_id - - if self.payment_token_id: - self.payment_token_id.verified = True + if res: + token_id = self.env['payment.token'].create({ + 'authorize_profile': res.get('profile_id'), + 'name': res.get('name'), + 'acquirer_ref': res.get('payment_profile_id'), + 'acquirer_id': self.acquirer_id.id, + 'partner_id': self.partner_id.id, + }) + self.payment_token_id = token_id + + if self.payment_token_id: + self.payment_token_id.verified = True return True elif status_code == self._authorize_pending_tx_status: self.write({'acquirer_reference': data.get('x_trans_id')}) diff --git a/addons/point_of_sale/models/pos_order.py b/addons/point_of_sale/models/pos_order.py index 1362497f657..0fdbe554f19 100644 --- a/addons/point_of_sale/models/pos_order.py +++ b/addons/point_of_sale/models/pos_order.py @@ -327,6 +327,7 @@ class PosOrder(models.Model): 'partner_id': partner_id, 'order_id': order.id, 'tax_repartition_line_id': tax['tax_repartition_line_id'], + 'tax_base_amount': tax['base'], 'tag_ids': tax['tag_ids'], } if currency_id != cur_company: @@ -904,7 +905,7 @@ class PosOrder(models.Model): # a serialnumber always has a quantity of 1 product, a lot number takes the full quantity of the order line qty = 1.0 if stock_production_lot.product_id.tracking == 'lot': - qty = pos_pack_lot.pos_order_line_id.qty + qty = abs(pos_pack_lot.pos_order_line_id.qty) qty_done += qty pack_lots.append({'lot_id': stock_production_lot.id, 'qty': qty}) else: diff --git a/addons/point_of_sale/static/src/js/gui.js b/addons/point_of_sale/static/src/js/gui.js index 5c946406133..845233af3c5 100644 --- a/addons/point_of_sale/static/src/js/gui.js +++ b/addons/point_of_sale/static/src/js/gui.js @@ -404,7 +404,7 @@ var Gui = core.Class.extend({ } } else if (input === '-') { if (options.firstinput) { - newbuf = '-'; + newbuf = '-0'; } else if ( newbuf[0] === '-' ) { newbuf = newbuf.substring(1,newbuf.length); } else { @@ -419,6 +419,9 @@ var Gui = core.Class.extend({ newbuf += input; } } + if (newbuf === "-") { + newbuf = ""; + } // End of input buffer at 12 characters. if (newbuf.length > buffer.length && newbuf.length > 12) { diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js index bb1c74b9718..124042b3345 100644 --- a/addons/point_of_sale/static/src/js/models.js +++ b/addons/point_of_sale/static/src/js/models.js @@ -1447,7 +1447,7 @@ exports.Orderline = Backbone.Model.extend({ var lots_required = 1; if (this.product.tracking == 'serial') { - lots_required = this.quantity; + lots_required = Math.abs(this.quantity); } return lots_required; @@ -1995,8 +1995,11 @@ var PacklotlineCollection = Backbone.Collection.extend({ set_quantity_by_lot: function() { if (this.order_line.product.tracking == 'serial') { - var valid_lots = this.get_valid_lots(); - this.order_line.set_quantity(valid_lots.length); + var valid_lots_quantity = this.get_valid_lots().length; + if (this.order_line.quantity < 0){ + valid_lots_quantity = -valid_lots_quantity; + } + this.order_line.set_quantity(valid_lots_quantity); } } }); diff --git a/addons/point_of_sale/static/src/xml/pos.xml b/addons/point_of_sale/static/src/xml/pos.xml index 94b1d4e31f7..75eeb66b355 100644 --- a/addons/point_of_sale/static/src/xml/pos.xml +++ b/addons/point_of_sale/static/src/xml/pos.xml @@ -1002,7 +1002,7 @@ <line t-foreach='taxes' t-as='taxe'> <left><t t-esc='taxe.name' /></left> <right> - <t t-esc='widget.format_currency_no_symbol(taxe.total)'/> + <t t-esc='widget.format_currency_no_symbol(taxe.tax_amount)'/> </right> </line> diff --git a/addons/point_of_sale/views/point_of_sale_dashboard.xml b/addons/point_of_sale/views/point_of_sale_dashboard.xml index cd627937472..c8f1ebca3d4 100644 --- a/addons/point_of_sale/views/point_of_sale_dashboard.xml +++ b/addons/point_of_sale/views/point_of_sale_dashboard.xml @@ -47,7 +47,6 @@ <field name="arch" type="xml"> <kanban create="false" class="o_kanban_dashboard o_pos_kanban"> <field name="name"/> - <field name="session_ids"/> <field name="current_session_id"/> <field name="current_session_state"/> <field name="last_session_closing_date"/> diff --git a/addons/portal/controllers/mail.py b/addons/portal/controllers/mail.py index 39e83703a3c..7376e17521b 100644 --- a/addons/portal/controllers/mail.py +++ b/addons/portal/controllers/mail.py @@ -139,7 +139,7 @@ class PortalChatter(http.Controller): raise Forbidden() # Non-employee see only messages with not internal subtype (aka, no internal logs) if not request.env['res.users'].has_group('base.group_user'): - domain = expression.AND([['&', '&', ('subtype_id', '!=', False), ('subtype_id.internal', '=', False), ('website_published', '=', True)], domain]) + domain = expression.AND([Message._non_employee_message_domain(), domain]) Message = request.env['mail.message'].sudo() return { 'messages': Message.search(domain, limit=limit, offset=offset).portal_message_format(), diff --git a/addons/portal/models/mail_message.py b/addons/portal/models/mail_message.py index 4a5d1f804d9..afe97f1b8ca 100644 --- a/addons/portal/models/mail_message.py +++ b/addons/portal/models/mail_message.py @@ -21,3 +21,7 @@ class MailMessage(models.Model): message_tree = dict((m.id, m) for m in self.sudo()) self._message_read_dict_postprocess(message_values, message_tree) return message_values + + @api.model + def _non_employee_message_domain(self): + return ['&', ('subtype_id', '!=', False), ('subtype_id.internal', '=', False)] diff --git a/addons/pos_sale/report/sale_report.py b/addons/pos_sale/report/sale_report.py index 13f16a69c03..561d3698095 100644 --- a/addons/pos_sale/report/sale_report.py +++ b/addons/pos_sale/report/sale_report.py @@ -20,11 +20,6 @@ class SaleReport(models.Model): ('invoiced', 'Invoiced')], string='Status', readonly=True) def _query(self, with_clause='', fields={}, groupby='', from_clause=''): - with_clause += '''pol_tax AS (SELECT line_tax.pos_order_line_id AS pol_id,sum(tax.amount/100) AS amount - FROM account_tax_pos_order_line_rel line_tax - JOIN account_tax tax ON line_tax.account_tax_id = tax.id - GROUP BY line_tax.pos_order_line_id)''' - res = super(SaleReport, self)._query(with_clause, fields, groupby, from_clause) select_ = ''' @@ -35,10 +30,10 @@ class SaleReport(models.Model): sum(l.qty * u.factor) AS qty_delivered, CASE WHEN pos.state = 'invoiced' THEN sum(qty) ELSE 0 END AS qty_invoiced, CASE WHEN pos.state != 'invoiced' THEN sum(qty) ELSE 0 END AS qty_to_invoice, - (COALESCE(COALESCE((l.price_unit * l.qty * u.factor * ((1 - COALESCE(l.discount / 100, 0)) )) + (l.price_unit * l.qty * u.factor * ((1 - COALESCE(l.discount / 100, 0)) ) * pol_tax.amount), (l.price_unit * l.qty * u.factor * ((1 - COALESCE(l.discount / 100, 0)) ))), 1.0)) / MIN(CASE COALESCE(pos.currency_rate, 0) WHEN 0 THEN 1.0 ELSE pos.currency_rate END) AS price_total, - (COALESCE((l.qty * u.factor * l.price_unit * ((1 - COALESCE(l.discount / 100, 0)) )), 1.0)) / MIN(CASE COALESCE(pos.currency_rate, 0) WHEN 0 THEN 1.0 ELSE pos.currency_rate END) AS price_subtotal, - (CASE WHEN pos.state != 'invoiced' THEN ((l.price_unit * l.qty * u.factor * ((1 - COALESCE(l.discount / 100, 0)))) + (l.price_unit * l.qty * u.factor * (1 - COALESCE(l.discount / 100, 0)) * pol_tax.amount)) ELSE 0 END) / MIN(CASE COALESCE(pos.currency_rate, 0) WHEN 0 THEN 1.0 ELSE pos.currency_rate END) AS amount_to_invoice, - (CASE WHEN pos.state = 'invoiced' THEN ((l.price_unit * l.qty * u.factor * ((1 - COALESCE(l.discount / 100, 0)) )) + (l.price_unit * l.qty * u.factor * (1 - COALESCE(l.discount / 100, 0)) * pol_tax.amount)) ELSE 0 END) / MIN(CASE COALESCE(pos.currency_rate, 0) WHEN 0 THEN 1.0 ELSE pos.currency_rate END) AS amount_invoiced, + SUM(l.price_subtotal_incl) / MIN(CASE COALESCE(pos.currency_rate, 0) WHEN 0 THEN 1.0 ELSE pos.currency_rate END) AS price_total, + SUM(l.price_subtotal) / MIN(CASE COALESCE(pos.currency_rate, 0) WHEN 0 THEN 1.0 ELSE pos.currency_rate END) AS price_subtotal, + (CASE WHEN pos.state != 'invoiced' THEN SUM(l.price_subtotal_incl) ELSE 0 END) / MIN(CASE COALESCE(pos.currency_rate, 0) WHEN 0 THEN 1.0 ELSE pos.currency_rate END) AS amount_to_invoice, + (CASE WHEN pos.state = 'invoiced' THEN SUM(l.price_subtotal_incl) ELSE 0 END) / MIN(CASE COALESCE(pos.currency_rate, 0) WHEN 0 THEN 1.0 ELSE pos.currency_rate END) AS amount_invoiced, count(*) AS nbr, pos.name AS name, pos.date_order AS date, @@ -79,7 +74,6 @@ class SaleReport(models.Model): pos_order_line l join pos_order pos on (l.order_id=pos.id) left join res_partner partner ON (pos.partner_id = partner.id OR pos.partner_id = NULL) - left join pol_tax pol_tax on pol_tax.pol_id=l.id left join product_product p on (l.product_id=p.id) left join product_template t on (p.product_tmpl_id=t.id) LEFT JOIN uom_uom u ON (u.id=t.uom_id) @@ -107,7 +101,6 @@ class SaleReport(models.Model): partner.country_id, partner.industry_id, partner.commercial_partner_id, - pol_tax.amount, u.factor, config.crm_team_id ''' diff --git a/addons/product/models/product.py b/addons/product/models/product.py index a7cc6779f34..a8df285d257 100644 --- a/addons/product/models/product.py +++ b/addons/product/models/product.py @@ -196,6 +196,7 @@ class ProductProduct(models.Model): ('active', '=', True), ]) <= 1 ): + record.image_raw_original = False record.product_tmpl_id.image_original = record.image_original else: record.image_raw_original = record.image_original @@ -391,6 +392,16 @@ class ProductProduct(models.Model): product._set_standard_price(vals.get('standard_price') or 0.0) # `_get_variant_id_for_combination` depends on existing variants self.clear_caches() + self.env['product.template'].invalidate_cache( + fnames=[ + 'valid_archived_variant_ids', + 'valid_existing_variant_ids', + 'product_variant_ids', + 'product_variant_id', + 'product_variant_count' + ], + ids=products.mapped('product_tmpl_id').ids + ) return products @api.multi diff --git a/addons/product/models/product_template.py b/addons/product/models/product_template.py index dbbb3d488f9..d09975cc4da 100644 --- a/addons/product/models/product_template.py +++ b/addons/product/models/product_template.py @@ -135,8 +135,10 @@ class ProductTemplate(models.Model): compute="_compute_valid_attributes", string='Valid Product Attribute Values Without No Variant Attributes', help="Technical compute") valid_product_attribute_wnva_ids = fields.Many2many('product.attribute', compute="_compute_valid_attributes", string='Valid Product Attributes Without No Variant Attributes', help="Technical compute") + # valid_archived_variant_ids deprecated valid_archived_variant_ids = fields.Many2many('product.product', - compute="_compute_valid_attributes", string='Valid Archived Variants', help="Technical compute") + compute="_compute_valid_archived_variant_ids", string='Valid Archived Variants', help="Technical compute") + # valid_existing_variant_ids deprecated valid_existing_variant_ids = fields.Many2many('product.product', compute="_compute_valid_existing_variant_ids", string='Valid Existing Variants', help="Technical compute") @@ -606,8 +608,6 @@ class ProductTemplate(models.Model): For what is considered an archived variant, see `_has_valid_attributes`. """ - archived_variants = self.env['product.product'].search([('product_tmpl_id', 'in', self.ids), ('active', '=', False)]) - # prefetch self.mapped('attribute_line_ids.value_ids.id') self.mapped('attribute_line_ids.attribute_id.create_variant') @@ -622,6 +622,12 @@ class ProductTemplate(models.Model): record.valid_product_attribute_ids = record.valid_product_template_attribute_line_ids.mapped('attribute_id') record.valid_product_attribute_wnva_ids = record.valid_product_template_attribute_line_wnva_ids.mapped('attribute_id') + @api.multi + def _compute_valid_archived_variant_ids(self): + """This compute is done outside of `_compute_valid_attributes` because + it is often not needed, and it can be very bad on performance.""" + archived_variants = self.env['product.product'].search([('product_tmpl_id', 'in', self.ids), ('active', '=', False)]) + for record in self: valid_value_ids = record.valid_product_attribute_value_wnva_ids valid_attribute_ids = record.valid_product_attribute_wnva_ids @@ -699,14 +705,12 @@ class ProductTemplate(models.Model): :return: dict of exclusions - exclusions: from this product itself + - parent_combination: ids of the given parent_combination - parent_exclusions: from the parent_combination - - archived_combinations: variants that are archived - - existing_combinations: variants that are existing (as opposed to - deleted, because deleted need to be considered impossible if - there are no dynamic attributes). - - has_dynamic_attributes: whether there is a dynamic attribute - - no_variant_product_template_attribute_value_ids: values that are - no_variant + - archived_combinations: deprecated + - existing_combinations: deprecated + - has_dynamic_attributes: deprecated + - no_variant_product_template_attribute_value_ids: deprecated - parent_product_name: the name of the parent product if any, used in the interface to explain why some combinations are not available. (e.g: Not available with Customizable Desk (Legs: Steel)) @@ -715,13 +719,15 @@ class ProductTemplate(models.Model): (e.g: Not available with Color: Black) """ self.ensure_one() + parent_combination = parent_combination or self.env['product.template.attribute.value'] return { 'exclusions': self._complete_inverse_exclusions(self._get_own_attribute_exclusions()), 'parent_exclusions': self._get_parent_attribute_exclusions(parent_combination), - 'archived_combinations': self._get_archived_combinations(), + 'parent_combination': parent_combination.ids, + 'archived_combinations': [], 'has_dynamic_attributes': self.has_dynamic_attributes(), - 'existing_combinations': self._get_existing_combinations(), - 'no_variant_product_template_attribute_value_ids': self._get_no_variant_product_template_attribute_values(), + 'existing_combinations': [], + 'no_variant_product_template_attribute_value_ids': [], 'parent_product_name': parent_name, 'mapped_attribute_names': self._get_mapped_attribute_names(parent_combination), } @@ -789,29 +795,21 @@ class ProductTemplate(models.Model): @api.multi def _get_archived_combinations(self): - """Get archived combinations. - - Array, each element is an array with ids of an archived combination. - """ + """Deprecated""" self.ensure_one() return [archived_variant.product_template_attribute_value_ids.ids for archived_variant in self.valid_archived_variant_ids] @api.multi def _get_existing_combinations(self): - """Get existing combinations. - - Needed because when not using dynamic attributes, the combination is - not ok if it doesn't exist (= if the variant has been deleted). - - Array, each element is an array with ids of an existing combination. - """ + """Deprecated""" self.ensure_one() return [variant.product_template_attribute_value_ids.ids for variant in self.valid_existing_variant_ids] @api.multi def _get_no_variant_product_template_attribute_values(self): + """Deprecated""" self.ensure_one() product_template_attribute_values = self._get_valid_product_template_attribute_lines().mapped('product_template_value_ids') return product_template_attribute_values.filtered( @@ -876,9 +874,16 @@ class ProductTemplate(models.Model): # combination has different values than the ones configured on the template return False - if not self.has_dynamic_attributes() and not self._get_variant_for_combination(combination): - # the variant has been deleted - return False + variant = self._get_variant_for_combination(combination) + + if self.has_dynamic_attributes(): + if variant and not variant.active: + # dynamic and the variant has been archived + return False + else: + if not variant or not variant.active: + # not dynamic, the variant has been archived or deleted + return False exclusions = self._get_own_attribute_exclusions() if exclusions: @@ -898,11 +903,6 @@ class ProductTemplate(models.Model): if exclusion in combination.ids: return False - filtered_combination = combination._without_no_variant_attributes() - archived_combinations = self._get_archived_combinations() - if archived_combinations and filtered_combination.ids in archived_combinations: - return False - return True @api.multi @@ -936,7 +936,7 @@ class ProductTemplate(models.Model): for pav in attribute_values: domain = expression.AND([[('attribute_value_ids', 'in', pav.id)], domain]) - res = self.env['product.product'].with_context(active_test=False).search(domain) + res = self.env['product.product'].with_context(active_test=False).search(domain, order='active DESC') # The domain above is checking for the `product.attribute.value`, but we # need to make sure it's the same `product.template.attribute.value`. diff --git a/addons/product/tests/test_product_attribute_value_config.py b/addons/product/tests/test_product_attribute_value_config.py index e966adbdbdc..497f6fbeba3 100644 --- a/addons/product/tests/test_product_attribute_value_config.py +++ b/addons/product/tests/test_product_attribute_value_config.py @@ -312,6 +312,21 @@ class TestProductAttributeValueConfig(TestProductAttributeValueSetup): variant.unlink() self.assertFalse(self.computer._is_combination_possible(computer_ssd_256 + computer_ram_8 + computer_hdd_1)) + # CASE: if multiple variants exist for the same combination and at least + # one of them is not archived, the combination is possible + values = self.ssd_256 + self.ram_8 + self.hdd_1 + self.env['product.product'].create({ + 'product_tmpl_id': self.computer.id, + 'attribute_value_ids': [(6, 0, values.ids)], + 'active': False, + }) + self.env['product.product'].create({ + 'product_tmpl_id': self.computer.id, + 'attribute_value_ids': [(6, 0, values.ids)], + 'active': True, + }) + self.assertTrue(self.computer._is_combination_possible(computer_ssd_256 + computer_ram_8 + computer_hdd_1)) + def test_get_first_possible_combination(self): computer_ssd_256 = self._get_product_template_attribute_value(self.ssd_256) computer_ssd_512 = self._get_product_template_attribute_value(self.ssd_512) diff --git a/addons/product/tests/test_variants.py b/addons/product/tests/test_variants.py index 9381868b623..ed7f1f777c5 100644 --- a/addons/product/tests/test_variants.py +++ b/addons/product/tests/test_variants.py @@ -1,6 +1,10 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +import base64 +import io +from PIL import Image + from . import common from odoo.exceptions import UserError from odoo.tests.common import TransactionCase @@ -461,3 +465,72 @@ class TestVariantsManyAttributes(common.TestAttributesCommon): self.assertEqual(len(toto.attribute_line_ids.mapped('attribute_id')), 10) self.assertEqual(len(toto.attribute_line_ids.mapped('value_ids')), 100) self.assertEqual(len(toto.product_variant_ids), 0) + + +class TestVariantsImages(common.TestProductCommon): + + def setUp(self): + res = super(TestVariantsImages, self).setUp() + + self.colors = {'red': '#FF0000', 'green': '#00FF00', 'blue': '#0000FF'} + self.images = {} + + product_attribute = self.env['product.attribute'].create({'name': 'Color'}) + + self.template = self.env['product.template'].create({ + 'name': 'template', + }) + + for color in self.colors: + attr = self.env['product.attribute.value'].create({ + 'name': color, + 'attribute_id': product_attribute.id, + }) + + f = io.BytesIO() + Image.new('RGB', (800, 500), self.colors[color]).save(f, 'PNG') + f.seek(0) + self.images.update({color: base64.b64encode(f.read())}) + + self.env['product.product'].create({ + 'image_raw_original': self.images[color], + 'attribute_value_ids': [(6, 0, [attr.id])], + 'product_tmpl_id': self.template.id, + }) + # the first one has no image, no color + self.variants = self.template.product_variant_ids.sorted('id') + + return res + + def test_variant_images(self): + """Check that on variant, the image used is the image_raw_original if set, + and defaults to the template image otherwise. + """ + f = io.BytesIO() + Image.new('RGB', (800, 500), '#000000').save(f, 'PNG') + f.seek(0) + image_black = base64.b64encode(f.read()) + + images = self.variants.mapped('image_original') + self.assertEqual(len(set(images)), 4) + + variant_no_image = self.variants[0] + self.assertFalse(variant_no_image.image) + self.template.image = image_black + + # the first has no image variant, all the others do + self.assertFalse(variant_no_image.image_raw_original) + self.assertTrue(all(images[1:])) + + # template image is the same as this one, since it has no image variant + self.assertEqual(variant_no_image.image, self.template.image) + # having changed the template image should not have changed these + self.assertEqual(images[1:], self.variants.mapped('image')[1:]) + + def test_update_images_with_archived_variants(self): + """Update images after variants have been archived""" + self.variants[1:].write({'active': False}) + self.variants[0].image = self.images['red'] + self.assertEqual(self.template.image, self.images['red']) + self.assertEqual(self.variants[0].image_raw_original, False) + self.assertEqual(self.variants[0].image, self.images['red']) diff --git a/addons/product_email_template/models/product.py b/addons/product_email_template/models/product.py index 9ef844e8f46..e043717e6e8 100644 --- a/addons/product_email_template/models/product.py +++ b/addons/product_email_template/models/product.py @@ -11,4 +11,6 @@ class ProductTemplate(models.Model): _inherit = "product.template" email_template_id = fields.Many2one('mail.template', string='Product Email Template', - help='Send a product-specific email once the invoice is paid.') + help='When validating an invoice, an email will be sent to the customer ' + 'based on this template. The customer will receive an email for each ' + 'product linked to an email template.') diff --git a/addons/product_email_template/views/product_views.xml b/addons/product_email_template/views/product_views.xml index 393ab6d3ec9..9afb300f64f 100644 --- a/addons/product_email_template/views/product_views.xml +++ b/addons/product_email_template/views/product_views.xml @@ -10,7 +10,7 @@ <attribute name="invisible">0</attribute> </xpath> <xpath expr="//group[@name='email_template_and_project']" position="inside"> - <field name="email_template_id" string="Email Template" help="Send a product-specific email once the invoice is paid" + <field name="email_template_id" string="Email Template" help="Send a product-specific email once the invoice is validated" domain="[('model','=','product.template')]" context="{ 'form_view_ref':'product_email_template.email_template_form_simplified', diff --git a/addons/purchase/models/account_invoice.py b/addons/purchase/models/account_invoice.py index 150c5c02cb9..74811a27436 100644 --- a/addons/purchase/models/account_invoice.py +++ b/addons/purchase/models/account_invoice.py @@ -103,11 +103,6 @@ class AccountInvoice(models.Model): #as there's no invoice line yet, we keep the currency of the PO self.currency_id = self.purchase_id.currency_id - vendor_ref = self.purchase_id.partner_ref - if vendor_ref: - self.reference = ", ".join([self.reference, vendor_ref]) if ( - self.reference and vendor_ref not in self.reference) else vendor_ref - new_lines = self.env['account.invoice.line'] for line in self.purchase_id.order_line - self.invoice_line_ids.mapped('purchase_line_id'): data = self._prepare_invoice_line_from_po_line(line) diff --git a/addons/purchase_stock/models/account_invoice.py b/addons/purchase_stock/models/account_invoice.py index c6d138352cf..a98e0dda26f 100644 --- a/addons/purchase_stock/models/account_invoice.py +++ b/addons/purchase_stock/models/account_invoice.py @@ -96,27 +96,27 @@ class AccountInvoice(models.Model): interim_account_price = valuation_price_unit * line['quantity'] invoice_cur_prec = inv.currency_id.decimal_places - if float_compare(valuation_price_unit, i_line.price_unit, precision_digits=invoice_cur_prec) != 0 and float_compare(line['price_unit'], i_line.price_unit, precision_digits=invoice_cur_prec) == 0: - - # price with discount and without tax included - price_unit = i_line.price_unit * (1 - (i_line.discount or 0.0) / 100.0) - tax_ids = [] - if line['tax_ids']: - #line['tax_ids'] is -2many commands list for account.move.line - tax_ids = [tax_data[id] for tax_data in self.env['account.move.line'].resolve_2many_commands('tax_ids', line['tax_ids'], ['id'])] - taxes = self.env['account.tax'].browse(tax_ids) - price_unit = taxes.compute_all(price_unit, currency=inv.currency_id, quantity=1.0, is_refund=inv.type in ('in_refund', 'out_refund'))['total_excluded'] - for tax in taxes: - tax_ids.append((4, tax.id, None)) - for child in tax.children_tax_ids: - if child.type_tax_use != 'none': - tax_ids.append((4, child.id, None)) - + # price with discount and without tax included + price_unit = i_line.price_unit * (1 - (i_line.discount or 0.0) / 100.0) + tax_ids = [] + if line['tax_ids']: + #line['tax_ids'] is -2many commands list for account.move.line + tax_ids = [tax_data[id] for tax_data in self.env['account.move.line'].resolve_2many_commands('tax_ids', line['tax_ids'], ['id'])] + taxes = self.env['account.tax'].browse(tax_ids) + price_unit = taxes.compute_all(price_unit, currency=inv.currency_id, quantity=1.0, is_refund=inv.type in ('in_refund', 'out_refund'))['total_excluded'] + + for tax in taxes: + tax_ids.append((4, tax.id, None)) + for child in tax.children_tax_ids: + if child.type_tax_use != 'none': + tax_ids.append((4, child.id, None)) + + if float_compare(valuation_price_unit, price_unit, precision_digits=invoice_cur_prec) != 0 and float_compare(line['price_unit'], i_line.price_unit, precision_digits=invoice_cur_prec) == 0: price_before = line.get('price', 0.0) price_unit_val_dif = price_unit - valuation_price_unit price_val_dif = price_before - interim_account_price - if inv.currency_id.compare_amounts(i_line.price_unit, valuation_price_unit) != 0 and acc: + if inv.currency_id.compare_amounts(price_unit, valuation_price_unit) != 0 and acc: # If the unit prices have not changed and we have a # valuation difference, it means this difference is due to exchange rates, # so we don't create anything, the exchange rate entries will @@ -134,9 +134,12 @@ class AccountInvoice(models.Model): 'tax_ids': tax_ids, } # We update the original line accordingly - line['price_unit'] = line['price_unit'] - diff_line['price_unit'] - line['price'] = inv.currency_id.round(line['quantity'] * line['price_unit']) - line['price_unit'] = inv.currency_id.round(line['price_unit']) + # line['price_unit'] doesn't contain the discount, so use price_unit + # instead. It could make sense to include the discount in line['price_unit'], + # but that doesn't seem a good idea in stable since it is done in + # "invoice_line_move_line_get" of "account.invoice". + line['price_unit'] = inv.currency_id.round(price_unit - diff_line['price_unit']) + line['price'] = inv.currency_id.round(line['price'] - diff_line['price']) diff_res.append(diff_line) return diff_res return [] diff --git a/addons/purchase_stock/models/purchase.py b/addons/purchase_stock/models/purchase.py index 83405a9dad9..c652940b2e9 100644 --- a/addons/purchase_stock/models/purchase.py +++ b/addons/purchase_stock/models/purchase.py @@ -352,7 +352,9 @@ class PurchaseOrderLine(models.Model): for move in self.move_ids.filtered(lambda x: x.state != 'cancel' and not x.location_dest_id.usage == "supplier"): qty += move.product_uom._compute_quantity(move.product_uom_qty, self.product_uom, rounding_method='HALF-UP') template = { - 'name': self.name or '', + # truncate to 2000 to avoid triggering index limit error + # TODO: remove index in master? + 'name': (self.name or '')[:2000], 'product_id': self.product_id.id, 'product_uom': self.product_uom.id, 'date': self.order_id.date_order, diff --git a/addons/purchase_stock/models/stock.py b/addons/purchase_stock/models/stock.py index 61e285db200..259035e4bd6 100644 --- a/addons/purchase_stock/models/stock.py +++ b/addons/purchase_stock/models/stock.py @@ -132,7 +132,7 @@ class StockWarehouse(models.Model): @api.multi def _get_all_routes(self): - routes = super(StockWarehouse, self).get_all_routes_for_wh() + routes = super(StockWarehouse, self)._get_all_routes() routes |= self.filtered(lambda self: self.buy_to_resupply and self.buy_pull_id and self.buy_pull_id.route_id).mapped('buy_pull_id').mapped('route_id') return routes diff --git a/addons/purchase_stock/tests/test_stockvaluation.py b/addons/purchase_stock/tests/test_stockvaluation.py index 69208fd9080..8fcbabed1ad 100644 --- a/addons/purchase_stock/tests/test_stockvaluation.py +++ b/addons/purchase_stock/tests/test_stockvaluation.py @@ -1165,3 +1165,151 @@ class TestStockValuationWithCOA(AccountingTestCase): exchange_stock_line = reconciled_lines.filtered(lambda l: l.journal_id == exhange_diff_journal) self.assertEqual(exchange_stock_line.amount_currency, 0.00) self.assertAlmostEqual(exchange_stock_line.balance, 119.32) + + def test_anglosaxon_valuation_price_total_diff_discount(self): + """ + PO: price unit: 110 + Inv: price unit: 100 + discount: 10 + """ + self.env.company.anglo_saxon_accounting = True + self.product1.categ_id.property_cost_method = 'fifo' + self.product1.categ_id.property_valuation = 'real_time' + self.product1.product_tmpl_id.invoice_policy = 'delivery' + self.product1.property_account_creditor_price_difference = self.price_diff_account + + # Create PO + po_form = Form(self.env['purchase.order']) + po_form.partner_id = self.partner_id + with po_form.order_line.new() as po_line: + po_line.product_id = self.product1 + po_line.product_qty = 1 + po_line.price_unit = 110.0 + order = po_form.save() + order.button_confirm() + + # Receive the goods + receipt = order.picking_ids[0] + receipt.move_lines.quantity_done = 1 + receipt.button_validate() + + # Create an invoice with a different price and a discount + invoice = self.env['account.invoice'].create({ + 'partner_id': order.partner_id.id, + 'purchase_id': order.id, + 'account_id': order.partner_id.property_account_payable_id.id, + 'type': 'in_invoice', + }) + invoice.purchase_order_change() + invoice.invoice_line_ids[0].price_unit = 100.0 + invoice.invoice_line_ids[0].discount = 10.0 + invoice.action_invoice_open() + + # Check what was posted in the price difference account + price_diff_aml = self.env['account.move.line'].search([('account_id','=', self.price_diff_account.id)]) + self.assertEquals(len(price_diff_aml), 1, "Only one line should have been generated in the price difference account.") + self.assertAlmostEquals(price_diff_aml.credit, 20, "Price difference should be equal to 20 (110-90)") + + # Check what was posted in stock input account + input_aml = self.env['account.move.line'].search([('account_id','=', self.stock_input_account.id)]) + self.assertEquals(len(input_aml), 2, "Only two lines should have been generated in stock input account: one when receiving the product, one when making the invoice.") + self.assertAlmostEquals(sum(input_aml.mapped('debit')), 110, "Total debit value on stock input account should be equal to the original PO price of the product.") + self.assertAlmostEquals(sum(input_aml.mapped('credit')), 110, "Total credit value on stock input account should be equal to the original PO price of the product.") + + def test_anglosaxon_valuation_discount(self): + """ + PO: price unit: 100 + Inv: price unit: 100 + discount: 10 + """ + self.env.company.anglo_saxon_accounting = True + self.product1.categ_id.property_cost_method = 'fifo' + self.product1.categ_id.property_valuation = 'real_time' + self.product1.product_tmpl_id.invoice_policy = 'delivery' + self.product1.property_account_creditor_price_difference = self.price_diff_account + + # Create PO + po_form = Form(self.env['purchase.order']) + po_form.partner_id = self.partner_id + with po_form.order_line.new() as po_line: + po_line.product_id = self.product1 + po_line.product_qty = 1 + po_line.price_unit = 100.0 + order = po_form.save() + order.button_confirm() + + # Receive the goods + receipt = order.picking_ids[0] + receipt.move_lines.quantity_done = 1 + receipt.button_validate() + + # Create an invoice with a different price and a discount + invoice = self.env['account.invoice'].create({ + 'partner_id': order.partner_id.id, + 'purchase_id': order.id, + 'account_id': order.partner_id.property_account_payable_id.id, + 'type': 'in_invoice', + }) + invoice.purchase_order_change() + invoice.invoice_line_ids[0].discount = 10.0 + invoice.action_invoice_open() + + # Check what was posted in the price difference account + price_diff_aml = self.env['account.move.line'].search([('account_id','=', self.price_diff_account.id)]) + self.assertEquals(len(price_diff_aml), 1, "Only one line should have been generated in the price difference account.") + self.assertAlmostEquals(price_diff_aml.credit, 10, "Price difference should be equal to 20 (110-90)") + + # Check what was posted in stock input account + input_aml = self.env['account.move.line'].search([('account_id','=', self.stock_input_account.id)]) + self.assertEquals(len(input_aml), 2, "Only two lines should have been generated in stock input account: one when receiving the product, one when making the invoice.") + self.assertAlmostEquals(sum(input_aml.mapped('debit')), 100, "Total debit value on stock input account should be equal to the original PO price of the product.") + self.assertAlmostEquals(sum(input_aml.mapped('credit')), 100, "Total credit value on stock input account should be equal to the original PO price of the product.") + + def test_anglosaxon_valuation_price_unit_diff_discount(self): + """ + PO: price unit: 90 + Inv: price unit: 100 + discount: 10 + """ + self.env.company.anglo_saxon_accounting = True + self.product1.categ_id.property_cost_method = 'fifo' + self.product1.categ_id.property_valuation = 'real_time' + self.product1.product_tmpl_id.invoice_policy = 'delivery' + self.product1.property_account_creditor_price_difference = self.price_diff_account + + # Create PO + po_form = Form(self.env['purchase.order']) + po_form.partner_id = self.partner_id + with po_form.order_line.new() as po_line: + po_line.product_id = self.product1 + po_line.product_qty = 1 + po_line.price_unit = 90.0 + order = po_form.save() + order.button_confirm() + + # Receive the goods + receipt = order.picking_ids[0] + receipt.move_lines.quantity_done = 1 + receipt.button_validate() + + # Create an invoice with a different price and a discount + invoice = self.env['account.invoice'].create({ + 'partner_id': order.partner_id.id, + 'purchase_id': order.id, + 'account_id': order.partner_id.property_account_payable_id.id, + 'type': 'in_invoice', + }) + invoice.purchase_order_change() + invoice.invoice_line_ids[0].price_unit = 100.0 + invoice.invoice_line_ids[0].discount = 10.0 + invoice.action_invoice_open() + + # Check if something was posted in the price difference account + price_diff_aml = self.env['account.move.line'].search([('account_id','=', self.price_diff_account.id)]) + self.assertEquals(len(price_diff_aml), 0, "No line should have been generated in the price difference account.") + + # Check what was posted in stock input account + input_aml = self.env['account.move.line'].search([('account_id','=', self.stock_input_account.id)]) + self.assertEquals(len(input_aml), 2, "Only two lines should have been generated in stock input account: one when receiving the product, one when making the invoice.") + self.assertAlmostEquals(sum(input_aml.mapped('debit')), 90, "Total debit value on stock input account should be equal to the original PO price of the product.") + self.assertAlmostEquals(sum(input_aml.mapped('credit')), 90, "Total credit value on stock input account should be equal to the original PO price of the product.") diff --git a/addons/sale/controllers/variant.py b/addons/sale/controllers/variant.py index 7a4ee7ea7c3..3afdc19bd8d 100644 --- a/addons/sale/controllers/variant.py +++ b/addons/sale/controllers/variant.py @@ -13,7 +13,18 @@ class VariantController(http.Controller): ProductTemplate = request.env['product.template'] if 'context' in kw: ProductTemplate = ProductTemplate.with_context(**kw.get('context')) - return ProductTemplate.browse(int(product_template_id))._get_combination_info(combination, int(product_id or 0), int(add_qty or 1), pricelist) + product_template = ProductTemplate.browse(int(product_template_id)) + res = product_template._get_combination_info(combination, int(product_id or 0), int(add_qty or 1), pricelist) + if 'parent_combination' in kw: + parent_combination = request.env['product.template.attribute.value'].browse(kw.get('parent_combination')) + if not combination.exists() and product_id: + product = request.env['product.product'].browse(int(product_id)) + if product.exists(): + combination = product.product_template_attribute_value_ids + res.update({ + 'is_combination_possible': product_template._is_combination_possible(combination=combination, parent_combination=parent_combination), + }) + return res @http.route(['/sale/create_product_variant'], type='json', auth="user", methods=['POST']) def create_product_variant(self, product_template_id, product_template_attribute_value_ids, **kwargs): diff --git a/addons/sale/models/account_invoice.py b/addons/sale/models/account_invoice.py index f439341bd29..235378d03e3 100644 --- a/addons/sale/models/account_invoice.py +++ b/addons/sale/models/account_invoice.py @@ -44,7 +44,7 @@ class AccountInvoice(models.Model): inv_type = self.type or self.env.context.get('type', 'out_invoice') if inv_type == 'out_invoice': company = self.company_id or self.env.company - self.comment = company.with_context(lang=self.partner_id.lang).invoice_terms + self.comment = company.with_context(lang=self.partner_id.lang).invoice_terms or (self._origin.company_id == company and self.comment) @api.multi def _prepare_refund(self, invoice, date_invoice=None, date=None, description=None, journal_id=None): diff --git a/addons/sale/models/payment.py b/addons/sale/models/payment.py index 8580878a45d..e89f4b78a2a 100644 --- a/addons/sale/models/payment.py +++ b/addons/sale/models/payment.py @@ -99,11 +99,11 @@ class PaymentTransaction(models.Model): if self.env['ir.config_parameter'].sudo().get_param('sale.automatic_invoice'): default_template = self.env['ir.config_parameter'].sudo().get_param('sale.default_email_template') if default_template: - ctx_company = {'company_id': self.acquirer_id.company_id.id, - 'force_company': self.acquirer_id.company_id.id, - 'mark_invoice_as_sent': True, - } for trans in self.filtered(lambda t: t.sale_order_ids): + ctx_company = {'company_id': trans.acquirer_id.company_id.id, + 'force_company': trans.acquirer_id.company_id.id, + 'mark_invoice_as_sent': True, + } trans = trans.with_context(ctx_company) for invoice in trans.invoice_ids: invoice.message_post_with_template(int(default_template), email_layout_xmlid="mail.mail_notification_paynow") @@ -112,9 +112,9 @@ class PaymentTransaction(models.Model): @api.multi def _invoice_sale_orders(self): if self.env['ir.config_parameter'].sudo().get_param('sale.automatic_invoice'): - ctx_company = {'company_id': self.acquirer_id.company_id.id, - 'force_company': self.acquirer_id.company_id.id} for trans in self.filtered(lambda t: t.sale_order_ids): + ctx_company = {'company_id': trans.acquirer_id.company_id.id, + 'force_company': trans.acquirer_id.company_id.id} trans = trans.with_context(**ctx_company) trans.sale_order_ids._force_lines_to_invoice_policy_order() invoices = trans.sale_order_ids._create_invoices() diff --git a/addons/sale/models/sale.py b/addons/sale/models/sale.py index 63d219df0c8..fc6197723bc 100644 --- a/addons/sale/models/sale.py +++ b/addons/sale/models/sale.py @@ -1270,7 +1270,7 @@ class SaleOrderLine(models.Model): result.setdefault(so_line_id, 0.0) uom = product_uom_map.get(item['product_uom_id'][0]) if so_line.product_uom.category_id == uom.category_id: - qty = uom._compute_quantity(item['unit_amount'], so_line.product_uom) + qty = uom._compute_quantity(item['unit_amount'], so_line.product_uom, rounding_method='HALF-UP') else: qty = item['unit_amount'] result[so_line_id] += qty @@ -1624,7 +1624,7 @@ class SaleOrderLine(models.Model): new_list_price, self.order_id.pricelist_id.currency_id, self.order_id.company_id, self.order_id.date_order or fields.Date.today()) discount = (new_list_price - price) / new_list_price * 100 - if discount > 0: + if (discount > 0 and new_list_price > 0) or (discount < 0 and new_list_price < 0): self.discount = discount def _is_delivery(self): diff --git a/addons/sale/report/report_all_channels_sales.py b/addons/sale/report/report_all_channels_sales.py index 1638c087d05..473ab1f1362 100644 --- a/addons/sale/report/report_all_channels_sales.py +++ b/addons/sale/report/report_all_channels_sales.py @@ -52,7 +52,7 @@ class PosSaleReport(models.Model): LEFT JOIN product_pricelist pp ON (so.pricelist_id = pp.id) LEFT JOIN uom_uom u on (u.id=sol.product_uom) LEFT JOIN uom_uom u2 on (u2.id=pt.uom_id) - WHERE so.state != 'cancel' + WHERE so.state in ('sale','done') """ return so_str diff --git a/addons/sale/static/src/js/variant_mixin.js b/addons/sale/static/src/js/variant_mixin.js index 19954256a75..907ebf603c8 100644 --- a/addons/sale/static/src/js/variant_mixin.js +++ b/addons/sale/static/src/js/variant_mixin.js @@ -1,6 +1,7 @@ odoo.define('sale.VariantMixin', function (require) { 'use strict'; +var concurrency = require('web.concurrency'); var core = require('web.core'); var utils = require('web.utils'); var ajax = require('web.ajax'); @@ -8,9 +9,8 @@ var _t = core._t; var VariantMixin = { events: { - 'click .css_attribute_color input': '_onChangeColorAttribute', + 'change .css_attribute_color input': '_onChangeColorAttribute', 'change .main_product:not(.in_cart) input.js_quantity': 'onChangeAddQuantity', - 'click button.js_add_cart_json': 'onClickAddCartJSON', 'change [data-attribute_exclusions]': 'onChangeVariant' }, @@ -36,10 +36,25 @@ var VariantMixin = { * @param {$.Element} [params.$container] force the used container */ onChangeVariant: function (ev, params) { + var $parent = $(ev.target).closest('.js_product'); + if (!$parent.data('uniqueId')) { + $parent.data('uniqueId', _.uniqueId()); + } + this._throttledGetCombinationInfo($parent.data('uniqueId'))(ev, params); + }, + /** + * @see onChangeVariant + * + * @private + * @param {Event} ev + * @param {$.Element} [params.$container] force the used container + * @returns {Deferred} + */ + _getCombinationInfo: function (ev, params) { var self = this; if ($(ev.target).hasClass('variant_custom_value')) { - return; + return Promise.resolve(); } var $component; @@ -57,16 +72,21 @@ var VariantMixin = { var qty = $component.find('input[name="add_qty"]').val(); var $parent = $(ev.target).closest('.js_product'); + var combination = this.getSelectedVariantValues($parent); + var parentCombination = $parent.find('ul[data-attribute_exclusions]').data('attribute_exclusions').parent_combination; + + var productTemplateId = parseInt($parent.find('.product_template_id').val()); self._checkExclusions($parent, combination); - ajax.jsonRpc(this._getUri('/sale/get_combination_info'), 'call', { - product_template_id: parseInt($parent.find('.product_template_id').val()), - product_id: this._getProductId($parent), - combination: combination, - add_qty: parseInt(qty), - pricelist_id: this.pricelistId || false, + return ajax.jsonRpc(this._getUri('/sale/get_combination_info'), 'call', { + 'product_template_id': productTemplateId, + 'product_id': this._getProductId($parent), + 'combination': combination, + 'add_qty': parseInt(qty), + 'pricelist_id': this.pricelistId || false, + 'parent_combination': parentCombination, }).then(function (combinationData) { self._onChangeCombination(ev, $parent, combinationData); }); @@ -138,10 +158,13 @@ var VariantMixin = { var $input = $link.closest('.input-group').find("input"); var min = parseFloat($input.data("min") || 0); var max = parseFloat($input.data("max") || Infinity); - var quantity = ($link.has(".fa-minus").length ? -1 : 1) + parseFloat($input.val() || 0, 10); + var previousQty = parseFloat($input.val() || 0, 10); + var quantity = ($link.has(".fa-minus").length ? -1 : 1) + previousQty; var newQty = quantity > min ? (quantity < max ? quantity : max) : min; - $input.val(newQty).trigger('change'); + if (newQty !== previousQty) { + $input.val(newQty).trigger('change'); + } return false; }, @@ -332,23 +355,6 @@ var VariantMixin = { * @param {Array} combination the selected combination of product attribute values */ _checkExclusions: function ($parent, combination) { - - function areCombinationsEqual(c1, c2) { - return c1.length === c2.length && _.every(c1, function (ptav) { - return c2.indexOf(ptav) > -1; - }); - } - - function isCombinationInList(c1, list) { - return _.some(list, function (c2) { - return areCombinationsEqual(c1, c2); - }); - } - - function isPtavInCombination(ptav, combination) { - return combination.indexOf(ptav) > -1; - } - var self = this; var combinationData = $parent .find('ul[data-attribute_exclusions]') @@ -360,17 +366,6 @@ var VariantMixin = { .attr('title', '') .data('excluded-by', ''); - var disable = false; - - // compatibility 12.0 - var filteredCombination = combination; - if (combinationData.no_variant_product_template_attribute_value_ids !== undefined) { - var no_variants = combinationData.no_variant_product_template_attribute_value_ids; - filteredCombination = _.filter(combination, function (ptav) { - return !isPtavInCombination(ptav, no_variants); - }); - } - // exclusion rules: array of ptav // for each of them, contains array with the other ptav they exclude if (combinationData.exclusions) { @@ -379,11 +374,6 @@ var VariantMixin = { if (combinationData.exclusions.hasOwnProperty(current_ptav)) { // for each exclusion of the current attribute: _.each(combinationData.exclusions[current_ptav], function (excluded_ptav) { - // disable if it excludes any other attribute already in the combination - if (isPtavInCombination(excluded_ptav, combination)) { - disable = true; - } - // disable the excluded input (even when not already selected) // to give a visual feedback before click self._disableInput( @@ -401,9 +391,6 @@ var VariantMixin = { _.each(combinationData.parent_exclusions, function (exclusions, excluded_by){ // check that the selected combination is in the parent exclusions _.each(exclusions, function (ptav) { - if (isPtavInCombination(ptav, combination)) { - disable = true; - } // disable the excluded input (even when not already selected) // to give a visual feedback before click @@ -416,30 +403,8 @@ var VariantMixin = { ); }); }); - - // archived variants - if (isCombinationInList(filteredCombination, combinationData.archived_combinations)) { - disable = true; - } - - // if not using dynamic attributes, exclude variants that are deleted - if (filteredCombination.length && // compatibility 12.0 list view of variants - combinationData.has_dynamic_attributes === false && - combinationData.existing_combinations !== undefined && - !isCombinationInList(filteredCombination, combinationData.existing_combinations) - ) { - disable = true; - } - - $parent.toggleClass('css_not_available', disable); - $parent.find("#add_to_cart").toggleClass('disabled', disable); $parent.find("#buy_now").toggleClass('disabled', disable); - $parent - .parents('.modal') - .find('.o_sale_product_configurator_add, .o_sale_product_configurator_edit') - .toggleClass('disabled', disable); }, - /** * Extracted to a method to be extendable by other modules * @@ -448,7 +413,6 @@ var VariantMixin = { _getProductId: function ($parent) { return parseInt($parent.find('.product_id').val()); }, - /** * Will disable the input/option that refers to the passed attributeValueId. * This is used for showing the user that some combinations are not available. @@ -490,7 +454,6 @@ var VariantMixin = { $target.data('excluded-by', JSON.stringify(excludedByData)); } }, - /** * @see onChangeVariant * @@ -507,6 +470,13 @@ var VariantMixin = { $price.text(self._priceToStr(combination.price)); $default_price.text(self._priceToStr(combination.list_price)); + var isCombinationPossible = true; + if (!_.isUndefined(combination.is_combination_possible)) { + isCombinationPossible = combination.is_combination_possible; + } + this._toggleDisable($parent, isCombinationPossible); + + // compatibility_check to remove in master // needed for fix in 12.0 in the case of git pull and no server restart var compatibility_check = combination.list_price - combination.price >= 0.01; @@ -546,7 +516,8 @@ var VariantMixin = { combination.display_image, combination.product_id, combination.product_template_id, - combination.carousel + combination.carousel, + isCombinationPossible ); } @@ -587,7 +558,46 @@ var VariantMixin = { formatted[0] = utils.insert_thousand_seps(formatted[0]); return formatted.join(l10n.decimal_point); }, - + /** + * Returns a throttled `_getCombinationInfo` with a leading and a trailing + * call, which is memoized per `uniqueId`, and for which previous results + * are dropped. + * + * The uniqueId is needed because on the configurator modal there might be + * multiple elements triggering the rpc at the same time, and we need each + * individual product rpc to be executed, but only once per individual + * product. + * + * The leading execution is to keep good reactivity on the first call, for + * a better user experience. The trailing is because ultimately only the + * information about the last selected combination is useful. All + * intermediary rpc can be ignored and are therefore best not done at all. + * + * The DropMisordered is to make sure slower rpc are ignored if the result + * of a newer rpc has already been received. + * + * @private + * @param {string} uniqueId + * @returns {function} + */ + _throttledGetCombinationInfo: _.memoize(function (uniqueId) { + var dropMisordered = new concurrency.DropMisordered(); + var _getCombinationInfo = _.throttle(this._getCombinationInfo.bind(this), 500); + return function (ev, params) { + return dropMisordered.add(_getCombinationInfo(ev, params)); + }; + }), + /** + * Toggles the disabled class depending on the $parent element + * and the possibility of the current combination. + * + * @private + * @param {$.Element} $parent + * @param {boolean} isCombinationPossible + */ + _toggleDisable: function ($parent, isCombinationPossible) { + $parent.toggleClass('css_not_available', !isCombinationPossible); + }, /** * Updates the product image. * This will use the productId if available or will fallback to the productTemplateId. diff --git a/addons/sale/views/res_config_settings_views.xml b/addons/sale/views/res_config_settings_views.xml index 6ae4798b98f..567f17e8dc9 100644 --- a/addons/sale/views/res_config_settings_views.xml +++ b/addons/sale/views/res_config_settings_views.xml @@ -61,7 +61,7 @@ <div class="o_setting_right_pane"> <label for="module_product_email_template" string="Deliver Content by Email"/> <div class="text-muted"> - Send a product-specific email once the invoice is paid + Send a product-specific email once the invoice is validated </div> </div> </div> diff --git a/addons/sale/views/sale_views.xml b/addons/sale/views/sale_views.xml index 758108db0ea..0dc660a46ea 100644 --- a/addons/sale/views/sale_views.xml +++ b/addons/sale/views/sale_views.xml @@ -498,6 +498,7 @@ <field name="price_total" invisible="1"/> <field name="price_unit"/> <field name="display_type"/> + <field name="tax_id" invisible="1"/> <templates> <t t-name="kanban-box"> <div t-attf-class="oe_kanban_card oe_kanban_global_click {{ record.display_type.raw_value ? 'o_is_' + record.display_type.raw_value : '' }}"> diff --git a/addons/sale_coupon/tests/common.py b/addons/sale_coupon/tests/common.py index 03820578f64..fec5d669aba 100644 --- a/addons/sale_coupon/tests/common.py +++ b/addons/sale_coupon/tests/common.py @@ -1,14 +1,19 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from openerp.tests import common +from odoo.addons.sale.tests.test_sale_product_attribute_value_config import TestSaleProductAttributeValueSetup -class TestSaleCouponCommon(common.TransactionCase): +class TestSaleCouponCommon(TestSaleProductAttributeValueSetup): def setUp(self): super(TestSaleCouponCommon, self).setUp() + # set currency to not rely on demo data and avoid possible race condition + self.currency_ratio = 1.0 + pricelist = self.env.ref('product.list0') + pricelist.currency_id = self._setup_currency(self.currency_ratio) + # Set all the existing programs to active=False to avoid interference self.env['sale.coupon.program'].search([]).write({'active': False}) diff --git a/addons/sale_product_configurator/static/src/js/product_configurator_modal.js b/addons/sale_product_configurator/static/src/js/product_configurator_modal.js index da3ac1d8494..7d52b07b76d 100644 --- a/addons/sale_product_configurator/static/src/js/product_configurator_modal.js +++ b/addons/sale_product_configurator/static/src/js/product_configurator_modal.js @@ -11,6 +11,7 @@ var optionalProductsMap = {}; var OptionalProductsModal = Dialog.extend(ServicesMixin, VariantMixin, { events: _.extend({}, Dialog.prototype.events, VariantMixin.events, { 'click a.js_add, a.js_remove': '_onAddOrRemoveOption', + 'click button.js_add_cart_json': 'onClickAddCartJSON', 'change .in_cart.main_product input.js_quantity': '_onChangeQuantity', 'change .js_raw_price': '_computePriceTotal' }), @@ -60,7 +61,6 @@ var OptionalProductsModal = Dialog.extend(ServicesMixin, VariantMixin, { this.previousModalHeight = params.previousModalHeight; this.dialogClass = 'oe_optional_products_modal'; this._productImageField = 'image_medium'; - // reset any previously populated properties maps optionalProductsMap = {}; @@ -437,6 +437,8 @@ var OptionalProductsModal = Dialog.extend(ServicesMixin, VariantMixin, { .text(combination.display_name); VariantMixin._onChangeCombination.apply(this, arguments); + + this._computePriceTotal(); }, /** * When the quantity of the root product is updated, we need to update @@ -496,4 +498,4 @@ var OptionalProductsModal = Dialog.extend(ServicesMixin, VariantMixin, { return OptionalProductsModal; -});
\ No newline at end of file +}); diff --git a/addons/sale_product_configurator/static/src/js/product_configurator_renderer.js b/addons/sale_product_configurator/static/src/js/product_configurator_renderer.js index 339254c8e45..0e3829f69e1 100644 --- a/addons/sale_product_configurator/static/src/js/product_configurator_renderer.js +++ b/addons/sale_product_configurator/static/src/js/product_configurator_renderer.js @@ -5,6 +5,11 @@ var FormRenderer = require('web.FormRenderer'); var VariantMixin = require('sale.VariantMixin'); var ProductConfiguratorFormRenderer = FormRenderer.extend(VariantMixin, { + + events: _.extend({}, FormRenderer.prototype.events, VariantMixin.events, { + 'click button.js_add_cart_json': 'onClickAddCartJSON', + }), + /** * @override */ @@ -74,6 +79,17 @@ var ProductConfiguratorFormRenderer = FormRenderer.extend(VariantMixin, { }, /** + * Toggles the add button depending on the possibility of the current + * combination. + * + * @override + */ + _toggleDisable: function ($parent, isCombinationPossible) { + VariantMixin._toggleDisable.apply(this, arguments); + $parent.parents('.modal').find('.o_sale_product_configurator_add').toggleClass('disabled', !isCombinationPossible); + }, + + /** * Will fill the custom values input based on the provided initial configuration. * * @private diff --git a/addons/sale_product_configurator/static/tests/tours/product_configurator_ui.js b/addons/sale_product_configurator/static/tests/tours/product_configurator_ui.js index 7bce9880f73..6c36897ef71 100644 --- a/addons/sale_product_configurator/static/tests/tours/product_configurator_ui.js +++ b/addons/sale_product_configurator/static/tests/tours/product_configurator_ui.js @@ -36,11 +36,7 @@ tour.register('sale_product_configurator_tour', { run: 'click' }, { trigger: '.configurator_container span:contains("Steel")', - run: function () { - $('input.product_id').change(function () { - $('.o_sale_product_configurator_add').attr('request_count', 1); - }); - } + run: function () {}, }, { trigger: '.configurator_container span:contains("Aluminium")', run: 'click' @@ -48,8 +44,11 @@ tour.register('sale_product_configurator_tour', { trigger: 'span.oe_currency_value:contains("800.40")', run: function (){} // check updated price }, { - trigger: '.o_sale_product_configurator_add[request_count="1"]', - run: function (){} // used to sync with "get_combination_info" completion + trigger: 'input[data-value_name="Black"]' +}, { + trigger: '.o_sale_product_configurator_add.disabled' +}, { + trigger: 'input[data-value_name="White"]' }, { trigger: '.o_sale_product_configurator_add:not(.disabled)' }, { diff --git a/addons/sale_stock/models/sale_order.py b/addons/sale_stock/models/sale_order.py index 3d6b67ae58d..5d682178a0b 100644 --- a/addons/sale_stock/models/sale_order.py +++ b/addons/sale_stock/models/sale_order.py @@ -107,6 +107,21 @@ class SaleOrder(models.Model): if self.warehouse_id.company_id: self.company_id = self.warehouse_id.company_id.id + @api.onchange('partner_shipping_id') + def _onchange_partner_shipping_id(self): + res = {} + pickings = self.picking_ids.filtered( + lambda p: p.state not in ['done', 'cancel'] and p.partner_id != self.partner_shipping_id + ) + if pickings: + res['warning'] = { + 'title': _('Warning!'), + 'message': _( + 'Do not forget to change the partner on the following delivery orders: %s' + ) % (','.join(pickings.mapped('name'))) + } + return res + @api.multi def action_view_delivery(self): ''' diff --git a/addons/sale_timesheet/models/sale_order.py b/addons/sale_timesheet/models/sale_order.py index ee49687a9d2..82654d8b81d 100644 --- a/addons/sale_timesheet/models/sale_order.py +++ b/addons/sale_timesheet/models/sale_order.py @@ -258,6 +258,10 @@ class SaleOrderLine(models.Model): 'partner_id': self.order_id.partner_id.id, 'email_from': self.order_id.partner_id.email, }) + # duplicating a project doesn't set the SO on sub-tasks + project.tasks.filtered(lambda task: task.parent_id != False).write({ + 'sale_line_id': self.id, + }) else: project = self.env['project.project'].create(values) # link project as generated by current so line diff --git a/addons/sales_team/tests/__init__.py b/addons/sales_team/tests/__init__.py new file mode 100644 index 00000000000..7bb8a11fc5e --- /dev/null +++ b/addons/sales_team/tests/__init__.py @@ -0,0 +1 @@ +from . import test_default_team diff --git a/addons/sales_team/tests/test_default_team.py b/addons/sales_team/tests/test_default_team.py new file mode 100644 index 00000000000..9905272b3d9 --- /dev/null +++ b/addons/sales_team/tests/test_default_team.py @@ -0,0 +1,48 @@ +from odoo.tests import common + + +class TestDefaultTeam(common.SavepointCase): + """Tests to check if correct default team is found.""" + + @classmethod + def setUpClass(cls): + """Set up data for default team tests.""" + super(TestDefaultTeam, cls).setUpClass() + cls.CrmTeam = cls.env['crm.team'] + ResUsers = cls.env['res.users'].with_context( + {'no_reset_password': True}) + group_sale_manager = cls.env.ref('sales_team.group_sale_manager') + cls.user = ResUsers.create({ + 'name': 'Team User', + 'login': 'sales_team_user', + 'email': 'sales.team.user@example.com', + 'groups_id': [(6, 0, [group_sale_manager.id])] + }) + cls.team_1 = cls.env['crm.team'].create({ + 'name': 'Test Team', + 'member_ids': [(4, cls.user.id)], + 'company_id': False + }) + # Europe Team (fall back team) + cls.team_2 = cls.env.ref('sales_team.team_sales_department') + + def test_01_user_team(self): + """Get default team, when user belongs to one.""" + team = self.CrmTeam.sudo(self.user)._get_default_team_id() + self.assertEqual(team, self.team_1) + + def test_02_fallback_team(self): + """Get default team when user does not belong to any team. + + Case 1: fall back default team (from XML ref) is active. + Case 2: fall back default team is not active. + """ + # Clear users from team. + self.team_1.member_ids = [(5,)] + # Case 1. + team = self.CrmTeam.sudo(self.user)._get_default_team_id() + self.assertEqual(team, self.team_2) + # Case 2. + self.team_2.active = False + team = self.CrmTeam.sudo(self.user)._get_default_team_id() + self.assertEqual(team, self.CrmTeam) diff --git a/addons/sms/models/mail_thread.py b/addons/sms/models/mail_thread.py index 1fb62a72de4..a439487e4e9 100644 --- a/addons/sms/models/mail_thread.py +++ b/addons/sms/models/mail_thread.py @@ -39,8 +39,8 @@ class MailThread(models.AbstractModel): if not partners: partners = self._get_default_sms_recipients() - # Collect numbers, we will consider the message to be sent if at least one number can be found - numbers = list(set([i.mobile for i in partners if i.mobile])) + # Collect numbers, we will consider the message to be sent if at least one number can be found + numbers = list(set([i.mobile for i in partners if i.mobile])) if numbers: try: diff --git a/addons/sms/wizard/send_sms.py b/addons/sms/wizard/send_sms.py index ac2d63a0d75..e264eab59f8 100644 --- a/addons/sms/wizard/send_sms.py +++ b/addons/sms/wizard/send_sms.py @@ -45,8 +45,8 @@ class SendSMS(models.TransientModel): return number if not phonenumbers.is_possible_number(phone_nbr) or not phonenumbers.is_valid_number(phone_nbr): return number - phone_fmt = phonenumbers.PhoneNumberFormat.INTERNATIONAL - return phonenumbers.format_number(phone_nbr, phone_fmt).replace(' ', '') + phone_fmt = phonenumbers.PhoneNumberFormat.E164 + return phonenumbers.format_number(phone_nbr, phone_fmt) else: return number @@ -62,12 +62,11 @@ class SendSMS(models.TransientModel): @api.model def default_get(self, fields): result = super(SendSMS, self).default_get(fields) - active_model = self.env.context.get('active_model') - model = self.env[active_model] - records = self._get_records(model) - if getattr(records, '_get_default_sms_recipients') and not self.env.context.get('default_recipients'): + if not self.env.context.get('default_recipients') and active_model and hasattr(self.env[active_model], '_get_default_sms_recipients'): + model = self.env[active_model] + records = self._get_records(model) partners = records._get_default_sms_recipients() phone_numbers = [] no_phone_partners = [] @@ -84,12 +83,12 @@ class SendSMS(models.TransientModel): return result def action_send_sms(self): - numbers = self.recipients.split(',') + numbers = [number.strip() for number in self.recipients.split(',') if number.strip()] active_model = self.env.context.get('active_model') - model = self.env[active_model] - records = self._get_records(model) - if getattr(records, 'message_post_send_sms'): + if active_model and hasattr(self.env[active_model], 'message_post_send_sms'): + model = self.env[active_model] + records = self._get_records(model) records.message_post_send_sms(self.message, numbers=numbers) else: self.env['sms.api']._send_sms(numbers, self.message) diff --git a/addons/snailmail/models/snailmail_letter.py b/addons/snailmail/models/snailmail_letter.py index a0925515a2c..b9e2fb55477 100644 --- a/addons/snailmail/models/snailmail_letter.py +++ b/addons/snailmail/models/snailmail_letter.py @@ -10,7 +10,6 @@ from odoo.exceptions import UserError from odoo.tools.safe_eval import safe_eval DEFAULT_ENDPOINT = 'https://iap-snailmail.odoo.com' -ESTIMATE_ENDPOINT = '/iap/snailmail/1/estimate' PRINT_ENDPOINT = '/iap/snailmail/1/print' ERROR_CODES = [ @@ -346,69 +345,6 @@ class SnailmailLetter(models.Model): self.write({'state': 'canceled', 'error_code': False}) self.send_snailmail_update() - @api.multi - def _snailmail_estimate(self): - """ - Send a request to estimate the cost of sending all the documents with - the differents options. - - The JSON object sent is the one generated from self._snailmail_create() - - arguments sent: - { - "documents":[{ - pages: int, - res_id: int (client_side, optional), - res_model: int (client_side, optional), - address: { - country_code: char (country name) - } - }], - 'color': # color on the letter, - 'duplex': # one/two side printing, - } - - The answer of the server is the same JSON object with some additionnal fields: - { - "total_cost": integer, //The cost of sending ALL the documents - body: JSON object (same as body param except new param 'cost' in each documents) - } - """ - endpoint = self.env['ir.config_parameter'].sudo().get_param('snailmail.endpoint', DEFAULT_ENDPOINT) - params = self._snailmail_create('estimate') - req = jsonrpc(endpoint + '/iap/snailmail/1/estimate', params=params) - - return req['total_cost'] - - @api.model - def _snailmail_estimate_from_documents(self, res_model, res_ids, partner_field=None): - endpoint = self.env['ir.config_parameter'].sudo().get_param('snailmail.endpoint', DEFAULT_ENDPOINT) - docs = self.env[res_model].browse(res_ids) - doc_list = [] - for doc in docs: - if partner_field: - country_code = doc[partner_field].country_id.code - else: - country_code = doc.country_id.code - val = { - 'pages': 1, - 'address': { - 'country_code': country_code, - }, - } - doc_list.append(val) - params = { - 'account_token': "", - 'documents': doc_list, - 'options': { - 'color': self.env.company.snailmail_color, - 'duplex': self.env.company.snailmail_duplex, - 'currency_name': 'EUR', - }, - } - req = jsonrpc(endpoint + '/iap/snailmail/1/estimate', params=params) - return req['total_cost'] - @api.model def _snailmail_cron(self): letters_send = self.search([ diff --git a/addons/snailmail_account/wizard/account_invoice_send.py b/addons/snailmail_account/wizard/account_invoice_send.py index d1545d808c4..de0829cd1d8 100644 --- a/addons/snailmail_account/wizard/account_invoice_send.py +++ b/addons/snailmail_account/wizard/account_invoice_send.py @@ -35,7 +35,7 @@ class AccountInvoiceSend(models.TransientModel): @api.depends('snailmail_is_letter') def _compute_snailmail_cost(self): for wizard in self: - wizard.snailmail_cost = wizard.env['snailmail.letter']._snailmail_estimate_from_documents(wizard.invoice_ids._name, wizard.invoice_ids.ids, 'partner_id') + wizard.snailmail_cost = len(wizard.invoice_ids.ids) @api.multi def snailmail_print_action(self): diff --git a/addons/stock/models/product.py b/addons/stock/models/product.py index 5582ab5839f..2f9da1de3f1 100644 --- a/addons/stock/models/product.py +++ b/addons/stock/models/product.py @@ -517,6 +517,10 @@ class ProductTemplate(models.Model): def _is_cost_method_standard(self): return True + @api.depends( + 'product_variant_ids', + 'product_variant_ids.stock_quant_ids', + ) def _compute_quantities(self): res = self._compute_quantities_dict() for template in self: diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py index f2e277f8245..e6874d6b71c 100644 --- a/addons/stock/models/stock_move.py +++ b/addons/stock/models/stock_move.py @@ -450,6 +450,7 @@ class StockMove(models.Model): view = self.env.ref('stock.view_stock_move_operations') + picking_type_id = self.picking_type_id or self.picking_id.picking_type_id return { 'name': _('Detailed Operations'), 'type': 'ir.actions.act_window', @@ -462,8 +463,8 @@ class StockMove(models.Model): 'res_id': self.id, 'context': dict( self.env.context, - show_lots_m2o=self.has_tracking != 'none' and (self.picking_type_id.use_existing_lots or self.state == 'done' or self.origin_returned_move_id.id), # able to create lots, whatever the value of ` use_create_lots`. - show_lots_text=self.has_tracking != 'none' and self.picking_type_id.use_create_lots and not self.picking_type_id.use_existing_lots and self.state != 'done' and not self.origin_returned_move_id.id, + show_lots_m2o=self.has_tracking != 'none' and (picking_type_id.use_existing_lots or self.state == 'done' or self.origin_returned_move_id.id), # able to create lots, whatever the value of ` use_create_lots`. + show_lots_text=self.has_tracking != 'none' and picking_type_id.use_create_lots and not picking_type_id.use_existing_lots and self.state != 'done' and not self.origin_returned_move_id.id, show_source_location=self.location_id.child_ids and self.picking_type_id.code != 'incoming', show_destination_location=self.location_dest_id.child_ids and self.picking_type_id.code != 'outgoing', show_package=not self.location_id.usage == 'supplier', diff --git a/addons/stock/models/stock_warehouse.py b/addons/stock/models/stock_warehouse.py index 4f694566f07..9e55e533b2a 100644 --- a/addons/stock/models/stock_warehouse.py +++ b/addons/stock/models/stock_warehouse.py @@ -894,7 +894,6 @@ class Warehouse(models.Model): @api.returns('self') def _get_all_routes(self): - # TDE FIXME: check overrides routes = self.mapped('route_ids') | self.mapped('mto_pull_id').mapped('route_id') routes |= self.env["stock.location.route"].search([('supplied_wh_id', 'in', self.ids)]) return routes diff --git a/addons/stock/report/report_stock_forecast.py b/addons/stock/report/report_stock_forecast.py index 59e788ac4b6..1bf69ab5719 100644 --- a/addons/stock/report/report_stock_forecast.py +++ b/addons/stock/report/report_stock_forecast.py @@ -129,6 +129,8 @@ class ReportStockForecat(models.Model): WHERE final.product_qty != 0 OR final.reference = 'Starting Inventory' %s + ORDER BY + date ) """ % (self._select(), self._left_join(), self._groupby()) self.env.cr.execute(query) @@ -185,7 +187,6 @@ class ReportStockForecat(models.Model): order to analyze different quantities in different situations. """ # Sort result by date_expected and id. - orderby = 'date, id' if not orderby else orderby if 'cumulative_quantity' in fields and 'quantity' not in fields: fields.append('quantity') res = super(ReportStockForecat, self).read_group(domain, fields, groupby, offset=offset, limit=limit, orderby=orderby, lazy=lazy) diff --git a/addons/stock/wizard/stock_picking_return.py b/addons/stock/wizard/stock_picking_return.py index c7b0ed32733..f52afdb72a1 100644 --- a/addons/stock/wizard/stock_picking_return.py +++ b/addons/stock/wizard/stock_picking_return.py @@ -46,7 +46,7 @@ class ReturnPicking(models.TransientModel): @api.onchange('picking_id') def _onchange_picking_id(self): move_dest_exists = False - product_return_moves = [] + product_return_moves = [(5,)] if self.picking_id and self.picking_id.state != 'done': raise UserError(_("You may only return Done pickings.")) for move in self.picking_id.move_lines: diff --git a/addons/stock/wizard/stock_picking_return_views.xml b/addons/stock/wizard/stock_picking_return_views.xml index 3c901072aa7..e4cb319456c 100644 --- a/addons/stock/wizard/stock_picking_return_views.xml +++ b/addons/stock/wizard/stock_picking_return_views.xml @@ -29,7 +29,7 @@ <group> <field name="parent_location_id" invisible="1"/> <field name="original_location_id" invisible="1"/> - <field name="location_id" options="{'no_create': True, 'no_open': True}" groups="stock.group_stock_multi_locations"/> + <field name="location_id" options="{'no_create': True, 'no_open': True}" groups="stock.group_stock_multi_locations" required="1"/> </group> <footer> <button name="create_returns" string="Return" type="object" class="btn-primary"/> diff --git a/addons/stock_account/models/__init__.py b/addons/stock_account/models/__init__.py index 707d4618bda..a11616c850a 100644 --- a/addons/stock_account/models/__init__.py +++ b/addons/stock_account/models/__init__.py @@ -4,6 +4,7 @@ from . import account_chart_template from . import account_invoice from . import account_move +from . import account_reconciliation_widget from . import product from . import stock from . import res_config_settings diff --git a/addons/stock_account/models/account_reconciliation_widget.py b/addons/stock_account/models/account_reconciliation_widget.py new file mode 100644 index 00000000000..44110053899 --- /dev/null +++ b/addons/stock_account/models/account_reconciliation_widget.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- + +from odoo import api, models +from odoo.osv import expression + + +class AccountReconciliation(models.AbstractModel): + _inherit = "account.reconciliation.widget" + + @api.model + def _domain_move_lines_for_reconciliation(self, st_line, aml_accounts, partner_id, excluded_ids=None, search_str=False): + def to_int(val): + try: + return int(val) + except (ValueError, TypeError): + return None + + domain = super()._domain_move_lines_for_reconciliation( + st_line, aml_accounts, partner_id, excluded_ids=excluded_ids, search_str=search_str + ) + acc_props = ( + "property_stock_account_input", + "property_stock_account_output", + "property_stock_account_input_categ_id", + "property_stock_account_output_categ_id", + ) + acc_ids = [ + (acc["value_reference"] or "").split(",")[-1] + for acc in self.env["ir.property"] + .sudo() + .search([("name", "in", acc_props), ("value_reference", "!=", False)]) + .read(["value_reference"]) + if to_int((acc["value_reference"] or "").split(",")[-1]) + ] + if acc_ids: + domain = expression.AND([domain, [("account_id.id", "not in", acc_ids)]]) + return domain diff --git a/addons/stock_account/tests/test_stockvaluation.py b/addons/stock_account/tests/test_stockvaluation.py index d3c4a9918e0..e628f11bf48 100644 --- a/addons/stock_account/tests/test_stockvaluation.py +++ b/addons/stock_account/tests/test_stockvaluation.py @@ -2590,7 +2590,7 @@ class TestStockValuation(SavepointCase): self.assertAlmostEqual(self.product1.stock_value, 0.0) def test_average_perpetual_6(self): - self.product1.product_tmpl_id.categ_id.property_cost_method = 'average' + self.product1.categ_id.property_cost_method = 'average' move1 = self.env['stock.move'].create({ 'name': 'Receive 1 unit at 10', @@ -2627,7 +2627,7 @@ class TestStockValuation(SavepointCase): """ Test edit in the past. Receive 5@10, receive 10@20, edit the first move to receive 15 instead. """ - self.product1.product_tmpl_id.categ_id.property_cost_method = 'average' + self.product1.categ_id.property_cost_method = 'average' move1 = self.env['stock.move'].create({ 'name': 'IN 5@10', @@ -2667,7 +2667,7 @@ class TestStockValuation(SavepointCase): """ Receive 1@10, then dropship 1@20, finally return the dropship. Dropship should not impact the price. """ - self.product1.product_tmpl_id.categ_id.property_cost_method = 'average' + self.product1.categ_id.property_cost_method = 'average' move1 = self.env['stock.move'].create({ 'name': 'IN 1@10', diff --git a/addons/survey/views/assets.xml b/addons/survey/views/assets.xml index 381531bfd41..75543048801 100644 --- a/addons/survey/views/assets.xml +++ b/addons/survey/views/assets.xml @@ -21,6 +21,12 @@ </xpath> </template> + <template id="assets_common" name="tour" inherit_id="web.assets_common"> + <xpath expr="//script[last()]" position="after"> + <script type="text/javascript" src="/survey/static/src/js/tour_test_survey.js"></script> + </xpath> + </template> + <template id="assets_backend" name="survey assets" inherit_id="web.assets_backend"> <xpath expr="." position="inside"> <link rel="stylesheet" href="/survey/static/src/css/survey_result.css"/> diff --git a/addons/test_mail/tests/test_mail_template.py b/addons/test_mail/tests/test_mail_template.py index ebff4b24a3a..3a70a575011 100644 --- a/addons/test_mail/tests/test_mail_template.py +++ b/addons/test_mail/tests/test_mail_template.py @@ -70,6 +70,46 @@ class TestMailTemplate(BaseFunctionalTest, MockEmails, TestRecipients): body_content=self.test_record.email_from, attachments=[('first.txt', b'My first attachment', 'text/plain'), ('second.txt', b'My second attachment', 'text/plain')]) + def test_composer_template_onchange_attachments(self): + """Tests that all attachments are added to the composer, + static attachments are not duplicated and while reports are re-generated, + and that intermediary attachments are dropped.""" + + composer = self.env['mail.compose.message'].with_context(default_attachment_ids=[]).create({}) + report_template = self.env.ref('web.action_report_externalpreview') + template_1 = self.email_template.copy({ + 'report_template': report_template.id, + }) + template_2 = self.email_template.copy({ + 'attachment_ids': False, + 'report_template': report_template.id, + }) + + onchange_templates = [template_1, template_2, template_1, False] + attachments_onchange = [composer.attachment_ids] + # template_1 has two static attachments and one dynamically generated report, + # template_2 only has the report, so we should get 3, 1, 3 attachments + attachment_numbers = [0, 3, 1, 3, 0] + + with self.env.do_in_onchange(): + for template in onchange_templates: + onchange = composer.onchange_template_id( + template.id if template else False, 'comment', self.test_record._name, self.test_record.id + ) + values = composer._convert_to_record(composer._convert_to_cache(onchange['value'])) + attachments_onchange.append(values['attachment_ids']) + composer.update(onchange['value']) + + self.assertEqual( + [len(attachments) for attachments in attachments_onchange], + attachment_numbers, + ) + + self.assertTrue( + len(attachments_onchange[1] & attachments_onchange[3]) == 2, + "The two static attachments on the template should be common to the two onchanges" + ) + @mute_logger('odoo.addons.mail.models.mail_mail') def test_post_post_w_template(self): self.test_record.sudo(self.user_employee).message_post_with_template(self.email_template.id, composition_mode='comment') @@ -235,3 +275,44 @@ class TestMailTemplate(BaseFunctionalTest, MockEmails, TestRecipients): new_partner = Partner.search([('email', '=', email_new_partner)]) self.assertTrue(new_partner) self.assertEqual(new_partner.company_id, company1) + + def test_composer_template_onchange_attachments(self): + """Tests that all attachments are added to the composer, + static attachments are not duplicated and while reports are re-generated, + and that intermediary attachments are dropped.""" + + composer = self.env['mail.compose.message'].with_context(default_attachment_ids=[]).create({}) + report_template = self.env.ref('web.action_report_externalpreview') + template_1 = self.email_template.copy({ + 'report_template': report_template.id, + }) + template_2 = self.email_template.copy({ + 'attachment_ids': False, + 'report_template': report_template.id, + }) + + onchange_templates = [template_1, template_2, template_1, False] + attachments_onchange = [composer.attachment_ids] + # template_1 has two static attachments and one dynamically generated report, + # template_2 only has the report, so we should get 3, 1, 3 attachments + # and when there is no template, no attachments + attachment_numbers = [0, 3, 1, 3, 0] + + with self.env.do_in_onchange(): + for template in onchange_templates: + onchange = composer.onchange_template_id( + template.id if template else False, 'comment', 'mail.test.simple', self.test_record.id + ) + values = composer._convert_to_record(composer._convert_to_cache(onchange['value'])) + attachments_onchange.append(values['attachment_ids']) + composer.update(onchange['value']) + + self.assertEqual( + [len(attachments) for attachments in attachments_onchange], + attachment_numbers, + ) + + self.assertTrue( + len(attachments_onchange[1] & attachments_onchange[3]) == 2, + "The two static attachments on the template should be common to the two onchanges" + ) 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 f1af6fc7234..6b7df97b767 100644 --- a/addons/web/static/src/js/chrome/abstract_web_client.js +++ b/addons/web/static/src/js/chrome/abstract_web_client.js @@ -514,7 +514,11 @@ var AbstractWebClient = Widget.extend(ServiceProviderMixin, KeyboardNavigationMi new RainbowMan(data).appendTo(this.$el); } else { // For instance keep title blank, as we don't have title in data - this.notification_manager.notify('', data.message, true); + this.call('notification', 'notify', { + title: "", + message: data.message, + sticky: false + }); } } else { throw new Error('Unknown effect type: ' + type); diff --git a/addons/web/static/src/js/chrome/action_manager_act_window.js b/addons/web/static/src/js/chrome/action_manager_act_window.js index f694e2ca1b0..ee3ce15bdd2 100644 --- a/addons/web/static/src/js/chrome/action_manager_act_window.js +++ b/addons/web/static/src/js/chrome/action_manager_act_window.js @@ -58,7 +58,11 @@ ActionManager.include({ * @param {string} [state.view_type] */ loadState: function (state) { + var callersArguments = arguments; + var _super = this._super.bind(this); + var self = this; var action; + var def; var options = { clear_breadcrumbs: true, pushState: false, @@ -113,11 +117,20 @@ ActionManager.include({ action = lastAction; options.viewType = state.view_type; } + } else if (state.sa) { + def = this._rpc({ + route: '/web/session/get_session_action', + params: {key: state.sa}, + }).then(function (sessionAction) { + action = sessionAction; + }); } - if (action) { - return this.doAction(action, options); - } - return this._super.apply(this, arguments); + return Promise.all([def]).then(function () { + if (action) { + return self.doAction(action, options); + } + return _super.apply(self, callersArguments); + }); }, //-------------------------------------------------------------------------- diff --git a/addons/web/static/src/js/core/ajax.js b/addons/web/static/src/js/core/ajax.js index a5ed4065282..af7628cc5ea 100644 --- a/addons/web/static/src/js/core/ajax.js +++ b/addons/web/static/src/js/core/ajax.js @@ -27,11 +27,17 @@ function _genericJsonRpc (fct_name, params, settings, fct) { core.bus.trigger('rpc:result', data, result); if (result.error !== undefined) { if (result.error.data.arguments[0] !== "bus.Bus not available in test mode") { + var func = console.error; if (result.error.data.exception_type === "user_error") { - console.log("Server application error", JSON.stringify(result.error)); - } else { - console.error("Server application error", JSON.stringify(result.error)); + func = console.log; } + func( + "Server application error\n", + "Error code:", result.error.code, "\n", + "Error message:", result.error.message, "\n", + "Error data message:\n", result.error.data.message, "\n", + "Error data debug:\n", result.error.data.debug + ); } return Promise.reject({type: "server", error: result.error}); } else { diff --git a/addons/web/static/src/js/tools/test_menus.js b/addons/web/static/src/js/tools/test_menus.js index 4b7ee4b2e02..82a4ed26c0e 100644 --- a/addons/web/static/src/js/tools/test_menus.js +++ b/addons/web/static/src/js/tools/test_menus.js @@ -44,8 +44,12 @@ } } - // Main function that starts orchestration of tests function clickEverywhere(){ + setTimeout(_clickEverywhere, 1000); + } + + // Main function that starts orchestration of tests + function _clickEverywhere(){ console.log("Starting ClickEverywhere test"); var startTime = performance.now(); createWebClientHooks(); diff --git a/addons/web/static/src/js/views/calendar/calendar_controller.js b/addons/web/static/src/js/views/calendar/calendar_controller.js index 61a966972aa..b3a04ec9a2b 100644 --- a/addons/web/static/src/js/views/calendar/calendar_controller.js +++ b/addons/web/static/src/js/views/calendar/calendar_controller.js @@ -160,7 +160,8 @@ var CalendarController = AbstractController.extend({ * @returns {Promise} */ _updateRecord: function (record) { - return this.model.updateRecord(record).then(this.reload.bind(this, {})); + var reload = this.reload.bind(this, {}); + return this.model.updateRecord(record).then(reload, reload); }, //-------------------------------------------------------------------------- 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 0ad1b61e4d7..c3934d930d7 100644 --- a/addons/web/static/src/js/views/calendar/calendar_model.js +++ b/addons/web/static/src/js/views/calendar/calendar_model.js @@ -310,7 +310,7 @@ return AbstractModel.extend({ * Toggle the sidebar (containing the mini calendar) */ toggleFullWidth: function () { - var fullWidth = this.call('local_storage', 'getItem', 'calendar_fullWidth') !== 'true'; + var fullWidth = this.call('local_storage', 'getItem', 'calendar_fullWidth') !== true; this.call('local_storage', 'setItem', 'calendar_fullWidth', fullWidth); }, /** @@ -442,7 +442,7 @@ return AbstractModel.extend({ */ _loadCalendar: function () { var self = this; - this.data.fullWidth = this.call('local_storage', 'getItem', 'calendar_fullWidth') === 'true'; + this.data.fullWidth = this.call('local_storage', 'getItem', 'calendar_fullWidth') === true; this.data.fc_options = this._getFullCalendarOptions(); var defs = _.map(this.data.filters, this._loadFilter.bind(this)); diff --git a/addons/web/static/src/js/widgets/date_picker.js b/addons/web/static/src/js/widgets/date_picker.js index 3206ef7b47a..5b4480838fe 100644 --- a/addons/web/static/src/js/widgets/date_picker.js +++ b/addons/web/static/src/js/widgets/date_picker.js @@ -13,6 +13,7 @@ var DateWidget = Widget.extend({ type_of_date: "date", events: { 'change.datetimepicker': 'changeDatetime', + 'error.datetimepicker': 'errorDatetime', 'change .o_datepicker_input': 'changeDatetime', 'click input': '_onInputClicked', 'input input': '_onInput', @@ -112,6 +113,12 @@ var DateWidget = Widget.extend({ } }, /** + * Library clears the wrong date format so just ignore error + */ + errorDatetime: function (e) { + return false; + }, + /** * Focuses the datepicker input. This function must be called in order to * prevent 'input' events triggered by the lib to bubble up, and to cause * unwanted effects (like triggering 'field_changed' events) @@ -276,17 +283,6 @@ var DateWidget = Widget.extend({ */ _onKeydown: function (ev) { if (ev.which === $.ui.keyCode.ESCAPE) { - this.__libInput++; - this.$el.datetimepicker('hide'); - this.__libInput--; - } - }, - /** - * @private - * @param {KeyEvent} ev - */ - _onKeydown: function (ev) { - if (ev.which === $.ui.keyCode.ESCAPE) { if (this.__isOpen) { // we don't want any other effects than closing the datepicker, // like leaving the edition of a row in editable list view diff --git a/addons/web/static/src/scss/bootstrap_review.scss b/addons/web/static/src/scss/bootstrap_review.scss index b9e889ce71a..414984dc42d 100644 --- a/addons/web/static/src/scss/bootstrap_review.scss +++ b/addons/web/static/src/scss/bootstrap_review.scss @@ -30,6 +30,15 @@ &:last-child { @include border-bottom-radius($card-inner-border-radius); } + + &.row { + // The 'row' class should not be used on a 'card-body' element but if + // it is done, our custom bg color would overflow the card. As a fix + // for those cases (normally only one at the time this fix is made), + // remove the background color. + // TODO remove me in master. + background-color: transparent !important; + } } .accordion { .collapsing, .collapse.show { @@ -70,3 +79,8 @@ min-height: 0; } } + +// Restore pointer cursor which came with the 'btn' class until BS 4.3.1 +.btn:not(:disabled):not(.disabled) { + cursor: pointer; +} diff --git a/addons/web/static/src/scss/form_view_extra.scss b/addons/web/static/src/scss/form_view_extra.scss index 277537beab4..0d59483c2d8 100644 --- a/addons/web/static/src/scss/form_view_extra.scss +++ b/addons/web/static/src/scss/form_view_extra.scss @@ -140,7 +140,7 @@ border-right: 1px solid #ddd; } .o_td_label + td { - padding: 2px 36px 2px 8px; + padding: 0px 36px 0px 8px; } .o_field_widget.o_text_overflow { width: 1px!important; // hack to make the table layout believe it is a small element (so that the table does not grow too much) ... diff --git a/addons/web/static/src/scss/kanban_view.scss b/addons/web/static/src/scss/kanban_view.scss index 7ab56e2bbb3..f80d50ba576 100644 --- a/addons/web/static/src/scss/kanban_view.scss +++ b/addons/web/static/src/scss/kanban_view.scss @@ -248,6 +248,7 @@ font-weight: 500; background-color: transparent; color: inherit; + box-shadow: none; @include o-kanban-tag-color; span { diff --git a/addons/web/static/src/scss/list_view.scss b/addons/web/static/src/scss/list_view.scss index 3d46aeef890..f8c0afdd499 100644 --- a/addons/web/static/src/scss/list_view.scss +++ b/addons/web/static/src/scss/list_view.scss @@ -42,6 +42,10 @@ font-weight: $o-list-footer-font-weight; border-top: 2px solid #cacaca; border-bottom: 1px solid #cacaca; + + .o_list_number { + text-align: right; + } } .o_column_sortable { diff --git a/addons/web/static/tests/chrome/action_manager_tests.js b/addons/web/static/tests/chrome/action_manager_tests.js index 7ac817af30c..c82755f2987 100644 --- a/addons/web/static/tests/chrome/action_manager_tests.js +++ b/addons/web/static/tests/chrome/action_manager_tests.js @@ -1177,6 +1177,30 @@ QUnit.module('ActionManager', { actionManager.destroy(); }); + QUnit.test('state.sa should load action from session', async function (assert) { + assert.expect(1); + + var actionManager = await createActionManager({ + actions: this.actions, + archs: this.archs, + data: this.data, + mockRPC: function (route, args) { + if (route === '/web/session/get_session_action') { + return Promise.resolve(1); + } + return this._super.apply(this, arguments); + }, + }); + await actionManager.loadState({ + sa: 1, + }); + + assert.strictEqual(actionManager.$('.o_kanban_view').length, 1, + "should have rendered a kanban view"); + + actionManager.destroy(); + }); + QUnit.module('Concurrency management'); QUnit.test('drop previous actions if possible', async function (assert) { diff --git a/addons/web/static/tests/fields/basic_fields_tests.js b/addons/web/static/tests/fields/basic_fields_tests.js index e768c1b67ea..5c399547458 100644 --- a/addons/web/static/tests/fields/basic_fields_tests.js +++ b/addons/web/static/tests/fields/basic_fields_tests.js @@ -2549,6 +2549,25 @@ QUnit.module('basic_fields', { form.destroy(); }); + QUnit.test('date field should remove the date if the date is not valid', async function (assert) { + assert.expect(1); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners"><field name="date"/></form>', + res_id: 4, + }); + // switch to edit mode + await testUtils.form.clickEdit(form); + // set an invalid date + var $input = form.$('.o_field_widget[name=date] input'); + $input.val('mmmh').trigger('change'); + assert.strictEqual($input.text(), "", "The date field should be empty"); + form.destroy(); + }); + QUnit.test('date field value should not set on first click', async function (assert) { assert.expect(2); diff --git a/addons/web/static/tests/views/calendar_tests.js b/addons/web/static/tests/views/calendar_tests.js index d78424841ff..babb22a5ba1 100644 --- a/addons/web/static/tests/views/calendar_tests.js +++ b/addons/web/static/tests/views/calendar_tests.js @@ -1,11 +1,13 @@ odoo.define('web.calendar_tests', function (require) { "use strict"; +var AbstractStorageService = require('web.AbstractStorageService'); var CalendarView = require('web.CalendarView'); var CalendarRenderer = require('web.CalendarRenderer'); var Dialog = require('web.Dialog'); var fieldUtils = require('web.field_utils'); var mixins = require('web.mixins'); +var RamStorage = require('web.RamStorage'); var testUtils = require('web.test_utils'); var session = require('web.session'); @@ -2612,6 +2614,45 @@ QUnit.module('Views', { calendar.destroy(); }); + QUnit.test('calendar sidebar toggle', async function (assert) { + assert.expect(5); + + var calendar = await createCalendarView({ + View: CalendarView, + model: 'event', + data: this.data, + arch: + '<calendar date_start="start_date">'+ + '<field name="name"/>'+ + '</calendar>', + services: { + local_storage: AbstractStorageService.extend({storage: new RamStorage()}), + }, + }); + + var fullWidth = calendar.call('local_storage', 'getItem', 'calendar_fullWidth'); + assert.strictEqual(fullWidth, undefined, + "calendar_fullWidth should be undefined in local storage"); + assert.containsNone(calendar, '.o_calendar_sidebar_container.o_sidebar_hidden', + "sidebar should be opened"); + + // click on close + await testUtils.dom.click(calendar.$('.o_calendar_sidebar_toggler')); + fullWidth = calendar.call('local_storage', 'getItem', 'calendar_fullWidth'); + assert.strictEqual(fullWidth, true, + "calendar_fullWidth should be true in local storage"); + assert.containsOnce(calendar, '.o_calendar_sidebar_container.o_sidebar_hidden', + "sidebar should be hidden"); + + // reload calendar view and check calendar view has still full width + calendar.reload(); + await testUtils.nextTick(); + assert.containsOnce(calendar, '.o_calendar_sidebar_container.o_sidebar_hidden', + "sidebar should be hidden"); + + calendar.destroy(); + }); + }); }); diff --git a/addons/web/static/tests/views/form_tests.js b/addons/web/static/tests/views/form_tests.js index 4d8b751cc1a..7c6b0c5c335 100644 --- a/addons/web/static/tests/views/form_tests.js +++ b/addons/web/static/tests/views/form_tests.js @@ -8158,6 +8158,72 @@ QUnit.module('Views', { await testUtils.nextTick(); form.destroy(); }); + + QUnit.test('resequence list lines when discardable lines are present', async function (assert) { + assert.expect(8); + + var onchangeNum = 0; + + this.data.partner.onchanges = { + p: function (obj) { + onchangeNum++; + obj.foo = obj.p.length.toString(); + }, + }; + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form>' + + '<field name="foo"/>' + + '<field name="p"/>' + + '</form>', + archs: { + 'partner,false,list': + '<tree editable="bottom">' + + '<field name="int_field" widget="handle"/>' + + '<field name="display_name" required="1"/>' + + '</tree>', + }, + }); + + assert.strictEqual(onchangeNum, 1, "one onchange happens when form is opened"); + assert.strictEqual(form.$('[name="foo"]').val(), "0", "onchange worked there is 0 line"); + + // Add one line + await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); + form.$('.o_field_one2many input:first').focus(); + await testUtils.nextTick(); + form.$('.o_field_one2many input:first').val('first line').trigger('input'); + await testUtils.nextTick(); + await testUtils.dom.click(form.$('input[name="foo"]')); + assert.strictEqual(onchangeNum, 2, "one onchange happens when a line is added"); + assert.strictEqual(form.$('[name="foo"]').val(), "1", "onchange worked there is 1 line"); + + // Drag and drop second line before first one (with 1 draft and invalid line) + await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); + await testUtils.dom.dragAndDrop( + form.$('.ui-sortable-handle').eq(0), + form.$('.o_data_row').last(), + {position: 'bottom'} + ); + assert.strictEqual(onchangeNum, 3, "one onchange happens when lines are resequenced"); + assert.strictEqual(form.$('[name="foo"]').val(), "1", "onchange worked there is 1 line"); + + // Add a second line + await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); + form.$('.o_field_one2many input:first').focus(); + await testUtils.nextTick(); + form.$('.o_field_one2many input:first').val('second line').trigger('input'); + await testUtils.nextTick(); + await testUtils.dom.click(form.$('input[name="foo"]')); + assert.strictEqual(onchangeNum, 4, "one onchange happens when a line is added"); + assert.strictEqual(form.$('[name="foo"]').val(), "2", "onchange worked there is 2 lines"); + + form.destroy(); + }); + QUnit.test('if the focus is on the discard button, hitting ESCAPE should discard', async function (assert) { assert.expect(1); diff --git a/addons/web/static/tests/views/list_tests.js b/addons/web/static/tests/views/list_tests.js index ddf340f62df..4175a7171aa 100644 --- a/addons/web/static/tests/views/list_tests.js +++ b/addons/web/static/tests/views/list_tests.js @@ -15,6 +15,7 @@ var widgetRegistry = require('web.widget_registry'); var Widget = require('web.Widget'); var _t = core._t; +var createActionManager = testUtils.createActionManager; var createView = testUtils.createView; @@ -982,6 +983,47 @@ QUnit.module('Views', { list.destroy(); }); + QUnit.test('editable list view: check that controlpanel buttons are updating when groupby applied', async function (assert) { + assert.expect(4); + + this.data.foo.fields.foo = {string: "Foo", type: "char", required:true}; + + var actionManager = await createActionManager({ + actions: [{ + id: 11, + name: 'Partners Action 11', + res_model: 'foo', + type: 'ir.actions.act_window', + views: [[3, 'list']], + search_view_id: [9, 'search'], + }], + archs: { + 'foo,3,list': '<tree editable="top"><field name="display_name"/><field name="foo"/></tree>', + + 'foo,9,search': '<search>'+ + '<filter string="candle" name="itsName" context="{\'group_by\': \'foo\'}"/>' + + '</search>', + }, + data: this.data, + }); + + await actionManager.doAction(11); + await testUtils.dom.click(actionManager.$('.o_list_button_add')); + + assert.isNotVisible(actionManager.$('.o_list_button_add'), + "create button should be invisible"); + assert.isVisible(actionManager.$('.o_list_button_save'), "save button should be visible"); + + await testUtils.dom.click(actionManager.$('.o_dropdown_toggler_btn:contains("Group By")')); + await testUtils.dom.click(actionManager.$('.o_group_by_menu .o_menu_item a:contains("candle")')); + + assert.isNotVisible(actionManager.$('.o_list_button_add'), "create button should be invisible"); + assert.isNotVisible(actionManager.$('.o_list_button_save'), + "save button should be invisible after applying groupby"); + + actionManager.destroy(); + }); + QUnit.test('selection changes are triggered correctly', async function (assert) { assert.expect(8); diff --git a/addons/web_editor/static/lib/summernote/summernote.js b/addons/web_editor/static/lib/summernote/summernote.js index 0f9dfaef656..5cebec404d9 100644 --- a/addons/web_editor/static/lib/summernote/summernote.js +++ b/addons/web_editor/static/lib/summernote/summernote.js @@ -147,7 +147,7 @@ var dialog = renderer.create('<div class="modal" aria-hidden="false" tabindex="- (options.title ? ' <div class="modal-header">' + ' <h4 class="modal-title">' + options.title + '</h4>' + - ' <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>' + + ' <button type="button" class="close" aria-label="Close"><span aria-hidden="true">×</span></button>' + ' </div>' : ''), ' <div class="modal-body">' + options.body + '</div>', (options.footer @@ -229,11 +229,16 @@ var ui = { onDialogHidden: function ($dialog, handler) { $dialog.one('hidden.bs.modal', handler); }, - showDialog: function ($dialog) { + showDialog: function ($dialog, onClose) { + $dialog.find('.close').on('click', function (ev) { + ev.preventDefault(); + ev.stopPropagation(); + onClose(); + }); $dialog.modal('show'); }, hideDialog: function ($dialog) { - $dialog.modal('hide'); + $dialog.detach().modal('hide'); }, createLayout: function ($note, options) { var $editor = (options.airMode ? ui.airEditor([ @@ -5986,7 +5991,7 @@ var LinkDialog = /** @class */ (function () { deferred.reject(); } }); - _this.ui.showDialog(_this.$dialog); + _this.ui.showDialog(_this.$dialog, _this.destroy.bind(_this)); }).promise(); }; /** @@ -6174,7 +6179,7 @@ var ImageDialog = /** @class */ (function () { deferred.reject(); } }); - _this.ui.showDialog(_this.$dialog); + _this.ui.showDialog(_this.$dialog, _this.destroy.bind(_this)); }); }; return ImageDialog; @@ -6464,7 +6469,7 @@ var VideoDialog = /** @class */ (function () { deferred.reject(); } }); - _this.ui.showDialog(_this.$dialog); + _this.ui.showDialog(_this.$dialog, _this.destroy.bind(_this)); }); }; return VideoDialog; @@ -6530,7 +6535,7 @@ var HelpDialog = /** @class */ (function () { _this.context.triggerEvent('dialog.shown'); deferred.resolve(); }); - _this.ui.showDialog(_this.$dialog); + _this.ui.showDialog(_this.$dialog, _this.destroy.bind(_this)); }).promise(); }; HelpDialog.prototype.show = function () { diff --git a/addons/web_editor/static/src/js/backend/field_html.js b/addons/web_editor/static/src/js/backend/field_html.js index 14f6d59b05c..40456961aeb 100644 --- a/addons/web_editor/static/src/js/backend/field_html.js +++ b/addons/web_editor/static/src/js/backend/field_html.js @@ -296,7 +296,7 @@ var FieldHtml = basic_fields.DebouncedField.extend(TranslatableFieldMixin, { resolver(); }; - this.$iframe.one('load', function onLoad() { + this.$iframe.on('load', function onLoad() { var _avoidDoubleLoad = ++avoidDoubleLoad; ajax.loadAsset(self.nodeOptions.cssReadonly).then(function (asset) { if (_avoidDoubleLoad !== avoidDoubleLoad) { diff --git a/addons/web_editor/static/src/js/wysiwyg/plugin/bullet.js b/addons/web_editor/static/src/js/wysiwyg/plugin/bullet.js index dc2a444384b..6a516dd2ad5 100644 --- a/addons/web_editor/static/src/js/wysiwyg/plugin/bullet.js +++ b/addons/web_editor/static/src/js/wysiwyg/plugin/bullet.js @@ -165,10 +165,10 @@ var BulletPlugin = AbstractPlugin.extend({ // convert ul <-> ol var ul; + $(ol).removeClass('o_checklist'); if (sorted === 'checklist' && current === "ul") { ul = ol; } else if (sorted === 'ul' && current === 'checklist') { - $(ol).removeClass('o_checklist'); ul = ol; } else { ul = this.document.createElement(sorted === "ol" ? "ol" : "ul"); @@ -321,7 +321,7 @@ var BulletPlugin = AbstractPlugin.extend({ var self = this; var nodes = []; var isWithinElem; - var ancestor = range.commonAncestor(); + var ancestor = range.commonAncestor() || this.editable; var $dom = $(ancestor); if (!dom.isList(ancestor)) { @@ -344,7 +344,8 @@ var BulletPlugin = AbstractPlugin.extend({ if (!$dom.length) { // if the selection is contained in a single HTML node, we indent // the first ancestor 'content block' (P, H1, PRE, ...) or TD - $dom = $(range.sc).closest(this.options.styleTags.join(',') + ',td'); + var nodes = this.context.invoke('HelperPlugin.getSelectedNodes'); + $dom = $(this._filterEditableFormatNodes(nodes)); } } diff --git a/addons/web_editor/static/src/js/wysiwyg/plugin/media.js b/addons/web_editor/static/src/js/wysiwyg/plugin/media.js index b11c15b2564..71365e4348f 100644 --- a/addons/web_editor/static/src/js/wysiwyg/plugin/media.js +++ b/addons/web_editor/static/src/js/wysiwyg/plugin/media.js @@ -44,6 +44,7 @@ var MediaPlugin = AbstractPlugin.extend({ }, mousePosition: {}, + _modalOpen: false, //-------------------------------------------------------------------------- // Public @@ -53,6 +54,10 @@ var MediaPlugin = AbstractPlugin.extend({ * Open the image dialog and listen to its saved/closed events. */ showImageDialog: function () { + if (this._modalOpen) { + return; + } + this._modalOpen = true; this.context.invoke('editor.saveRange'); var media = this.context.invoke('editor.restoreTarget'); @@ -79,6 +84,7 @@ var MediaPlugin = AbstractPlugin.extend({ }); mediaDialog.on('closed', this, function () { this.context.invoke('editor.restoreRange'); + this._modalOpen = false; }); mediaDialog.open(); }, diff --git a/addons/web_editor/static/src/js/wysiwyg/plugin/text.js b/addons/web_editor/static/src/js/wysiwyg/plugin/text.js index 16e22939cd4..f2834c531f7 100644 --- a/addons/web_editor/static/src/js/wysiwyg/plugin/text.js +++ b/addons/web_editor/static/src/js/wysiwyg/plugin/text.js @@ -229,8 +229,11 @@ var TextPlugin = AbstractPlugin.extend({ $(br).before(emptyText).remove(); range = this.context.invoke('editor.setRange', emptyText, 0, emptyText, 1); } else { + var offset = range.so; this.context.invoke('HelperPlugin.insertTextInline', '\u200B'); - range.eo += 1; + range = this.context.invoke('editor.createRange'); + range.so = offset; + range.eo = offset + 1; } range.select(); } diff --git a/addons/web_editor/static/src/js/wysiwyg/widgets/media.js b/addons/web_editor/static/src/js/wysiwyg/widgets/media.js index 545f06a38e2..198dbd14244 100644 --- a/addons/web_editor/static/src/js/wysiwyg/widgets/media.js +++ b/addons/web_editor/static/src/js/wysiwyg/widgets/media.js @@ -10,6 +10,7 @@ var fonts = require('wysiwyg.fonts'); var ImageOptimizeDialog = require('wysiwyg.widgets.image_optimize_dialog').ImageOptimizeDialog; var utils = require('web.utils'); var Widget = require('web.Widget'); +var session = require('web.session'); var QWeb = core.qweb; var _t = core._t; diff --git a/addons/web_editor/static/tests/wysiwyg_tests.js b/addons/web_editor/static/tests/wysiwyg_tests.js index e03fb3888fb..7e2c35d2dad 100644 --- a/addons/web_editor/static/tests/wysiwyg_tests.js +++ b/addons/web_editor/static/tests/wysiwyg_tests.js @@ -110,7 +110,7 @@ QUnit.test('Magic wand', async function (assert) { }); QUnit.test('Font style', function (assert) { - assert.expect(56); + assert.expect(58); return weTestUtils.createWysiwyg({ debug: false, @@ -283,6 +283,19 @@ QUnit.test('Font style', function (assert) { end: 'p:contents()[3]->2', }, }, + { + name: "Click BOLD: bold -> normal (at start of dom)", + content: '<p><b>abc</b></p>', + start: 'b:contents()[0]->0', + do: function () { + $btnBold.mousedown().click(); + }, + test: { + content: '<p>\u200B<b>abc</b></p>', + start: 'p:contents()[0]->1', + end: 'p:contents()[0]->1', + }, + }, /* ITALIC */ { name: "Click ITALIC: bold -> bold + italic", @@ -2227,7 +2240,7 @@ QUnit.test('Align', function (assert) { }); QUnit.test('Indent/outdent', function (assert) { - assert.expect(18); + assert.expect(20); return weTestUtils.createWysiwyg({ debug: false, @@ -2269,6 +2282,21 @@ QUnit.test('Indent/outdent', function (assert) { start: 'p:eq(1):contents()[0]->1', }, }, + { + name: "Click INDENT: li -> indented li", + content: '<p>aaa</p><p>bbb</p><p>ccc</p><p>ddd</p>', + start: 'p:eq(1):contents()[0]->0', + end: 'p:eq(3):contents()[0]->3', + do: function () { + $dropdownPara.mousedown().click(); + $btnIndent.mousedown().click(); + }, + test: { + content: '<p>aaa</p><p style="margin-left: 1.5em;">bbb</p><p style="margin-left: 1.5em;">ccc</p><p style="margin-left: 1.5em;">ddd</p>', + start: 'p:eq(1):contents()[0]->0', + end: 'p:eq(3):contents()[0]->3', + }, + }, /* OUTDENT */ { name: "Click OUTDENT: indented p -> p", diff --git a/addons/web_tour/static/src/js/tour_manager.js b/addons/web_tour/static/src/js/tour_manager.js index 09abee2e767..d117c24f919 100644 --- a/addons/web_tour/static/src/js/tour_manager.js +++ b/addons/web_tour/static/src/js/tour_manager.js @@ -213,6 +213,9 @@ return core.Class.extend(mixins.EventDispatcherMixin, ServicesMixin, { * @returns {boolean} true if a tip was found and activated/updated */ _check_for_tooltip: function (tip, tour_name) { + if (tip === undefined) { + return true; + } if ($('body').hasClass('o_ui_blocked')) { this._deactivate_tip(tip); this._log.push("blockUI is preventing the tip to be consumed"); diff --git a/addons/website/models/res_users.py b/addons/website/models/res_users.py index a84eaaeb3c6..8902e4c34b6 100644 --- a/addons/website/models/res_users.py +++ b/addons/website/models/res_users.py @@ -2,8 +2,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. import logging -from odoo import api, fields, models, tools, _ -from odoo.exceptions import ValidationError +from odoo import api, fields, models, tools from odoo.http import request _logger = logging.getLogger(__name__) @@ -12,8 +11,8 @@ _logger = logging.getLogger(__name__) class ResUsers(models.Model): _inherit = 'res.users' - website_id = fields.Many2one('website', related='partner_id.website_id', store=True, related_sudo=False, readonly=True) - + website_id = fields.Many2one('website', related='partner_id.website_id', store=True, related_sudo=False, readonly=False) + _sql_constraints = [ # Partial constraint, complemented by unique index (see below). Still # useful to keep because it provides a proper error message when a @@ -51,7 +50,7 @@ class ResUsers(models.Model): @api.model_cr_context def _auto_init(self): result = super(ResUsers, self)._auto_init() - # Use unique index to implement unique constraint per website, even if website_id is null + # Use unique index to implement unique constraint per website, even if website_id is null # (not possible using a constraint) tools.create_unique_index(self._cr, 'res_users_login_key_unique_website_index', self._table, ['login', 'COALESCE(website_id,-1)']) diff --git a/addons/website/models/website.py b/addons/website/models/website.py index bbc8489cf47..5202798fb97 100644 --- a/addons/website/models/website.py +++ b/addons/website/models/website.py @@ -149,14 +149,17 @@ class Website(models.Model): @api.multi def write(self, values): + public_user_to_change_websites = self.env['website'] self._handle_favicon(values) self._get_languages.clear_cache(self) if 'company_id' in values and 'user_id' not in values: - company = self.env['res.company'].browse(values['company_id']) - values['user_id'] = company._get_public_user().id + public_user_to_change_websites = self.filtered(lambda w: w.user_id.company_id.id != values['company_id']) + if public_user_to_change_websites: + company = self.env['res.company'].browse(values['company_id']) + super(Website, public_user_to_change_websites).write(dict(values, user_id=company._get_public_user().id)) - result = super(Website, self).write(values) + result = super(Website, self - public_user_to_change_websites).write(values) if 'cdn_activated' in values or 'cdn_url' in values or 'cdn_filters' in values: # invalidate the caches from static node at compile time self.env['ir.qweb'].clear_caches() @@ -185,6 +188,8 @@ class Website(models.Model): self.homepage_id = self.env['website.page'].search([('website_id', '=', self.id), ('key', '=', standard_homepage.key)]) + # prevent /-1 as homepage URL + self.homepage_id.url = '/' # Bootstrap default menu hierarchy, create a new minimalist one if no default default_menu = self.env.ref('website.main_menu') diff --git a/addons/website/static/src/js/content/snippets.animation.js b/addons/website/static/src/js/content/snippets.animation.js index 7f57eb81f2c..33e1e01ebcc 100644 --- a/addons/website/static/src/js/content/snippets.animation.js +++ b/addons/website/static/src/js/content/snippets.animation.js @@ -860,6 +860,10 @@ registry.gallerySlider = publicWidget.Widget.extend({ destroy: function () { this._super.apply(this, arguments); + if (!this.$indicator) { + return; + } + this.$prev.prependTo(this.$indicator); this.$next.appendTo(this.$indicator); this.$carousel.off('.gallery_slider'); diff --git a/addons/website/static/src/js/content/website_root.js b/addons/website/static/src/js/content/website_root.js index 22c71890643..612265124cd 100644 --- a/addons/website/static/src/js/content/website_root.js +++ b/addons/website/static/src/js/content/website_root.js @@ -19,6 +19,7 @@ var WebsiteRoot = publicRootData.PublicRoot.extend({ }), custom_events: _.extend({}, publicRootData.PublicRoot.prototype.custom_events || {}, { 'ready_to_clean_for_save': '_onWidgetsStopRequest', + seo_object_request: '_onSeoObjectRequest', }), /** @@ -111,6 +112,36 @@ var WebsiteRoot = publicRootData.PublicRoot.extend({ window.location.href = _.str.sprintf("/website/lang/%(lang)s?r=%(url)s%(hash)s", redirect); }, /** + /** + * Checks information about the page SEO object. + * + * @private + * @param {OdooEvent} ev + */ + _onSeoObjectRequest: function (ev) { + var res = this._unslugHtmlDataObject('seo-object'); + ev.data.callback(res); + }, + /** + * Returns a model/id object constructed from html data attribute. + * + * @private + * @param {string} dataAttr + * @returns {Object} an object with 2 keys: model and id, or null + * if not found + */ + _unslugHtmlDataObject: function (dataAttr) { + var repr = $('html').data(dataAttr); + var match = repr && repr.match(/(.+)\((\d+),(.*)\)/); + if (!match) { + return null; + } + return { + model: match[1], + id: match[2] | 0, + }; + }, + /** * @todo review * @private */ diff --git a/addons/website/static/src/js/menu/seo.js b/addons/website/static/src/js/menu/seo.js index 332fde71a4f..b65137ecc8f 100644 --- a/addons/website/static/src/js/menu/seo.js +++ b/addons/website/static/src/js/menu/seo.js @@ -129,17 +129,17 @@ var Keyword = Widget.extend({ _updateTitle: function () { var $title = this.$('.js_seo_keyword_title'); if (this.htmlPage.isInTitle(this.keyword)) { - $title.css('visibility','visible'); + $title.css('visibility', 'visible'); } else { - $title.css('visibility','hidden'); + $title.css('visibility', 'hidden'); } }, _updateDescription: function () { var $description = this.$('.js_seo_keyword_description'); if (this.htmlPage.isInDescription(this.keyword)) { - $description.css('visibility','visible'); + $description.css('visibility', 'visible'); } else { - $description.css('visibility','hidden'); + $description.css('visibility', 'hidden'); } }, }); @@ -210,7 +210,7 @@ var Preview = Widget.extend({ this.url = options.url; this.description = options.description || _t("The description will be generated by search engines based on page content unless you specify one."); if (this.description.length > 160) { - this.description = this.description.substring(0,159) + '…'; + this.description = this.description.substring(0, 159) + '…'; } this._super(parent); }, @@ -289,19 +289,19 @@ var HtmlPage = Class.extend(mixins.PropertiesMixin, { return $('body').children().not('.oe_seo_configuration').find('h2').text(); }, isInBody: function (text) { - return new RegExp(WORD_SEPARATORS_REGEX+text+WORD_SEPARATORS_REGEX, 'gi').test(this.bodyText()); + return new RegExp(WORD_SEPARATORS_REGEX + text + WORD_SEPARATORS_REGEX, 'gi').test(this.bodyText()); }, isInTitle: function (text) { - return new RegExp(WORD_SEPARATORS_REGEX+text+WORD_SEPARATORS_REGEX, 'gi').test(this.title()); + return new RegExp(WORD_SEPARATORS_REGEX + text + WORD_SEPARATORS_REGEX, 'gi').test(this.title()); }, isInDescription: function (text) { - return new RegExp(WORD_SEPARATORS_REGEX+text+WORD_SEPARATORS_REGEX, 'gi').test(this.description()); + return new RegExp(WORD_SEPARATORS_REGEX + text + WORD_SEPARATORS_REGEX, 'gi').test(this.description()); }, isInHeading1: function (text) { - return new RegExp(WORD_SEPARATORS_REGEX+text+WORD_SEPARATORS_REGEX, 'gi').test(this.heading1()); + return new RegExp(WORD_SEPARATORS_REGEX + text + WORD_SEPARATORS_REGEX, 'gi').test(this.heading1()); }, isInHeading2: function (text) { - return new RegExp(WORD_SEPARATORS_REGEX+text+WORD_SEPARATORS_REGEX, 'gi').test(this.heading2()); + return new RegExp(WORD_SEPARATORS_REGEX + text + WORD_SEPARATORS_REGEX, 'gi').test(this.heading2()); }, }); @@ -415,7 +415,7 @@ var MetaTitleDescription = Widget.extend({ _renderPreview: function () { var indexed = this.isIndexed; var preview = ""; - if (indexed){ + if (indexed) { preview = new Preview(this, { title: this.getTitle(), description: this.getDescription(), @@ -447,7 +447,7 @@ var MetaKeywords = Widget.extend({ start: function () { var self = this; this.$input = this.$('input[name=website_meta_keywords]'); - this.keywordList = new KeywordList(this, { htmlPage: this.htmlPage }); + this.keywordList = new KeywordList(this, {htmlPage: this.htmlPage}); this.keywordList.on('list-full', this, function () { self.$input.attr({ readonly: 'readonly', @@ -494,7 +494,7 @@ var MetaKeywords = Widget.extend({ model: 'website', method: 'get_languages', args: [[context.website_id]], - }).then( function (data) { + }).then(function (data) { self.$('#language-box').html(core.qweb.render('Configurator.language_promote', { 'language': data, 'def_lang': context.lang @@ -508,7 +508,7 @@ var MetaKeywords = Widget.extend({ * @param {boolean} removed: a keyword is about to be removed, * we need to exclude it from the count */ - _updateTable : function (removed) { + _updateTable: function (removed) { var min = removed ? 1 : 0; if (this.keywordList.keywords().length > min) { this.$('table').show(); @@ -562,7 +562,7 @@ var MetaImageSelector = Widget.extend({ _setDescription: function (description) { this.metaDescription = description || _t("The description will be generated by social media based on page content unless you specify one."); if (this.metaDescription.length > 160) { - this.metaDescription = this.metaDescription.substring(0,159) + '…'; + this.metaDescription = this.metaDescription.substring(0, 159) + '…'; } }, @@ -731,19 +731,25 @@ var SeoConfigurator = Dialog.extend({ }); }, getMainObject: function () { - var repr = $('html').data('main-object'); - var m = repr.match(/(.+)\((\d+),(.*)\)/); - if (!m) { - return null; - } else { - return { - model: m[1], - id: m[2]|0 - }; - } + var mainObject; + this.trigger_up('main_object_request', { + callback: function (value) { + mainObject = value; + }, + }); + return mainObject; + }, + getSeoObject: function () { + var seoObject; + this.trigger_up('seo_object_request', { + callback: function (value) { + seoObject = value; + }, + }); + return seoObject; }, loadMetaData: function () { - var obj = this.getMainObject(); + var obj = this.getSeoObject() || this.getMainObject(); return new Promise(function (resolve, reject) { if (!obj) { // return Promise.reject(new Error("No main_object was found.")); @@ -751,7 +757,7 @@ var SeoConfigurator = Dialog.extend({ } else { var fields = ['website_meta_title', 'website_meta_description', 'website_meta_keywords' ,'website_meta_og_img']; - if (obj.model === 'website.page'){ + if (obj.model === 'website.page') { fields.push('website_indexed'); fields.push('website_id'); } @@ -772,7 +778,7 @@ var SeoConfigurator = Dialog.extend({ }); }, saveMetaData: function (data) { - var obj = this.getMainObject(); + var obj = this.getSeoObject() || this.getMainObject(); if (!obj) { return Promise.reject(); } else { diff --git a/addons/website/static/tests/tours/rte.js b/addons/website/static/tests/tours/rte.js index e29f40e45ed..716562389e7 100644 --- a/addons/website/static/tests/tours/rte.js +++ b/addons/website/static/tests/tours/rte.js @@ -13,6 +13,7 @@ var ready = Promise.all([domReady, session.is_bound, ajax.loadXML()]); tour.register('rte_translator', { test: true, + url: '/', wait_for: ready, url: '/', }, [{ diff --git a/addons/website/tests/test_get_current_website.py b/addons/website/tests/test_get_current_website.py index a71126cb484..57d85887b4d 100644 --- a/addons/website/tests/test_get_current_website.py +++ b/addons/website/tests/test_get_current_website.py @@ -112,3 +112,10 @@ class TestGetCurrentWebsite(TransactionCase): self.assertEqual(Website._get_current_website_id('site-1.com:81', False), website2.id) self.assertEqual(Website._get_current_website_id('site-1.com:82', False), website1.id) self.assertEqual(Website._get_current_website_id('site-1.com', False), website1.id) + + def test_02_signup_user_website_id(self): + website = self.env['website'].browse(1) + website.specific_user_account = True + + user = self.env['res.users'].create({'website_id': website.id, 'login': 'sad@mail.com', 'name': 'Hope Fully'}) + self.assertTrue(user.website_id == user.partner_id.website_id == website) diff --git a/addons/website/views/website_templates.xml b/addons/website/views/website_templates.xml index 583e019e20e..4dd19528c0d 100644 --- a/addons/website/views/website_templates.xml +++ b/addons/website/views/website_templates.xml @@ -169,7 +169,8 @@ 'data-view-xmlid': xmlid if editable or translatable else None, 'data-viewid': viewid if editable or translatable else None, 'data-main-object': repr(main_object) if editable or translatable else None, - 'data-oe-company-name': res_company.name + 'data-seo-object': repr(seo_object) if seo_object else None, + 'data-oe-company-name': res_company.name, }"/> </xpath> <xpath expr="//head" position="before"> @@ -178,8 +179,9 @@ <t t-set="additional_title" t-value="main_object.name"/> </t> <t t-set="default_title"> <t t-if="additional_title"><t t-raw="additional_title"/> | </t><t t-raw="(website or res_company).name"/> </t> - <t t-if="main_object and 'website_meta_title' in main_object and main_object.website_meta_title"> - <t t-set="title" t-value="main_object.website_meta_title"/> + <t t-set="seo_object" t-value="seo_object or main_object"/> + <t t-if="seo_object and 'website_meta_title' in seo_object and seo_object.website_meta_title"> + <t t-set="title" t-value="seo_object.website_meta_title"/> </t> <t t-else=""> <t t-set="title" t-value="default_title"></t> @@ -189,17 +191,18 @@ </xpath> <xpath expr="//head/meta[last()]" position="after"> <meta name="generator" content="Odoo"/> - <t t-set="website_meta" t-value="main_object and main_object.get_website_meta() or {}"/> + <t t-set="website_meta" t-value="seo_object and seo_object.get_website_meta() or {}"/> <meta name="default_title" t-att-content="default_title" groups="website.group_website_designer"/> <meta t-if="main_object and 'website_indexed' in main_object and not main_object.website_indexed" name="robots" content="noindex"/> - <t t-set="meta_description" t-value="main_object and 'website_meta_description' in main_object - and main_object.website_meta_description or website_meta_description or website_meta.get('meta_description', '')"/> - <t t-set="meta_keywords" t-value="main_object and 'website_meta_keywords' in main_object - and main_object.website_meta_keywords or website_meta_keywords"/> + <t t-set="seo_object" t-value="seo_object or main_object"/> + <t t-set="meta_description" t-value="seo_object and 'website_meta_description' in seo_object + and seo_object.website_meta_description or website_meta_description or website_meta.get('meta_description', '')"/> + <t t-set="meta_keywords" t-value="seo_object and 'website_meta_keywords' in seo_object + and seo_object.website_meta_keywords or website_meta_keywords"/> <meta t-if="meta_description or editable" name="description" t-att-content="meta_description"/> <meta t-if="meta_keywords or editable" name="keywords" t-att-content="meta_keywords"/> - <t t-if="main_object"> + <t t-if="seo_object"> <meta name="default_description" t-att-content="website_meta_description or website_meta.get('meta_description')" groups="website.group_website_designer"/> <!-- OpenGraph tags for Facebook sharing --> <t t-set="opengraph_meta" t-value="website_meta.get('opengraph_meta')"/> @@ -399,7 +402,7 @@ </div> </li> <li groups="website.group_website_publisher" class="list-inline-item"> - <t t-set="url_return" t-value="url_for('', '[lang]') + '?' + keep_query()"/> + <t t-set="url_return" t-value="quote_plus(url_for('', '[lang]') + '?' + keep_query())"/> <a class="d-none d-sm-block" t-attf-href="/web#action=base.action_view_base_language_install&website_id=#{website.id if website else ''}&url_return=#{url_return}"> <i class="fa fa-plus-circle"/> Add a language... diff --git a/addons/website_blog/views/snippets.xml b/addons/website_blog/views/snippets.xml index f8564f6b254..8702928d12d 100644 --- a/addons/website_blog/views/snippets.xml +++ b/addons/website_blog/views/snippets.xml @@ -74,5 +74,8 @@ </div> </div> </xpath> + <xpath expr="//div[@data-js='layout_column']" position="attributes"> + <attribute name="data-exclude" add=".s_latest_posts, .s_latest_posts_big_picture" separator=","/> + </xpath> </template> </odoo> diff --git a/addons/website_crm_partner_assign/tests/test_partner_assign.py b/addons/website_crm_partner_assign/tests/test_partner_assign.py index 720c9afb65f..dce67bb4f03 100644 --- a/addons/website_crm_partner_assign/tests/test_partner_assign.py +++ b/addons/website_crm_partner_assign/tests/test_partner_assign.py @@ -16,6 +16,19 @@ class TestPartnerAssign(TransactionCase): def setUp(self): super(TestPartnerAssign, self).setUp() + self.customer_uk = self.env['res.partner'].create({ + 'name': 'Nigel', + 'country_id': self.env.ref('base.uk').id, + 'city': 'Birmingham', + 'zip': 'B46 3AG', + 'street': 'Cannon Hill Park', + }) + self.lead_uk = self.env['crm.lead'].create({ + 'type': 'opportunity', + 'name': 'Office Design and Architecture', + 'partner_id': self.customer_uk.id + }) + def geo_find(addr): return { 'Wavre, Belgium': (50.7158956, 4.6128075), @@ -46,7 +59,7 @@ class TestPartnerAssign(TransactionCase): "partner_weight": 10, }) - lead = self.env.ref('crm.crm_case_21') + lead = self.lead_uk # In order to test find nearest Partner functionality and assign to opportunity, # I Set Geo Lattitude and Longitude according to partner address. diff --git a/addons/website_crm_partner_assign/views/website_crm_partner_assign_templates.xml b/addons/website_crm_partner_assign/views/website_crm_partner_assign_templates.xml index bd7c88b3e43..21b70a11112 100644 --- a/addons/website_crm_partner_assign/views/website_crm_partner_assign_templates.xml +++ b/addons/website_crm_partner_assign/views/website_crm_partner_assign_templates.xml @@ -164,13 +164,9 @@ <t t-if="any([p.website_published for p in partner.implemented_partner_ids])"> <h3 id="references">References</h3> <div t-foreach="partner.implemented_partner_ids" t-if="reference.website_published" t-as="reference" class="media mt-3"> - <a t-attf-href="/customers/#{slug(reference)}"> - <span t-field="reference.image_medium" t-options='{"widget": "image", "class": "d-block mr-3"}'/> - </a> + <span t-field="reference.image_medium" t-options='{"widget": "image", "class": "d-block mr-3"}'/> <div class="media-body" style="min-height: 64px;"> - <a t-attf-href="/customers/#{slug(reference)}"> - <span t-field="reference.self"/> - </a> + <span t-field="reference.self"/> <div t-field='reference.website_short_description'/> </div> </div> diff --git a/addons/website_customer/views/website_customer_templates.xml b/addons/website_customer/views/website_customer_templates.xml index da6e9aa8a33..9cd668d901d 100644 --- a/addons/website_customer/views/website_customer_templates.xml +++ b/addons/website_customer/views/website_customer_templates.xml @@ -234,4 +234,13 @@ </t> </template> +<template id="references_block_href" inherit_id="website_crm_partner_assign.references_block" name="Partner References Block"> + <xpath expr="//div/span" position="replace"> + <a t-attf-href="/customers/#{slug(reference)}">$0</a> + </xpath> + <xpath expr="//div[hasclass('media-body')]/span" position="replace"> + <a t-attf-href="/customers/#{slug(reference)}">$0</a> + </xpath> +</template> + </odoo> diff --git a/addons/website_event/controllers/main.py b/addons/website_event/controllers/main.py index 1174f91d40e..edd01ebbe19 100644 --- a/addons/website_event/controllers/main.py +++ b/addons/website_event/controllers/main.py @@ -148,14 +148,15 @@ class WebsiteEventController(http.Controller): values = { 'event': event, - 'main_object': event } if '.' not in page: page = 'website_event.%s' % page try: - request.website.get_template(page) + # Every event page view should have its own SEO. + values['seo_object'] = request.website.get_template(page) + values['main_object'] = event except ValueError: # page not found values['path'] = re.sub(r"^website_event\.", '', page) diff --git a/addons/website_event/views/event_templates.xml b/addons/website_event/views/event_templates.xml index 4231efb2907..36228dfb580 100644 --- a/addons/website_event/views/event_templates.xml +++ b/addons/website_event/views/event_templates.xml @@ -529,20 +529,20 @@ </strong> </h4> <div class="row d-none d-lg-flex"> - <div class="col-4"> + <div class="col-md"> <strong>Name</strong> </div> - <div class="col-4"> + <div class="col-md"> <strong>Email</strong> </div> - <div class="col-4"> - <strong>Phone <small>(optional)</small></strong> + <div class="col-md"> + <strong>Phone <small class='lbl_optional'>(optional)</small></strong> </div> </div> <t t-foreach="range(1, ticket['quantity'] + 1)" t-as="att_counter" name="attendee_loop"> <t t-set="counter" t-value="counter + 1"/> <div class="row py-2 o_wevent_registration_list_attendees"> - <div class="col-12 col-lg-4"> + <div class="col-md"> <div class="mb-1 d-lg-none"> <strong>Name</strong> </div> @@ -550,7 +550,7 @@ <input class='form-control' type='text' t-attf-name="#{counter}-name" required="This field is required" t-att-placeholder="attendee_placeholder %counter"/> </div> - <div class="col-12 col-lg-4"> + <div class="col-md"> <div class="mt-1 d-lg-none"> <strong>Email</strong> </div> @@ -558,9 +558,9 @@ <input class='form-control' type='email' t-attf-name="#{counter}-email" required="This field is required"/> </div> </div> - <div class="col-12 col-lg-4"> + <div class="col-md"> <div class="mt-1 d-lg-none"> - <strong>Phone <small>(optional)</small></strong> + <strong>Phone <small class='lbl_optional'>(optional)</small></strong> </div> <input class='form-control' type='tel' t-attf-name="#{counter}-phone"/> </div> diff --git a/addons/website_forum/controllers/main.py b/addons/website_forum/controllers/main.py index ed2cb30df2e..bd62e519338 100644 --- a/addons/website_forum/controllers/main.py +++ b/addons/website_forum/controllers/main.py @@ -439,7 +439,7 @@ class WebsiteForum(WebsiteProfile): values = self._prepare_user_values(forum=forum) values.update({ - 'posts_ids': posts_to_validate_ids, + 'posts_ids': posts_to_validate_ids.sudo(), 'queue_type': 'validation', }) @@ -459,7 +459,7 @@ class WebsiteForum(WebsiteProfile): values = self._prepare_user_values(forum=forum) values.update({ - 'posts_ids': flagged_posts_ids, + 'posts_ids': flagged_posts_ids.sudo(), 'queue_type': 'flagged', 'flagged_queue_active': 1, }) @@ -478,7 +478,7 @@ class WebsiteForum(WebsiteProfile): values = self._prepare_user_values(forum=forum) values.update({ - 'posts_ids': offensive_posts_ids, + 'posts_ids': offensive_posts_ids.sudo(), 'queue_type': 'offensive', }) diff --git a/addons/website_forum/models/forum.py b/addons/website_forum/models/forum.py index 75039efff89..bf0effb67c6 100644 --- a/addons/website_forum/models/forum.py +++ b/addons/website_forum/models/forum.py @@ -251,10 +251,11 @@ class Post(models.Model): operator = operator == "=" and '!=' or '=' value = True - if self._uid == SUPERUSER_ID: + user = self.env.user + # Won't impact sitemap, search() in converter is forced as public user + if user._is_admin(): return [(1, '=', 1)] - user = self.env['res.users'].browse(self._uid) req = """ SELECT p.id FROM forum_post p diff --git a/addons/website_forum/models/res_users.py b/addons/website_forum/models/res_users.py index 67326175ac9..35a5fc32545 100644 --- a/addons/website_forum/models/res_users.py +++ b/addons/website_forum/models/res_users.py @@ -1,27 +1,12 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -import hashlib - -from datetime import datetime - -from werkzeug import urls - from odoo import api, fields, models class Users(models.Model): _inherit = 'res.users' - def __init__(self, pool, cr): - init_res = super(Users, self).__init__(pool, cr) - type(self).SELF_WRITEABLE_FIELDS = list( - set( - self.SELF_WRITEABLE_FIELDS + - ['country_id', 'city', 'website', 'website_description', 'website_published'])) - type(self).SELF_READABLE_FIELDS = type(self).SELF_READABLE_FIELDS + ['karma'] - return init_res - create_date = fields.Datetime('Create Date', readonly=True, index=True) forum_waiting_posts_count = fields.Integer('Waiting post', compute="_get_user_waiting_post") diff --git a/addons/website_forum/static/src/scss/website_forum.scss b/addons/website_forum/static/src/scss/website_forum.scss index d0cfce94ec6..018bf679e8a 100644 --- a/addons/website_forum/static/src/scss/website_forum.scss +++ b/addons/website_forum/static/src/scss/website_forum.scss @@ -22,6 +22,7 @@ $bronze: #eea91e; font-style: italic; font-weight: lighter; } + word-wrap: break-word; } .vote { diff --git a/addons/website_forum/views/website_forum.xml b/addons/website_forum/views/website_forum.xml index 24fdc26feb3..f971c068f8e 100644 --- a/addons/website_forum/views/website_forum.xml +++ b/addons/website_forum/views/website_forum.xml @@ -60,7 +60,7 @@ <div id="wrap" t-attf-class="container #{website_forum_action}"> <div class="row"> - <div class="col o_wprofile_email_validation_container flex-grow-1 mb16 mt16"> + <div class="col o_wprofile_email_validation_container mb16 mt16 col-lg-9"> <t t-call="website_profile.email_validation_banner"> <t t-set="redirect_url" t-value="'/forum/%s' % forum.id"/> <t t-set="send_validation_email_message" t-value="'Click here to send a verification email allowing you to participate to the forum.'"/> @@ -68,66 +68,65 @@ </t> <t t-raw="0"/> </div> - <t t-if="not is_profile_page"> - <div class="col-lg-3" id="right-column"> - <a role="button" type="button" class="btn btn-primary btn-lg btn-block mb-3" t-attf-href="/forum/#{slug(forum)}/ask">Ask a Question</a> - <div class="card bg-secondary" t-if="question"> - <h6 class="card-header"><b>Writer</b></h6> - <div class="card-body o_card_people"> - <img class="o_wforum_people_photo_container d-block mx-auto mb-3" t-attf-src="/web/image/res.users/#{question.create_uid.id}/image_medium"/> - <h6 class="text-center"> - <b><i class="fa fa-diamond text-secondary"/> <t t-esc="question.create_uid.sudo().karma"/></b> - <b><a t-attf-href="/forum/#{slug(forum)}/user/#{question.create_uid.sudo().id}" t-field="question.create_uid" t-options='{"widget": "contact", "fields": ["name", "country_id"]}'/></b> - </h6> - </div> - </div> - <div class="card my-3 bg-secondary" t-if="question and not len(question.tag_ids)<=0"> - <h6 class="card-header"> - <b> - <t t-if="len(question.tag_ids)<=1"> - Tag - </t> - <t t-else=""> - Tags - </t> - </b> + <div class="col-lg-3" id="right-column" t-if="not is_profile_page"> + <a role="button" type="button" class="btn btn-primary btn-lg btn-block mb-3" t-attf-href="/forum/#{slug(forum)}/ask">Ask a Question</a> + <div class="card bg-secondary" t-if="question"> + <h6 class="card-header"><b>Writer</b></h6> + <div class="card-body o_card_people"> + <img class="o_wforum_people_photo_container d-block mx-auto mb-3" t-attf-src="/web/image/res.users/#{question.create_uid.id}/image_medium"/> + <h6 class="text-center"> + <b><i class="fa fa-diamond text-secondary"/> <t t-esc="question.create_uid.sudo().karma"/></b> + <b><a t-attf-href="/forum/#{slug(forum)}/user/#{question.create_uid.sudo().id}" t-field="question.create_uid" t-options='{"widget": "contact", "fields": ["name", "country_id"]}'/></b> </h6> - <div class="card-body"> - <t t-foreach="question.tag_ids" t-as="tag"> - <a t-attf-href="/forum/#{ slug(forum) }/tag/#{ tag.id }/questions" class="badge badge-primary" t-field="tag.name"/> - </t> - </div> </div> - <div class="card my-3 bg-secondary" t-if="question"> - <h6 class="card-header"><b>Keep Informed</b></h6> - <div class="card-body o_subscribe_post"> - <t t-call="website_mail.follow"> - <t t-set="object" t-value="question"/> - <t t-set="div_class" t-value="'mx-auto'"/> + </div> + <div class="card my-3 bg-secondary" t-if="question and not len(question.tag_ids)<=0"> + <h6 class="card-header"> + <b> + <t t-if="len(question.tag_ids)<=1"> + Tag </t> - <div class="text-center mt-3"> - <strong><t t-raw="len(question.message_follower_ids)"/> follower(s)</strong> - </div> - </div> + <t t-else=""> + Tags + </t> + </b> + </h6> + <div class="card-body"> + <t t-foreach="question.tag_ids" t-as="tag"> + <a t-attf-href="/forum/#{ slug(forum) }/tag/#{ tag.id }/questions" class="badge badge-primary" t-field="tag.name"/> + </t> </div> - <div class="card my-3 bg-secondary" id="about_forum"> - <h6 class="card-header"><b>About This Community</b></h6> - <div class="card-body"> - <span t-field="forum.description"/> - <a t-if="not header.get('is_guidelines')" t-attf-href="/forum/#{slug(forum)}/faq" class="btn btn-primary mt-3">Read Guidelines</a> - <a t-if="header.get('is_guidelines')" t-attf-href="/forum/#{slug(forum)}" class="btn btn-primary mt-3"> Back to <span t-field="forum.name"/></a> + </div> + <div class="card my-3 bg-secondary" t-if="question"> + <h6 class="card-header"><b>Keep Informed</b></h6> + <div class="card-body o_subscribe_post"> + <t t-call="website_mail.follow"> + <t t-set="object" t-value="question"/> + <t t-set="div_class" t-value="'mx-auto'"/> + </t> + <div class="text-center mt-3"> + <strong><t t-raw="len(question.message_follower_ids)"/> follower(s)</strong> </div> </div> - <div class="card my-3 bg-secondary" id="moderation_tools" t-if="user.karma>=forum.karma_moderate"> - <h6 class="card-header"><b>Moderation Tools</b></h6> - <div class="card-body"> - <a t-attf-href="/forum/#{slug(forum)}/validation_queue" class="text-dark"><i class="fa fa-check-square-o mr-2 text-primary"/><strong><t t-esc="forum.count_posts_waiting_validation"/></strong> waiting for validation</a> - <hr style="margin-top:4px;margin-bottom:0;padding-bottom:4px;" /> - <a t-attf-href="/forum/#{slug(forum)}/flagged_queue" class="text-dark"><i class="fa fa-flag mr-2 text-primary"/><strong id="count_flagged_posts"><t t-esc="forum.count_flagged_posts"/></strong> post<t t-if="forum.count_flagged_posts>1">s</t> flagged</a> - </div> + </div> + <div class="card my-3 bg-secondary" id="about_forum"> + <h6 class="card-header"><b>About This Community</b></h6> + <div class="card-body"> + <span t-field="forum.description"/> + <a t-if="not header.get('is_guidelines')" t-attf-href="/forum/#{slug(forum)}/faq" class="btn btn-primary mt-3">Read Guidelines</a> + <a t-if="header.get('is_guidelines')" t-attf-href="/forum/#{slug(forum)}" class="btn btn-primary mt-3"> Back to <span t-field="forum.name"/></a> </div> </div> - </t> + <div class="card my-3 bg-secondary" id="moderation_tools" t-if="user.karma>=forum.karma_moderate"> + <h6 class="card-header"><b>Moderation Tools</b></h6> + <div class="card-body"> + <a t-attf-href="/forum/#{slug(forum)}/validation_queue" class="text-dark"><i class="fa fa-check-square-o mr-2 text-primary"/><strong><t t-esc="forum.count_posts_waiting_validation"/></strong> waiting for validation</a> + <hr style="margin-top:4px;margin-bottom:0;padding-bottom:4px;" /> + <a t-attf-href="/forum/#{slug(forum)}/flagged_queue" class="text-dark"><i class="fa fa-flag mr-2 text-primary"/><strong id="count_flagged_posts"><t t-esc="forum.count_flagged_posts"/></strong> post<t t-if="forum.count_flagged_posts>1">s</t> flagged</a> + </div> + </div> + </div> + </div> </div> <div class="oe_structure" id="oe_structure_website_forum_header_1"/> @@ -707,7 +706,7 @@ </div> </t> </div> - <div t-attf-class="col question-block mb-5 o_post_content" role="alert"> + <div t-attf-class="col-11 question-block mb-5 o_post_content" role="alert"> <span t-if="question.has_validated_answer" title="Question has accepted answer" role="img" aria-label="Question has accepted answer"><i class="fa fa-check-circle text-success"/> <b class="text-success">Resolved</b></span> <h1 t-attf-class="#{not question.active and 'text-danger' or 'text-primary'}"> <t t-esc="question.name"/> @@ -862,7 +861,7 @@ </div> </t> </div> - <div class="col"> + <div class="col-11"> <t t-if="answer.can_comment"> <div class="dropdown dropleft float-right"> <a class="btn bg-white" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> @@ -1004,14 +1003,14 @@ <t t-set="inDropdown" t-value="True"/> <t t-set="icon" t-value="'fa-trash-o text-primary'"/> <t t-set="classes" t-value="'comment_delete border-bottom'"/> - <t t-set="karma" t-value="not object.can_unlink and answer.karma_unlink or 0"/> + <t t-set="karma" t-value="not object.can_unlink and object.karma_unlink or 0"/> </t> <t t-call="website_forum.link_button"> <t t-set="url" t-value="'/forum/' + slug(forum) + '/post/' + slug(object) + '/comment/' + slug(message) + '/convert_to_answer'"/> <t t-set="label">Convert as a answer</t> <t t-set="inDropdown" t-value="True"/> <t t-set="icon" t-value="'fa-magic text-primary'"/> - <t t-set="karma" t-value="not object.can_comment_convert and answer.karma_comment_convert or 0"/> + <t t-set="karma" t-value="not object.can_comment_convert and object.karma_comment_convert or 0"/> </t> </div> </div> @@ -1179,7 +1178,7 @@ </div> </div> <t t-if="question.parent_id"> - <div t-foreach="question" t-as="answer" class="col"> + <div t-foreach="question" t-as="answer" class="col-md-10 col-sm-8"> <t t-call="website_forum.moderation_display_post_answer"/> <small class="text-muted"> <i t-attf-class="fa fa-user" role="img" aria-label="Question" /> diff --git a/addons/website_links/static/src/js/website_links_menu.js b/addons/website_links/static/src/js/website_links_menu.js index 5ebc7f98c77..6f673223720 100644 --- a/addons/website_links/static/src/js/website_links_menu.js +++ b/addons/website_links/static/src/js/website_links_menu.js @@ -8,9 +8,9 @@ odoo.define('website_links.website_links_menu', function (require) { 'use strict'; var publicWidget = require('web.public.widget'); +var websiteNavbarData = require('website.navbar'); -publicWidget.registry.websiteLinksMenu = publicWidget.Widget.extend({ - selector: '#o_website_links_share_page', +var WebsiteLinksMenu = publicWidget.Widget.extend({ /** * @override @@ -20,4 +20,7 @@ publicWidget.registry.websiteLinksMenu = publicWidget.Widget.extend({ return this._super.apply(this, arguments); }, }); + +websiteNavbarData.websiteNavbarRegistry.add(WebsiteLinksMenu, '#o_website_links_share_page'); + }); diff --git a/addons/website_mail/models/mail_message.py b/addons/website_mail/models/mail_message.py index 34609160f1d..683990e1e67 100644 --- a/addons/website_mail/models/mail_message.py +++ b/addons/website_mail/models/mail_message.py @@ -26,6 +26,11 @@ class MailMessage(models.Model): description = fields.Char(compute="_compute_description", help='Message description: either the subject, or the beginning of the body') website_published = fields.Boolean(string='Published', help="Visible on the website as a comment", copy=False) + @api.model + def _non_employee_message_domain(self): + domain = super(MailMessage, self)._non_employee_message_domain() + return expression.AND([domain, [('website_published', '=', True)]]) + @api.multi def _compute_description(self): for message in self: diff --git a/addons/website_profile/controllers/main.py b/addons/website_profile/controllers/main.py index e46e72b2cfb..c3b9ba54941 100644 --- a/addons/website_profile/controllers/main.py +++ b/addons/website_profile/controllers/main.py @@ -191,13 +191,13 @@ class WebsiteProfile(http.Controller): # --------------------------------------------------- def _prepare_all_users_values(self, user): return { - 'position': user.karma_position, 'id': user.id, 'name': user.name, - 'company_name': user.company_id.name, + 'company_name': user.partner_id.company_name, 'rank': user.rank_id.name, 'karma': user.karma, 'badge_count': len(user.badge_ids), + 'website_published': user.website_published } @http.route(['/profile/users', @@ -212,20 +212,56 @@ class WebsiteProfile(http.Controller): dom = expression.AND([['|', ('name', 'ilike', search_term), ('company_id.name', 'ilike', search_term)], dom]) user_count = User.sudo().search_count(dom) - page_count = math.ceil(user_count / self._users_per_page) - pager = request.website.pager(url="/profile/users", total=user_count, page=page, step=self._users_per_page, - scope=page_count if page_count < self._pager_max_pages else self._pager_max_pages) - users = User.sudo().search(dom, limit=self._users_per_page, offset=pager['offset'], order='karma DESC') - user_values = [self._prepare_all_users_values(user) for user in users] + if user_count: + page_count = math.ceil(user_count / self._users_per_page) + pager = request.website.pager(url="/profile/users", total=user_count, page=page, step=self._users_per_page, + scope=page_count if page_count < self._pager_max_pages else self._pager_max_pages) + + users = User.sudo().search(dom, limit=self._users_per_page, offset=pager['offset'], order='karma DESC') + user_values = [self._prepare_all_users_values(user) for user in users] + + # Get karma position for users (only website_published) + position_domain = [('karma', '>', 1), ('website_published', '=', True)] + position_map = self._get_users_karma_position(position_domain, users.ids) + for user in user_values: + user['position'] = position_map.get(user['id'], 0) + + values = { + 'top3_users': user_values[:3] if not search_term and page == 1 else None, + 'users': user_values[3:] if not search_term and page == 1 else user_values, + 'pager': pager + } + else: + values = {'top3_users': [], 'users': [], 'pager': dict(page_count=0)} - values = { - 'top3_users': user_values[:3] if not search_term and page == 1 else None, - 'users': user_values[3:] if not search_term and page == 1 else user_values, - 'pager': pager - } return request.render("website_profile.users_page_main", values) + def _get_users_karma_position(self, domain, user_ids): + if not user_ids: + return {} + + Users = request.env['res.users'] + where_query = Users._where_calc(domain) + Users._apply_ir_rules(where_query, 'read') + from_clause, where_clause, where_clause_params = where_query.get_sql() + + # we search on every user in the DB to get the real positioning (not the one inside the subset) + # then, we filter to get only the subset. + query = """ + SELECT sub.id, sub.karma_position + FROM ( + SELECT "res_users"."id", row_number() OVER (ORDER BY res_users.karma DESC) AS karma_position + FROM {from_clause} + WHERE {where_clause} + ) sub + WHERE sub.id IN %s + """.format(from_clause=from_clause, where_clause=where_clause) + + request.env.cr.execute(query, where_clause_params + [tuple(user_ids)]) + + return {item['id']: item['karma_position'] for item in request.env.cr.dictfetchall()} + # User and validation # -------------------------------------------------- diff --git a/addons/website_profile/models/res_users.py b/addons/website_profile/models/res_users.py index 5bf49be0a78..fd440665f44 100644 --- a/addons/website_profile/models/res_users.py +++ b/addons/website_profile/models/res_users.py @@ -14,6 +14,15 @@ VALIDATION_KARMA_GAIN = 3 class Users(models.Model): _inherit = 'res.users' + def __init__(self, pool, cr): + init_res = super(Users, self).__init__(pool, cr) + type(self).SELF_WRITEABLE_FIELDS = list( + set( + self.SELF_WRITEABLE_FIELDS + + ['country_id', 'city', 'website', 'website_description', 'website_published'])) + type(self).SELF_READABLE_FIELDS = type(self).SELF_READABLE_FIELDS + ['karma'] + return init_res + @api.model def _generate_profile_token(self, user_id, email): """Return a token for email validation. This token is valid for the day diff --git a/addons/website_profile/views/website_profile.xml b/addons/website_profile/views/website_profile.xml index 1f0d22e4123..d03428d9287 100644 --- a/addons/website_profile/views/website_profile.xml +++ b/addons/website_profile/views/website_profile.xml @@ -207,7 +207,7 @@ <div class="col-12 col-md-4 col-lg-3"> <div t-attf-class="d-flex align-items-start h-100 #{'justify-content-between' if (request.env.user == user) else 'justify-content-around' }"> <div class="o_wprofile_pict d-inline-block mb-3 mb-md-0" t-attf-style="background-image:url(/web/image/res.users/#{user.id}/image)"/> - <a class="btn btn-primary d-inline-block d-md-none" t-if="request.env.user == user and validation_email_done" t-attf-href="/profile/edit?url_param=#{edit_button_url_param}"> + <a class="btn btn-primary d-inline-block d-md-none" t-if="request.env.user == user and user.karma != 0" t-attf-href="/profile/edit?url_param=#{edit_button_url_param}"> <i class="fa fa-pencil mr-1"/>EDIT </a> </div> @@ -219,14 +219,14 @@ <h1 class="o_card_people_name"> <span t-field="user.name"/><small t-if="user.karma == 0"> (not verified)</small> </h1> - <a class="btn btn-primary d-none d-md-inline-block" t-if="request.env.user == user and validation_email_done or request.env.user._is_admin()" t-attf-href="/profile/edit?url_param=#{edit_button_url_param}"> + <a class="btn btn-primary d-none d-md-inline-block" t-if="request.env.user == user and user.karma != 0 or request.env.user._is_admin()" t-attf-href="/profile/edit?url_param=#{edit_button_url_param}"> <i class="fa fa-pencil mr-2"/>EDIT PROFILE </a> </div> <div class="d-flex flex-column justify-content-center flex-grow-1 mb-0 mb-md-5"> - <div t-if="user.company_id" class="lead mb-2"> - <i class="fa fa-building-o fa-fw mr-1"/><span t-field="user.company_id"/> + <div t-if="user.partner_id.company_name" class="lead mb-2"> + <i class="fa fa-building-o fa-fw mr-1"/><span t-field="user.partner_id.company_name"/> </div> <div t-if="user.city or user.country_id" class="lead mb-2"> <i class="fa fa-map-marker fa-fw mr-1"/> @@ -478,11 +478,14 @@ <t t-call="website_profile.top3_user_card"></t> </div> </div> - <table class="table table-sm"> + <table class="table table-sm" t-if='users'> <tr t-foreach="users" t-as="user" t-attf-onclick="location.href='/profile/user/#{user['id']}';" class="o_wprofile_pointer bg-white"> <t t-call="website_profile.all_user_card"/> </tr> </table> + <t else=''> + <div class='alert alert-warning'>No results matched</div> + </t> <div class="form-inline justify-content-center"> <t t-call="website_profile.pager_nobox"/> </div> @@ -497,6 +500,7 @@ <img class="position-absolute" t-attf-src="/website_profile/static/src/img/rank_#{user['position']}.svg" alt="User rank" style="bottom: 0; right: -10px"/> </div> <h3 class="mt-2 mb-0" t-esc="user['name']"></h3> + <span class="badge badge-danger font-weight-normal px-2" t-if="not user['website_published']">Unpublished</span> <strong class="text-muted" t-esc="user['rank']"/> </div> <div class="row mx-0 o_wprofile_top3_card_footer text-nowrap"> @@ -517,6 +521,9 @@ <span class="font-weight-bold" t-esc="user['name']"/><br/> <span class="text-muted font-weight-bold" t-esc="user['rank']"></span> </td> + <td t-if="not user['website_published']" class="align-middle font-weight-bold text-right text-nowrap"> + <span class="badge badge-danger font-weight-normal px-2 py-1 m-1">Unpublished</span> + </td> <td class="align-middle font-weight-bold text-right text-nowrap"> <b t-esc="user['karma']"/> <span class="text-muted small font-weight-bold">XP</span> </td> diff --git a/addons/website_sale/models/sale_order.py b/addons/website_sale/models/sale_order.py index 8228027fced..95b3e3bfd74 100644 --- a/addons/website_sale/models/sale_order.py +++ b/addons/website_sale/models/sale_order.py @@ -310,13 +310,9 @@ class SaleOrder(models.Model): for order in self: order._portal_ensure_token() composer_form_view_id = self.env.ref('mail.email_compose_message_wizard_form').id - try: - default_template = self.env.ref('website_sale.mail_template_sale_cart_recovery', raise_if_not_found=False) - default_template_id = default_template.id if default_template else False - template_id = (self.filtered('website_id') == self and - self.mapped('website_id')[-1:1].cart_recovery_mail_template_id.id) or default_template_id - except: - template_id = False + + template_id = self._get_cart_recovery_template().id + return { 'type': 'ir.actions.act_window', 'view_type': 'form', @@ -336,6 +332,34 @@ class SaleOrder(models.Model): } @api.multi + def _get_cart_recovery_template(self): + """ + Return the cart recovery template record for a set of orders. + If they all belong to the same website, we return the website-specific template; + otherwise we return the default template. + If the default is not found, the empty ['mail.template'] is returned. + """ + websites = self.mapped('website_id') + template = websites.cart_recovery_mail_template_id if len(websites) == 1 else False + template = template or self.env.ref('website_sale.mail_template_sale_cart_recovery', raise_if_not_found=False) + return template or self.env['mail.template'] + + @api.multi + def _cart_recovery_email_send(self): + """Send the cart recovery email on the current recordset, + making sure that the portal token exists to avoid broken links, and marking the email as sent. + Similar method to action_recovery_email_send, made to be called in automated actions. + Contrary to the former, it will use the website-specific template for each order.""" + sent_orders = self.env['sale.order'] + for order in self: + template = order._get_cart_recovery_template() + if template: + order._portal_ensure_token() + template.send_mail(order.id) + sent_orders |= order + sent_orders.write({'cart_recovery_email_sent': True}) + + @api.multi def get_base_url(self): """When using multi-website, we want the user to be redirected to the most appropriate website if possible.""" diff --git a/addons/website_sale/static/src/js/website_sale.js b/addons/website_sale/static/src/js/website_sale.js index 78161683b78..f257454e0f8 100644 --- a/addons/website_sale/static/src/js/website_sale.js +++ b/addons/website_sale/static/src/js/website_sale.js @@ -140,7 +140,6 @@ publicWidget.registry.WebsiteSale = publicWidget.Widget.extend(VariantMixin, { 'change form.js_attributes input, form.js_attributes select': '_onChangeAttribute', 'mouseup form.js_add_cart_json label': '_onMouseupAddCartLabel', 'touchend form.js_add_cart_json label': '_onMouseupAddCartLabel', - 'change .css_attribute_color input': '_onChangeColorAttribute', 'click .show_coupon': '_onClickShowCoupon', 'submit .o_wsale_products_searchbar_form': '_onSubmitSaleSearch', 'change select[name="country_id"]': '_onChangeCountry', @@ -148,7 +147,7 @@ publicWidget.registry.WebsiteSale = publicWidget.Widget.extend(VariantMixin, { 'click .toggle_summary': '_onToggleSummary', 'click #add_to_cart, #buy_now, #products_grid .o_wsale_product_btn .a-submit': 'async _onClickAdd', 'click input.js_product_change': 'onChangeVariant', - // dirty fix: prevent options modal events to be triggered and bubbled + 'change .js_main_product [data-attribute_exclusions]': 'onChangeVariant', 'change oe_optional_products_modal [data-attribute_exclusions]': 'onChangeVariant', }), @@ -162,6 +161,9 @@ publicWidget.registry.WebsiteSale = publicWidget.Widget.extend(VariantMixin, { this._changeCountry = _.debounce(this._changeCountry.bind(this), 500); this.isWebsite = true; + + delete this.events['change .main_product:not(.in_cart) input.js_quantity']; + delete this.events['change [data-attribute_exclusions]']; }, /** * @override @@ -412,10 +414,14 @@ publicWidget.registry.WebsiteSale = publicWidget.Widget.extend(VariantMixin, { * @override * @private */ - _updateProductImage: function ($productContainer, displayImage, productId, productTemplateId, new_carousel) { + _updateProductImage: function ($productContainer, displayImage, productId, productTemplateId, new_carousel, isCombinationPossible) { var $img; var $carousel = $productContainer.find('#o-carousel-product'); + if (isCombinationPossible === undefined) { + isCombinationPossible = this.isSelectedVariantAllowed; + } + if (new_carousel) { // When using the web editor, don't reload this or the images won't // be able to be edited depending on if this is done loading before @@ -458,8 +464,7 @@ publicWidget.registry.WebsiteSale = publicWidget.Widget.extend(VariantMixin, { } } - $carousel.toggleClass('css_not_available', - $productContainer.find('.js_main_product').hasClass('css_not_available')); + $carousel.toggleClass('css_not_available', !isCombinationPossible); }, /** * @private @@ -646,13 +651,6 @@ publicWidget.registry.WebsiteSale = publicWidget.Widget.extend(VariantMixin, { * @private * @param {Event} ev */ - _onChangeColorAttribute: function (ev) { // highlight selected color - this._changeColorAttribute(); - }, - /** - * @private - * @param {Event} ev - */ _onClickShowCoupon: function (ev) { $(ev.currentTarget).hide(); $('.coupon_form').removeClass('d-none'); @@ -692,20 +690,22 @@ publicWidget.registry.WebsiteSale = publicWidget.Widget.extend(VariantMixin, { $('.ship_to_other').toggle(!$(ev.currentTarget).prop('checked')); }, /** - * Dirty fix: prevent options modal events to be triggered and bubbled - * Also write the properties of the form elements in the DOM to prevent the + * Toggles the add to cart button depending on the possibility of the + * current combination. + * + * @override + */ + _toggleDisable: function ($parent, isCombinationPossible) { + VariantMixin._toggleDisable.apply(this, arguments); + $parent.find("#add_to_cart").toggleClass('disabled', !isCombinationPossible); + }, + /** + * Write the properties of the form elements in the DOM to prevent the * current selection from being lost when activating the web editor. * * @override */ onChangeVariant: function (ev, data) { - var $originPath = ev.originalEvent && Array.isArray(ev.originalEvent.path) ? $(ev.originalEvent.path) : $(); - var $container = data && data.$container ? data.$container : $(); - if ($originPath.add($container).hasClass('oe_optional_products_modal')) { - ev.stopPropagation(); - return; - } - var $component = $(ev.currentTarget).closest('.js_product'); $component.find('input').each(function () { var $el = $(this); diff --git a/addons/website_sale/tests/test_website_sale_cart_recovery.py b/addons/website_sale/tests/test_website_sale_cart_recovery.py index 3dd3206506a..215bee56188 100644 --- a/addons/website_sale/tests/test_website_sale_cart_recovery.py +++ b/addons/website_sale/tests/test_website_sale_cart_recovery.py @@ -2,7 +2,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo.tests import tagged -from odoo.tests.common import HttpCase +from odoo.tests.common import HttpCase, TransactionCase @tagged('post_install', '-at_install') @@ -11,3 +11,110 @@ class TestWebsiteSaleCartRecovery(HttpCase): def test_01_shop_cart_recovery_tour(self): """The goal of this test is to make sure cart recovery works.""" self.start_tour("/", 'shop_cart_recovery', login="portal") + + +@tagged('post_install', '-at_install') +class TestWebsiteSaleCartRecoveryServer(TransactionCase): + + def setUp(self): + res = super(TestWebsiteSaleCartRecoveryServer, self).setUp() + + self.customer = self.env['res.partner'].create({ + 'name': 'a', + 'email': 'a@example.com', + }) + self.recovery_template_default = self.env.ref('website_sale.mail_template_sale_cart_recovery') + self.recovery_template_custom1 = self.recovery_template_default.copy() + self.recovery_template_custom2 = self.recovery_template_default.copy() + + self.website0 = self.env['website'].create({ + 'name': 'web0', + 'cart_recovery_mail_template_id': self.recovery_template_default.id, + }) + self.website1 = self.env['website'].create({ + 'name': 'web1', + 'cart_recovery_mail_template_id': self.recovery_template_custom1.id, + }) + self.website2 = self.env['website'].create({ + 'name': 'web2', + 'cart_recovery_mail_template_id': self.recovery_template_custom2.id, + }) + self.so0 = self.env['sale.order'].create({ + 'partner_id': self.customer.id, + 'website_id': self.website0.id, + 'is_abandoned_cart': True, + 'cart_recovery_email_sent': False, + }) + self.so1 = self.env['sale.order'].create({ + 'partner_id': self.customer.id, + 'website_id': self.website1.id, + 'is_abandoned_cart': True, + 'cart_recovery_email_sent': False, + }) + self.so2 = self.env['sale.order'].create({ + 'partner_id': self.customer.id, + 'website_id': self.website2.id, + 'is_abandoned_cart': True, + 'cart_recovery_email_sent': False, + }) + + return res + + def test_cart_recovery_mail_template(self): + """Make sure that we get the correct cart recovery templates to send.""" + self.assertEqual( + self.so1._get_cart_recovery_template(), + self.recovery_template_custom1, + "We do not return the correct mail template" + ) + self.assertEqual( + self.so2._get_cart_recovery_template(), + self.recovery_template_custom2, + "We do not return the correct mail template" + ) + # Orders that belong to different websites; we should get the default template + self.assertEqual( + (self.so1 + self.so2)._get_cart_recovery_template(), + self.recovery_template_default, + "We do not return the correct mail template" + ) + + def test_cart_recovery_mail_template_send(self): + """The goal of this test is to make sure cart recovery works.""" + orders = self.so0 + self.so1 + self.so2 + + self.assertFalse( + any(orders.mapped('cart_recovery_email_sent')), + "The recovery mail should not have been sent yet." + ) + self.assertFalse( + any(orders.mapped('access_token')), + "There should not be an access token yet." + ) + + orders._cart_recovery_email_send() + + self.assertTrue( + all(orders.mapped('cart_recovery_email_sent')), + "The recovery mail should have been sent." + ) + self.assertTrue( + all(orders.mapped('access_token')), + "All tokens should have been generated." + ) + + sent_mail = {} + for order in orders: + mail = self.env["mail.mail"].search([ + ('record_name', '=', order['name']) + ]) + sent_mail.update({order: mail}) + + self.assertTrue( + all(len(sent_mail[order]) == 1 for order in orders), + "Each cart recovery mail has been sent exactly once." + ) + self.assertTrue( + all(order.access_token in sent_mail[order].body for order in orders), + "Each mail should contain the access token of the corresponding SO." + ) diff --git a/addons/website_sale_coupon/tests/test_shop_sale_coupon.py b/addons/website_sale_coupon/tests/test_shop_sale_coupon.py index e34b6904638..76df76157a1 100644 --- a/addons/website_sale_coupon/tests/test_shop_sale_coupon.py +++ b/addons/website_sale_coupon/tests/test_shop_sale_coupon.py @@ -1,14 +1,23 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo.tests import HttpCase, tagged +from odoo.addons.sale.tests.test_sale_product_attribute_value_config import TestSaleProductAttributeValueSetup @tagged('post_install', '-at_install') -class TestUi(HttpCase): +class TestUi(TestSaleProductAttributeValueSetup, HttpCase): post_install = True at_install = False + def setUp(self): + super(TestUi, self).setUp() + + # set currency to not rely on demo data and avoid possible race condition + self.currency_ratio = 1.0 + pricelist = self.env.ref('product.list0') + pricelist.currency_id = self._setup_currency(self.currency_ratio) + def test_01_admin_shop_sale_coupon_tour(self): # pre enable "Show # found" option to avoid race condition... self.env.ref("website_sale.search_count_box").write({"active": True}) diff --git a/addons/website_sale_delivery/controllers/main.py b/addons/website_sale_delivery/controllers/main.py index 73c89e4943f..c623ebab63c 100644 --- a/addons/website_sale_delivery/controllers/main.py +++ b/addons/website_sale_delivery/controllers/main.py @@ -87,14 +87,13 @@ class WebsiteSaleDelivery(WebsiteSale): Monetary = request.env['ir.qweb.field.monetary'] carrier_id = int(post['carrier_id']) currency = order.currency_id - delivery_price = order.order_line.filtered(lambda line: line.is_delivery).price_unit if order: return { 'status': order.delivery_rating_success, 'error_message': order.delivery_message, 'carrier_id': carrier_id, - 'is_free_delivery': not bool(delivery_price), - 'new_amount_delivery': Monetary.value_to_html(delivery_price, {'display_currency': currency}), + 'is_free_delivery': not bool(order.amount_delivery), + 'new_amount_delivery': Monetary.value_to_html(order.amount_delivery, {'display_currency': currency}), 'new_amount_untaxed': Monetary.value_to_html(order.amount_untaxed, {'display_currency': currency}), 'new_amount_tax': Monetary.value_to_html(order.amount_tax, {'display_currency': currency}), 'new_amount_total': Monetary.value_to_html(order.amount_total, {'display_currency': currency}), diff --git a/addons/website_sale_product_configurator/static/src/js/product_configurator_modal.js b/addons/website_sale_product_configurator/static/src/js/product_configurator_modal.js index a6ebf95d32c..940d7e04a63 100644 --- a/addons/website_sale_product_configurator/static/src/js/product_configurator_modal.js +++ b/addons/website_sale_product_configurator/static/src/js/product_configurator_modal.js @@ -24,10 +24,6 @@ OptionalProductsModal.include({ this._super.apply(this, arguments); this.isWebsite = params.isWebsite; - if (this.isWebsite) { - delete this.events['click button.js_add_cart_json']; - } - this.dialogClass = 'oe_optional_products_modal' + (params.isWebsite ? ' oe_website_sale' : ''); }, /** diff --git a/addons/website_sale_product_configurator/static/src/js/website_sale_options.js b/addons/website_sale_product_configurator/static/src/js/website_sale_options.js index 26e8efaabd3..18558954c0c 100644 --- a/addons/website_sale_product_configurator/static/src/js/website_sale_options.js +++ b/addons/website_sale_product_configurator/static/src/js/website_sale_options.js @@ -44,7 +44,7 @@ publicWidget.registry.WebsiteSale.include({ */ _onOptionsUpdateQuantity: function (quantity) { this.$form - .find('input[name="add_qty"]') + .find('.js_main_product input[name="add_qty"]') .first() .val(quantity) .trigger('change'); diff --git a/addons/website_sale_wishlist/static/src/js/website_sale_wishlist.js b/addons/website_sale_wishlist/static/src/js/website_sale_wishlist.js index 49ab188b294..5a899feb7ed 100644 --- a/addons/website_sale_wishlist/static/src/js/website_sale_wishlist.js +++ b/addons/website_sale_wishlist/static/src/js/website_sale_wishlist.js @@ -162,9 +162,9 @@ publicWidget.registry.ProductWishlist = publicWidget.Widget.extend(VariantMixin, wSaleUtils.animateClone($('#my_cart'), tr, 25, 40); if ($('#b2b_wish').is(':checked')) { - return this._addToCart(product, tr.find('qty').val() || 1); + return this._addToCart(product, tr.find('add_qty').val() || 1); } else { - var adding_deffered = this._addToCart(product, tr.find('qty').val() || 1); + var adding_deffered = this._addToCart(product, tr.find('add_qty').val() || 1); this._removeWish(e, adding_deffered); return adding_deffered; } diff --git a/addons/website_slides/controllers/main.py b/addons/website_slides/controllers/main.py index 5bff0ef6adb..c7e36539ce7 100644 --- a/addons/website_slides/controllers/main.py +++ b/addons/website_slides/controllers/main.py @@ -294,7 +294,7 @@ class WebsiteSlides(WebsiteProfile): domain = request.website.website_domain() domain = self._build_channel_domain(domain, slide_type=slide_type, my=my, **post) - order = self._channel_order_by_criterion.get(post.get('sorting', 'date'), 'create_date desc') + order = self._channel_order_by_criterion.get(post.get('sorting')) channels = request.env['slide.channel'].search(domain, order=order) # channels_layouted = list(itertools.zip_longest(*[iter(channels)] * 4, fillvalue=None)) @@ -460,7 +460,7 @@ class WebsiteSlides(WebsiteProfile): @http.route(['/slides/channel/join'], type='json', auth='public', website=True) def slide_channel_join(self, channel_id): if request.website.is_public_user(): - return {'error': 'public_user'} + return {'error': 'public_user', 'error_signup_allowed': request.env['res.users'].sudo()._get_signup_invitation_scope() == 'b2c'} success = request.env['slide.channel'].browse(channel_id).action_add_member() if not success: return {'error': 'join_done'} @@ -587,7 +587,7 @@ class WebsiteSlides(WebsiteProfile): @http.route('/slides/slide/like', type='json', auth="public", website=True) def slide_like(self, slide_id, upvote): if request.website.is_public_user(): - return {'error': 'public_user'} + return {'error': 'public_user', 'error_signup_allowed': request.env['res.users'].sudo()._get_signup_invitation_scope() == 'b2c'} slide_partners = request.env['slide.slide.partner'].sudo().search([ ('slide_id', '=', slide_id), ('partner_id', '=', request.env.user.partner_id.id) diff --git a/addons/website_slides/static/src/js/slides_course_join.js b/addons/website_slides/static/src/js/slides_course_join.js index 985d8f446c3..002ea049e5f 100644 --- a/addons/website_slides/static/src/js/slides_course_join.js +++ b/addons/website_slides/static/src/js/slides_course_join.js @@ -53,7 +53,12 @@ var CourseJoinWidget = publicWidget.Widget.extend({ location.reload(); } else { if (data.error === 'public_user') { - self._popoverAlert(self.$el, _.str.sprintf(_t('Please <a href="/web/login?redirect=%s">login</a> to join this course.'), (document.URL))); + var message = _t('Please <a href="/web/login?redirect=%s">login</a> to join this course'); + var signupAllowed = data.error_signup_allowed || false; + if (signupAllowed) { + message = _t('Please <a href="/web/signup?redirect=%s">create an account</a> to join this course'); + } + self._popoverAlert(self.$el, _.str.sprintf(message, (document.URL))); } else if (data.error === 'join_done') { self._popoverAlert(self.$el, _t('You have already joined this channel')); } else { diff --git a/addons/website_slides/static/src/js/slides_slide_like.js b/addons/website_slides/static/src/js/slides_slide_like.js index e85ca928a7f..88bc3db1b77 100644 --- a/addons/website_slides/static/src/js/slides_slide_like.js +++ b/addons/website_slides/static/src/js/slides_slide_like.js @@ -55,7 +55,12 @@ var SlideLikeWidget = publicWidget.Widget.extend({ self.$el.find('span.o_wslides_js_slide_like_down span').text(data.dislikes); } else { if (data.error === 'public_user') { - self._popoverAlert(self.$el, _.str.sprintf(_t('Please <a href="/web/login?redirect=%s">login</a> to vote this lesson'), (document.URL))); + var message = _t('Please <a href="/web/login?redirect=%s">login</a> to vote this lesson'); + var signupAllowed = data.error_signup_allowed || false; + if (signupAllowed) { + message = _t('Please <a href="/web/signup?redirect=%s">create an account</a> to vote this lesson'); + } + self._popoverAlert(self.$el, _.str.sprintf(message, (document.URL))); } else if (data.error === 'vote_done') { self._popoverAlert(self.$el, _t('You have already voted for this lesson')); } else if (data.error === 'slide_access') { diff --git a/addons/website_slides/views/website_slides_templates_course.xml b/addons/website_slides/views/website_slides_templates_course.xml index d612215cc0a..e5412a34bd3 100644 --- a/addons/website_slides/views/website_slides_templates_course.xml +++ b/addons/website_slides/views/website_slides_templates_course.xml @@ -167,7 +167,7 @@ <i class="fa fa-home"/> Course </a> </li> - <li class="nav-item"> + <li t-if="channel.allow_comment" class="nav-item"> <a class="nav-link" id="review-tab" data-toggle="pill" href="#review" role="tab" aria-controls="review" aria-selected="false">Review</a> </li> </ul> @@ -176,7 +176,7 @@ <div class="tab-pane fade show active" id="home" role="tabpanel" aria-labelledby="home-tab"> <t t-if="channel.channel_type == 'training'" t-call="website_slides.course_slides_list"/> </div> - <div class="tab-pane fade" id="review" role="tabpanel" aria-labelledby="review-tab"> + <div t-if="channel.allow_comment" class="tab-pane fade" id="review" role="tabpanel" aria-labelledby="review-tab"> <t t-call="portal.message_thread"> <t t-set="object" t-value="channel"/> <t t-set="hash" t-value="message_post_hash"/> @@ -335,9 +335,8 @@ <div t-if="channel.can_publish and category_id" class="o_wslides_slides_list_drag py-2 pr-3"> <i class="fa fa-bars"/> </div> - <span t-if="category_id" t-att-class="'text-muted' if not channel.can_upload else ''" t-esc="category['name']"/> - <em t-elif="channel.can_publish" class="text-muted mb-1">Uncategorized</em> - <small t-if="not category['total_slides']" class="ml-1 text-muted"><b>(empty)</b></small> + <span t-if="category_id" t-att-class="'text-muted' if not channel.can_upload else ''" t-field="category['category'].name"/> + <small t-if="not category['total_slides'] and category_id" class="ml-1 text-muted"><b>(empty)</b></small> </div> <div t-if="category_id" class="o_text_link d-flex border-left"> <a t-if="channel.can_upload" diff --git a/addons/website_slides/views/website_slides_templates_homepage.xml b/addons/website_slides/views/website_slides_templates_homepage.xml index 9df61fe9c44..59db5c942b0 100644 --- a/addons/website_slides/views/website_slides_templates_homepage.xml +++ b/addons/website_slides/views/website_slides_templates_homepage.xml @@ -321,7 +321,6 @@ <div class="o_wslides_home_aside"> <div class="row o_wslides_home_aside_title"> <div class="col"> - <a href="/slides" class="float-right">View all</a> <h5 class="m-0">Latest achievements</h5> <hr class="mt-2"/> </div> @@ -345,7 +344,7 @@ <t t-set="user" t-value="achievement.user_id"/> </t> <div style="line-height: 1.3"> - <span class="font-weight-bold" t-esc="achievement.user_id.name"/> achieved <a href="/slides"><span t-esc="achievement.badge_id.name"/></a> + <span class="font-weight-bold" t-esc="achievement.user_id.name"/> achieved <span class="font-weight-bold" t-esc="achievement.badge_id.name"/> </div> </div> </template> diff --git a/doc/_extensions/autojsdoc/parser/parser.py b/doc/_extensions/autojsdoc/parser/parser.py index a5eb45a8e85..62660514598 100644 --- a/doc/_extensions/autojsdoc/parser/parser.py +++ b/doc/_extensions/autojsdoc/parser/parser.py @@ -36,7 +36,7 @@ class ModuleMatcher(Visitor): 'property': {'name': 'define'}, }, }}): - [module, func] = node['expression']['arguments'] + [module, *_, func] = node['expression']['arguments'] mod = jsdoc.parse_comments(node.get('comments'), jsdoc.ModuleDoc) # set module name mod.set_name(module['value']) diff --git a/doc/cla/corporate/acsone.md b/doc/cla/corporate/acsone.md index d4ee1947927..25b0e1b1c8a 100644 --- a/doc/cla/corporate/acsone.md +++ b/doc/cla/corporate/acsone.md @@ -27,3 +27,4 @@ Thomas Binsfeld thomas.binsfeld@acsone.eu https://github.com/ThomasBinsfeld Benjamin Willig benjamin.willig@acsone.eu https://github.com/benwillig François Honoré francois.honore@acsone.eu https://github.com/acsonefho Denis Robinet denis.robinet@acsone.eu https://github.com/RobinetDenisAcsone +Benoit Aimont benoit.aimont@acsone.eu https://github.com/baimont diff --git a/doc/cla/corporate/ecoservice.md b/doc/cla/corporate/ecoservice.md new file mode 100644 index 00000000000..b1b1cbe8ff0 --- /dev/null +++ b/doc/cla/corporate/ecoservice.md @@ -0,0 +1,21 @@ +Germany, 2019-05-14 + +ecoservice GbR agrees to the terms of the Odoo Corporate Contributor License +Agreement v1.0. + +I declare that I am authorized and able to make this agreement and sign this +declaration. + +Signed, + +Falk Neubert neubert@ecoservice.de https://github.com/FNeu75 + +List of contributors: + +* Christian Engelhardt c.engelhardt@ecoservice.de https://github.com/c-engelhardt-ecoservice +* Christian Schöttke c.schoettke@ecoservice.de https://github.com/cschoettke +* Falk Neubert neubert@ecoservice.de https://github.com/FNeu75 +* Gülhan Celik g.celik@ecoservice.de https://github.com/guelhancelik +* Jan Brodersen 4rmitxes@gmail.com https://github.com/Armitxes +* Kamal Prajapati k.prajapati@ecoservice.de https://github.com/kamalprajapati +* Marcel Much m.much@ecoservice.de https://github.com/mmuch diff --git a/doc/cla/corporate/savoirfairelinux.md b/doc/cla/corporate/savoirfairelinux.md index 913e317cb07..04f4563d4b7 100644 --- a/doc/cla/corporate/savoirfairelinux.md +++ b/doc/cla/corporate/savoirfairelinux.md @@ -12,17 +12,24 @@ João Alfredo Gama Batista joao.gama@savoirfairelinux.com https://github.com/joa List of contributors: -Agathe Mollé agathe.molle@savoirfairelinux.com https://github.com/Ehtaga -Bruno Joliveau bruno.joliveau@savoirfairelinux.com https://github.com/bjoliveau -David Dufresne david.dufresne@savoirfairelinux.com https://github.com/dufresnedavid -Istvan SZALAÏ istvan.szalai@savoirfairelinux.com https://github.com/ventilooo -João Alfredo Gama Batista joao.gama@savoirfairelinux.com https://github.com/joaoalf -Julien Jezequel-Breard julien.jezequel-breard@savoirfairelinux.com https://github.com/jjbreard -Quentin Lavallée-Bourdeau quentin.lavallee@savoirfairelinux.com https://github.com/qtiplb + Rim Ben Dhaou rim.bendhaou@savoirfairelinux.com https://github.com/rimbendhaou -Yasmine El Mrini yasmine.elmrini@savoirfairelinux.com https://github.com/yasmineelmrini Luis Garcia Ruiz luis.garcia@savoirfairelinux.com https://github.com/eilst Dimitar Apostolov dimitar.apostolov@savoirfairelinux.com https://github.com/dapostolov-sfl +Pierre Gault pierre.gault@savoirfairelinux.com https://github.com/gaultp +Maroua Romdhane maroua.romdhane@savoirfairelinux.com https://github.com/mromdhane +Jean-François Bolduc jean-francois.bolduc@savoirfairelinux.com https://github.com/fjdrake +Blaise Frison blaise.frison@savoirfairelinux.com https://github.com/bfrison-sfl +Larbi Gharib larbi.gharib@savoirfairelinux.com https://github.com/lgharib +Jananjoy Rajkumar jananjoy.rajkumar@savoirfairelinux.com https://github.com/jananjoy +Yasmine El Mrini yasmine.elmrini@savoirfairelinux.com https://github.com/yasmineelmrini (up to 2019-05-13) +Agathe Mollé agathe.molle@savoirfairelinux.com https://github.com/Ehtaga (up to 2019-05-13) +Bruno Joliveau bruno.joliveau@savoirfairelinux.com https://github.com/bjoliveau (up to 2019-05-13) +David Dufresne david.dufresne@savoirfairelinux.com https://github.com/dufresnedavid (up to 2019-05-13) +Istvan SZALAÏ istvan.szalai@savoirfairelinux.com https://github.com/ventilooo (up to 2019-05-13) +João Alfredo Gama Batista joao.gama@savoirfairelinux.com https://github.com/joaoalf (up to 2019-05-13) +Julien Jezequel-Breard julien.jezequel-breard@savoirfairelinux.com https://github.com/jjbreard (up to 2019-05-13) +Quentin Lavallée-Bourdeau quentin.lavallee@savoirfairelinux.com https://github.com/qtiplb (up to 2019-05-13) Jérome Boisvert-Chouinard jerome.boisvertchouinard@savoirfairelinux.com https://github.com/jbchouinard (up to 2017-10-17) Julie Moussu julie.mousse@savoirfairelinux.com https://github.com/JulieSFL (up to 2017-10-17) Pierre Lamarche pierre.lamarche@savoirfairelinux.com https://github.com/plamarche (up to 2017-10-17) @@ -34,6 +41,5 @@ Jordi Riera jordi.riera@savoirfairelinux.com https://github.com/foutoucour (up t Julien Roux julien.roux@savoirfairelinux.com https://github.com/jrouxsfl (up to 2017-05-12) Loïc Faure-Lacroix loic.lacroix@savoirfairelinux.com https://github.com/llacroix (up to 2017-05-12) Maxime Chambreuil maxime.chambreuil@savoirfairelinux.com https://github.com/max3903 (up to 2017-05-12) -Pierre Gault pierre.gault@savoirfairelinux.com https://github.com/gaultp (up to 2017-05-12) Sandy Carter sandy.carter@savoirfairelinux.com https://github.com/bwrsandman (up to 2017-05-12) Vincent Vinet vincent.vinet@savoirfairelinux.com https://github.com/veloutin (up to 2017-05-12) diff --git a/doc/cla/corporate/vauxoo.md b/doc/cla/corporate/vauxoo.md index 235931654a6..2cce9509360 100644 --- a/doc/cla/corporate/vauxoo.md +++ b/doc/cla/corporate/vauxoo.md @@ -44,4 +44,5 @@ Alexander Olivares alexander@vauxoo.com https://github.com/alxolivares Jose Manuel Robles josemanuel@vauxoo.com https://github.com/keylor2906 Erick Birbe erick@vauxoo.com https://github.com/ebirbe Tomas Alvarez tomas@vauxoo.com https://github.com/tomeyro +Carmen Liliana Miranda González carmen@vauxoo.com https://github.com/CarmenMiranda Arturo Flores arturo@vauxoo.com https://github.com/umiphos diff --git a/doc/cla/individual/0yik.md b/doc/cla/individual/0yik.md new file mode 100644 index 00000000000..a70e2510a71 --- /dev/null +++ b/doc/cla/individual/0yik.md @@ -0,0 +1,11 @@ +Indonesia, 2019-04-24 + +I hereby agree to the terms of the Odoo Individual Contributor License +Agreement v1.0. + +I declare that I am authorized and able to make this agreement and sign this +declaration. + +Signed, + +Anang Aji Rahmawan aji.abuismail@gmail.com https://github.com/0yik diff --git a/doc/cla/individual/AfroMonkey.md b/doc/cla/individual/AfroMonkey.md new file mode 100644 index 00000000000..6a5e4a0d561 --- /dev/null +++ b/doc/cla/individual/AfroMonkey.md @@ -0,0 +1,11 @@ +México, 2019-04-03 + +I hereby agree to the terms of the Odoo Individual Contributor License +Agreement v1.0. + +I declare that I am authorized and able to make this agreement and sign this +declaration. + +Signed, + +Navarro Presas Moisés Alejandro moisalejandro@gmail.com https://github.com/AfroMonkey diff --git a/doc/cla/individual/gjanssens.md b/doc/cla/individual/gjanssens.md new file mode 100644 index 00000000000..d3cda29d978 --- /dev/null +++ b/doc/cla/individual/gjanssens.md @@ -0,0 +1,11 @@ +Belgium, 2019-05-09 + +I hereby agree to the terms of the Odoo Individual Contributor License +Agreement v1.0. + +I declare that I am authorized and able to make this agreement and sign this +declaration. + +Signed, + +Geert Janssens geert@kobaltwit.be https://github.com/gjanssens diff --git a/doc/howtos/backend/exercise-basic-action b/doc/howtos/backend/exercise-basic-action index 8fa44f743c6..277e88bed1e 100644 --- a/doc/howtos/backend/exercise-basic-action +++ b/doc/howtos/backend/exercise-basic-action @@ -31,7 +31,7 @@ Index: addons/openacademy/views/openacademy.xml + <field name="view_type">form</field> + <field name="view_mode">tree,form</field> + <field name="help" type="html"> -+ <p class="oe_view_nocontent_smiling_face">Create the first course ++ <p class="o_view_nocontent_smiling_face">Create the first course + </p> + </field> + </record> diff --git a/doc/howtos/backend/exercise-searchview b/doc/howtos/backend/exercise-searchview index ed6fc681a99..e11c96627e7 100644 --- a/doc/howtos/backend/exercise-searchview +++ b/doc/howtos/backend/exercise-searchview @@ -24,5 +24,5 @@ Index: addons/openacademy/views/openacademy.xml <field name="view_mode">tree,form</field> + <field name="context" eval="{'search_default_my_courses': 1}"/> <field name="help" type="html"> - <p class="oe_view_nocontent_smiling_face">Create the first course + <p class="o_view_nocontent_smiling_face">Create the first course </p> diff --git a/odoo/addons/base/models/ir_mail_server.py b/odoo/addons/base/models/ir_mail_server.py index 6773e1f61cc..2422b9cf4a9 100644 --- a/odoo/addons/base/models/ir_mail_server.py +++ b/odoo/addons/base/models/ir_mail_server.py @@ -119,10 +119,18 @@ def encode_rfc2822_address_header(header_text): # Header as a string, using an unlimited line length.", the old one # was "A synonym for Header.encode()." so call encode() directly? name = Header(pycompat.to_text(name)).encode() - return formataddr((name, email)) + # if the from does not follow the (name <addr>),* convention, we might + # try to encode meaningless strings as address, as getaddresses is naive + # note it would also fail on real addresses with non-ascii characters + try: + return formataddr((name, email)) + except UnicodeEncodeError: + _logger.warning(_('Failed to encode the address %s\n' + 'from mail header:\n%s') % addr, header_text) + return "" addresses = getaddresses([pycompat.to_text(ustr(header_text))]) - return COMMASPACE.join(encode_addr(a) for a in addresses) + return COMMASPACE.join(a for a in (encode_addr(addr) for addr in addresses) if a) class IrMailServer(models.Model): diff --git a/odoo/addons/base/models/ir_translation.py b/odoo/addons/base/models/ir_translation.py index 9c3b5a18fcc..000cb6a3a44 100644 --- a/odoo/addons/base/models/ir_translation.py +++ b/odoo/addons/base/models/ir_translation.py @@ -650,8 +650,8 @@ class IrTranslation(models.Model): rows_by_type = defaultdict(list) for vals in vals_list: rows_by_type[vals['type']].append(( - vals['name'], vals['lang'], vals['res_id'], vals['src'], vals['type'], - vals.get('module'), vals['value'], vals.get('state'), vals.get('comments'), + vals['name'], vals['lang'], vals['res_id'], vals['src'] or '', vals['type'], + vals.get('module'), vals['value'] or '', vals.get('state'), vals.get('comments'), )) if rows_by_type['model']: diff --git a/odoo/addons/base/models/res_company.py b/odoo/addons/base/models/res_company.py index d9f35d76276..2b69228c655 100644 --- a/odoo/addons/base/models/res_company.py +++ b/odoo/addons/base/models/res_company.py @@ -263,19 +263,15 @@ class Company(models.Model): return self.env['res.config.settings'].open_company() @api.multi - def write_company_and_print_report(self, values): - res = self.write(values) - - report_name = values.get('default_report_name') - active_ids = values.get('active_ids') - active_model = values.get('active_model') + def write_company_and_print_report(self): + context = self.env.context + report_name = context.get('default_report_name') + active_ids = context.get('active_ids') + active_model = context.get('active_model') if report_name and active_ids and active_model: docids = self.env[active_model].browse(active_ids) return (self.env['ir.actions.report'].search([('report_name', '=', report_name)], limit=1) - .with_context(values) .report_action(docids)) - else: - return res @api.model def action_open_base_onboarding_company(self): diff --git a/odoo/addons/base/models/res_config.py b/odoo/addons/base/models/res_config.py index 9d94f636164..4572d2a2ba1 100644 --- a/odoo/addons/base/models/res_config.py +++ b/odoo/addons/base/models/res_config.py @@ -543,7 +543,7 @@ class ResConfigSettings(models.TransientModel, ResConfigModuleInstallationMixin) _logger.warning(WARNING_MESSAGE, value, field, icp) value = 0.0 elif field.type == 'boolean': - value = value.lower() == 'true' + value = bool(value) res[name] = value # other fields: call the method 'get_values' diff --git a/odoo/addons/base/models/res_partner.py b/odoo/addons/base/models/res_partner.py index e9d607f42a8..9f3fcd91c17 100644 --- a/odoo/addons/base/models/res_partner.py +++ b/odoo/addons/base/models/res_partner.py @@ -248,7 +248,7 @@ class Partner(models.Model): @api.depends('is_company', 'name', 'parent_id.name', 'type', 'company_name') def _compute_display_name(self): - diff = dict(show_address=None, show_address_only=None, show_email=None) + diff = dict(show_address=None, show_address_only=None, show_email=None, html_format=None, show_vat=False) names = dict(self.with_context(**diff).name_get()) for partner in self: partner.display_name = names.get(partner.id) diff --git a/odoo/addons/base/models/res_users.py b/odoo/addons/base/models/res_users.py index a999b14bf5e..6037c59d7d6 100644 --- a/odoo/addons/base/models/res_users.py +++ b/odoo/addons/base/models/res_users.py @@ -1044,10 +1044,9 @@ class GroupsView(models.Model): """ Modify the view with xmlid ``base.user_groups_view``, which inherits the user form view, and introduces the reified group fields. """ - if self._context.get('install_mode'): - # use installation/admin language for translatable names in the view - user_context = self.env['res.users'].context_get() - self = self.with_context(**user_context) + + # remove the language to avoid translations, it will be handled at the view level + self = self.with_context(lang=None) # We have to try-catch this, because at first init the view does not # exist but we are already creating some basic groups. @@ -1057,7 +1056,7 @@ class GroupsView(models.Model): group_employee = view.env.ref('base.group_user') xml1, xml2, xml3 = [], [], [] xml_by_category = {} - xml1.append(E.separator(string=_('User Type'), colspan="2", groups='base.group_no_one')) + xml1.append(E.separator(string='User Type', colspan="2", groups='base.group_no_one')) user_type_field_name = '' for app, kind, gs, category_name in self.get_groups_by_application(): @@ -1088,7 +1087,7 @@ class GroupsView(models.Model): else: # application separator with boolean fields - app_name = app.name or _('Other') + app_name = app.name or 'Other' xml3.append(E.separator(string=app_name, colspan="4", **attrs)) for g in gs: field_name = name_boolean_group(g.id) @@ -1178,9 +1177,9 @@ class UsersView(models.Model): group_multi_company = self.env.ref('base.group_multi_company', False) if group_multi_company and 'company_ids' in values: if len(user.company_ids) <= 1 and user.id in group_multi_company.users.ids: - group_multi_company.write({'users': [(3, user.id)]}) + user.write({'groups_id': [(3, group_multi_company.id)]}) elif len(user.company_ids) > 1 and user.id not in group_multi_company.users.ids: - group_multi_company.write({'users': [(4, user.id)]}) + user.write({'groups_id': [(4, group_multi_company.id)]}) return user @api.multi @@ -1191,9 +1190,9 @@ class UsersView(models.Model): if group_multi_company and 'company_ids' in values: for user in self: if len(user.company_ids) <= 1 and user.id in group_multi_company.users.ids: - group_multi_company.write({'users': [(3, user.id)]}) + user.write({'groups_id': [(3, group_multi_company.id)]}) elif len(user.company_ids) > 1 and user.id not in group_multi_company.users.ids: - group_multi_company.write({'users': [(4, user.id)]}) + user.write({'groups_id': [(4, group_multi_company.id)]}) return res def _remove_reified_groups(self, values): diff --git a/odoo/addons/base/static/src/js/res_config_settings.js b/odoo/addons/base/static/src/js/res_config_settings.js index 404ad4d3d68..9ad0a2c9196 100644 --- a/odoo/addons/base/static/src/js/res_config_settings.js +++ b/odoo/addons/base/static/src/js/res_config_settings.js @@ -309,7 +309,7 @@ var BaseSettingRenderer = FormRenderer.extend({ module.settingView.find('h2').addClass('o_hidden'); module.settingView.find('.settingSearchHeader').addClass('o_hidden'); module.settingView.find('.o_settings_container').removeClass('mt16'); - var resultSetting = module.settingView.find("label:containsTextLike('" + self.searchText + "')"); + var resultSetting = module.settingView.find(".o_form_label:containsTextLike('" + self.searchText + "')"); if (resultSetting.length > 0) { resultSetting.each(function () { var settingBox = $(this).closest('.o_setting_box'); @@ -347,8 +347,8 @@ var BaseSettingRenderer = FormRenderer.extend({ } var match = text.search(new RegExp(word, "i")); word = text.substring(match, match + word.length); - var hilitedWord = "<span class='highlighter'>" + word + '</span>'; - return text.replace(word, hilitedWord); + var highlightedWord = "<span class='highlighter'>" + word + '</span>'; + return text.replace(word, highlightedWord); }, }); diff --git a/odoo/addons/base/static/tests/base_settings_tests.js b/odoo/addons/base/static/tests/base_settings_tests.js index 319e7bf93b5..98d68b1281e 100644 --- a/odoo/addons/base/static/tests/base_settings_tests.js +++ b/odoo/addons/base/static/tests/base_settings_tests.js @@ -25,7 +25,7 @@ QUnit.module('base_settings_tests', { QUnit.module('BaseSetting'); QUnit.test('change setting on nav bar click in base settings', async function (assert) { - assert.expect(4); + assert.expect(5); var form = await createView({ View: BaseSettingsView, @@ -63,7 +63,7 @@ QUnit.module('base_settings_tests', { '<field name="foo"/>'+ '</div>'+ '<div class="o_setting_right_pane">'+ - '<label for="foo"/>'+ + '<span class="o_form_label">Foo</span>'+ '<div class="text-muted">'+ 'this is foo'+ '</div>'+ @@ -79,9 +79,11 @@ QUnit.module('base_settings_tests', { assert.hasAttrValue(form.$('.selected'), 'data-key',"crm","crm setting selected"); assert.isVisible(form.$(".settings .app_settings_block"), "project settings show"); await testUtils.fields.editAndTrigger(form.$('.searchInput'), 'b', 'keyup'); - assert.strictEqual($('.highlighter').html(),"B","b word hilited"); + assert.strictEqual(form.$('.highlighter').html(), "B", "b word highlighted"); await testUtils.fields.editAndTrigger(form.$('.searchInput'), 'bx', 'keyup'); assert.isVisible(form.$('.notFound'), "record not found message shown"); + form.$('.searchInput').val('f').trigger('keyup'); + assert.strictEqual(form.$('span.o_form_label .highlighter').html(), "F", "F word highlighted"); form.destroy(); }); diff --git a/odoo/addons/test_convert/__init__.py b/odoo/addons/test_convert/__init__.py index dc5e6b693d1..a92fd9a2904 100644 --- a/odoo/addons/test_convert/__init__.py +++ b/odoo/addons/test_convert/__init__.py @@ -1,4 +1,4 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from . import models +from . import models # noqa diff --git a/odoo/addons/test_convert/ir.model.access.csv b/odoo/addons/test_convert/ir.model.access.csv index 0282a269c41..0ba260e485e 100644 --- a/odoo/addons/test_convert/ir.model.access.csv +++ b/odoo/addons/test_convert/ir.model.access.csv @@ -1,2 +1,3 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_test_convert_usered,access_test_convert_usered,model_test_convert_usered,base.group_user,1,1,1,1 +access_test_convert_test_model,access_test_convert_test_model,model_test_convert_test_model,,1,1,1,1
\ No newline at end of file diff --git a/odoo/addons/test_convert/models.py b/odoo/addons/test_convert/models.py index e48ac2d1e7f..c98eafe624a 100644 --- a/odoo/addons/test_convert/models.py +++ b/odoo/addons/test_convert/models.py @@ -1,8 +1,23 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import models, fields, api +from odoo import api, fields, models +class TestModel(models.Model): + _name = 'test_convert.test_model' + _description = "Test Convert Model" + + @api.model + def action_test_date(self, today_date): + return True + + @api.model + def action_test_time(self, cur_time): + return True + + @api.model + def action_test_timezone(self, timezone): + return True class Usered(models.Model): _name = 'test_convert.usered' diff --git a/odoo/addons/test_convert/tests/test_convert.py b/odoo/addons/test_convert/tests/test_convert.py index 709a5eeab10..a2615ac488b 100644 --- a/odoo/addons/test_convert/tests/test_convert.py +++ b/odoo/addons/test_convert/tests/test_convert.py @@ -4,8 +4,10 @@ import collections import unittest +from lxml import etree as ET from lxml.builder import E +import odoo from odoo.tests import common from odoo.tools.convert import xml_import, _eval_xml @@ -16,6 +18,25 @@ class TestEvalXML(common.TransactionCase): def eval_xml(self, node, obj=None): return _eval_xml(obj, node, self.env) + def test_function_eval(self): + def id_get(): pass + Obj = collections.namedtuple('Obj', ['module', 'idref', 'id_get']) + obj = Obj('test_convert', {}, id_get) + + try: + test_datetime = ET.XML("<function name='action_test_date' model='test_convert.test_model' eval='[datetime.now().strftime(\"%Y-%m-%d %H:%M:%S\")]'/>") + self.eval_xml(node=test_datetime, obj=obj) + test_time = ET.XML("<function name='action_test_time' model='test_convert.test_model' eval='[time.strftime(\"%Y-%m-%d %H:%M:%S\")]'/>") + self.eval_xml(node=test_time, obj=obj) + test_timedelta = ET.XML("<function name='action_test_date' model='test_convert.test_model' eval='[(datetime.today()-timedelta(days=365)).strftime(\"%Y-%m-%d %H:%M:%S\")]'/>") + self.eval_xml(node=test_timedelta, obj=obj) + test_relativedelta = ET.XML("<function name='action_test_date' model='test_convert.test_model' eval='[(datetime.today()+relativedelta(months=3)).strftime(\"%Y-%m-%d %H:%M:%S\")]'/>") + self.eval_xml(node=test_relativedelta, obj=obj) + test_timezone = ET.XML("<function name='action_test_timezone' model='test_convert.test_model' eval='[pytz.timezone(\"Asia/Calcutta\")]'/>") + self.eval_xml(node=test_timezone, obj=obj) + except ValueError as e: + self.fail(e.message) + def test_char(self): self.assertEqual( self.eval_xml(Field("foo")), diff --git a/odoo/addons/test_new_api/tests/test_new_fields.py b/odoo/addons/test_new_api/tests/test_new_fields.py index bb9f10c7792..e7deced446f 100644 --- a/odoo/addons/test_new_api/tests/test_new_fields.py +++ b/odoo/addons/test_new_api/tests/test_new_fields.py @@ -255,11 +255,12 @@ class TestFields(common.TransactionCase): self.assertEqual(c.display_name, 'B / C') self.assertEqual(d.display_name, 'B / C / D') - b.name = 'X' + # rename several records to trigger several recomputations at once + (d + c + b).write({'name': 'X'}) self.assertEqual(a.display_name, 'A') self.assertEqual(b.display_name, 'X') - self.assertEqual(c.display_name, 'X / C') - self.assertEqual(d.display_name, 'X / C / D') + self.assertEqual(c.display_name, 'X / X') + self.assertEqual(d.display_name, 'X / X / X') # delete b; both c and d are deleted in cascade; c should also be marked # to recompute, but recomputation should not fail... diff --git a/odoo/fields.py b/odoo/fields.py index 857a942bdb2..cde78d4b180 100644 --- a/odoo/fields.py +++ b/odoo/fields.py @@ -116,8 +116,8 @@ class Field(MetaField('DummyField', (object,), {})): :param required: whether the value of the field is required (boolean, by default ``False``) - :param index: whether the field is indexed in database (boolean, by - default ``False``) + :param index: whether the field is indexed in database. Note: no effect + on non-stored and virtual fields. (boolean, by default ``False``) :param default: the default value for the field; this is either a static value, or a function taking a recordset and returning a value; use @@ -640,6 +640,7 @@ class Field(MetaField('DummyField', (object,), {})): _related_help = property(attrgetter('help')) _related_groups = property(attrgetter('groups')) _related_group_operator = property(attrgetter('group_operator')) + _related_context_dependent = property(attrgetter('context_dependent')) @property def base_field(self): @@ -1090,6 +1091,8 @@ class Field(MetaField('DummyField', (object,), {})): recs = record._recompute_check(self) if recs: # recompute the value (only in cache) + if self.recursive: + recs = record self.compute_value(recs) # HACK: if result is in the wrong cache, copy values if recs.env != env: diff --git a/odoo/models.py b/odoo/models.py index 77701cf676a..ddbae89f9a4 100644 --- a/odoo/models.py +++ b/odoo/models.py @@ -4035,8 +4035,7 @@ Fields: # The quotes surrounding `ir_translation` are important as well. unique_translation_subselect = """ (SELECT res_id, value FROM "ir_translation" - WHERE name=%s AND lang=%s AND value!='' - ORDER BY res_id, id DESC) + WHERE type='model' AND name=%s AND lang=%s AND value!='') """ alias, alias_statement = query.add_join( (table_alias, unique_translation_subselect, 'id', 'res_id', field), @@ -5478,6 +5477,19 @@ Fields: if field.type in ('one2many', 'many2many') else record[name] ) + def has_changed(self, name): + """ Return whether a field on record has changed. """ + record = self['<record>'] + subnames = self['<tree>'][name] + if not subnames: + return self[name] != record[name] + else: + return len(self[name]) != len(record[name]) or any( + line_snapshot.has_changed(subname) + for line_snapshot in self[name] + for subname in subnames + ) + def diff(self, other): """ Return the values in ``self`` that differ from ``other``. Requires record cache invalidation for correct output! @@ -5553,24 +5565,23 @@ Fields: result = {'warnings': OrderedSet()} - # process names in order (or the keys of values if no name given) + # process names in order while todo: - name = todo.pop(0) - if name in done: - continue - done.add(name) - # apply field-specific onchange methods - if field_onchange.get(name): - record._onchange_eval(name, field_onchange[name], result) - - # make a snapshot (this forces evaluation of computed fields) - snapshot1 = Snapshot(record, nametree) + for name in todo: + if field_onchange.get(name): + record._onchange_eval(name, field_onchange[name], result) + done.add(name) + + # determine which fields to process for the next pass + todo = [ + name + for name in nametree + if name not in done and snapshot0.has_changed(name) + ] - # determine which fields have been modified - for name in nametree: - if snapshot1[name] != snapshot0[name]: - todo.append(name) + # make the snapshot with the final values of record + snapshot1 = Snapshot(record, nametree) # determine values that have changed by comparing snapshots self.invalidate_cache() diff --git a/odoo/service/server.py b/odoo/service/server.py index 977cc0a3630..207d590e724 100644 --- a/odoo/service/server.py +++ b/odoo/service/server.py @@ -329,7 +329,7 @@ class ThreadedServer(CommonServer): os._exit(0) # interrupt run() to start shutdown raise KeyboardInterrupt() - elif sig == signal.SIGXCPU: + elif hasattr(signal, 'SIGXCPU') and sig == signal.SIGXCPU: sys.stderr.write("CPU time limit exceeded! Shutting down immediately\n") sys.stderr.flush() os._exit(0) diff --git a/odoo/tests/common.py b/odoo/tests/common.py index 4cdb964d1e7..c121c6389a4 100644 --- a/odoo/tests/common.py +++ b/odoo/tests/common.py @@ -1426,7 +1426,8 @@ class Form(object): if not any(spec[f] for f in fields): return - result = self._model.onchange(self._onchange_values(), fields, spec) + record = self._model.browse(self._values.get('id')) + result = record.onchange(self._onchange_values(), fields, spec) if result.get('warning'): _logger.getChild('onchange').warn("%(title)s %(message)s" % result.get('warning')) values = result.get('value', {}) diff --git a/odoo/tools/convert.py b/odoo/tools/convert.py index b2ea452192b..ac4098d4f84 100644 --- a/odoo/tools/convert.py +++ b/odoo/tools/convert.py @@ -182,15 +182,18 @@ def _eval_xml(self, node, env): return tuple(res) return res elif node.tag == "function": - model = env[node.get('model')] + model_str = node.get('model') + model = env[model_str] method_name = node.get('name') # determine arguments args = [] kwargs = {} a_eval = node.get('eval') + if a_eval: - self.idref['ref'] = self.id_get - args = list(safe_eval(a_eval, self.idref)) + idref2 = _get_idref(self, env, model_str, self.idref) + args = safe_eval(a_eval, idref2) + args = list(safe_eval(a_eval, idref2)) for child in node: if child.tag == 'value' and child.get('name'): kwargs[child.get('name')] = _eval_xml(self, child, env) diff --git a/odoo/tools/image.py b/odoo/tools/image.py index 6f02f3c3d65..498a4cfda5b 100644 --- a/odoo/tools/image.py +++ b/odoo/tools/image.py @@ -118,14 +118,9 @@ class ImageProcess(): if output_format == 'PNG': opt['optimize'] = True if quality: - alpha = False - if output_image.mode in ('RGBA', 'LA') or (output_image.mode == 'P' and 'transparency' in output_image.info): - alpha = output_image.convert('RGBA').split()[-1] if output_image.mode != 'P': # Floyd Steinberg dithering by default output_image = output_image.convert('RGBA').convert('P', palette=Image.WEB, colors=256) - if alpha: - output_image.putalpha(alpha) if output_format == 'JPEG': opt['optimize'] = True opt['quality'] = quality or 95 diff --git a/odoo/tools/translate.py b/odoo/tools/translate.py index 160cce4f6e4..9dfd9ec09e0 100644 --- a/odoo/tools/translate.py +++ b/odoo/tools/translate.py @@ -984,8 +984,11 @@ def trans_generate(lang, modules, cr): extra_comments = extra_comments or [] if not module: return src_file = open(fabsolutepath, 'rb') + options = {} + if extract_method == 'python': + options['encoding'] = 'UTF-8' try: - for extracted in extract.extract(extract_method, src_file, keywords=extract_keywords): + for extracted in extract.extract(extract_method, src_file, keywords=extract_keywords, options=options): # Babel 0.9.6 yields lineno, message, comments # Babel 1.3 yields lineno, message, comments, context lineno, message, comments = extracted[:3] |
