[MERGE] forward port branch 12.0 up to 6a0675d36d

This commit is contained in:
Christophe Simonis
2019-01-14 10:34:50 +01:00
99 changed files with 1161 additions and 223 deletions
@@ -667,8 +667,9 @@ class AccountBankStatementLine(models.Model):
#create the res.partner.bank if needed
if self.account_number and self.partner_id and not self.bank_account_id:
bank_account = self.env['res.partner.bank'].search(
[('acc_number', '=', self.account_number), ('partner_id', '=', self.partner_id.id)])
# Search bank account without partner to handle the case the res.partner.bank already exists but is set
# on a different partner.
bank_account = self.env['res.partner.bank'].search([('acc_number', '=', self.account_number)])
if not bank_account:
bank_account = self.env['res.partner.bank'].create({
'acc_number': self.account_number, 'partner_id': self.partner_id.id
+8 -6
View File
@@ -992,6 +992,7 @@ class AccountInvoice(models.Model):
@api.multi
def get_taxes_values(self):
tax_grouped = {}
round_curr = self.currency_id.round
for line in self.invoice_line_ids:
if not line.account_id:
continue
@@ -1003,9 +1004,10 @@ class AccountInvoice(models.Model):
if key not in tax_grouped:
tax_grouped[key] = val
tax_grouped[key]['base'] = round_curr(val['base'])
else:
tax_grouped[key]['amount'] += val['amount']
tax_grouped[key]['base'] += val['base']
tax_grouped[key]['base'] += round_curr(val['base'])
return tax_grouped
@api.multi
@@ -1734,17 +1736,17 @@ class AccountInvoiceLine(models.Model):
self.price_unit = 0.0
domain['uom_id'] = []
else:
self_lang = self
if part.lang:
product = self.product_id.with_context(lang=part.lang)
else:
product = self.product_id
self_lang = self.with_context(lang=part.lang)
product = self_lang.product_id
account = self.get_invoice_line_account(type, product, fpos, company)
if account:
self.account_id = account.id
self._set_taxes()
product_name = self._get_invoice_line_name_from_product()
product_name = self_lang._get_invoice_line_name_from_product()
if product_name != None:
self.name = product_name
+11 -8
View File
@@ -775,7 +775,7 @@ class AccountMoveLine(models.Model):
(multiple_currency and float_compare(total_debit, total_credit, precision_rounding=digits_rounding_precision) == 0):
exchange_move_id = False
# Eventually create a journal entry to book the difference due to foreign currency's exchange rate that fluctuates
if to_balance and any([residual for aml, residual in to_balance.values()]):
if to_balance and any([not float_is_zero(residual, precision_rounding=digits_rounding_precision) for aml, residual in to_balance.values()]):
exchange_move = self.env['account.move'].create(
self.env['account.full.reconcile']._prepare_exchange_diff_move(move_date=maxdate, company=amls[0].company_id))
part_reconcile = self.env['account.partial.reconcile']
@@ -804,7 +804,7 @@ class AccountMoveLine(models.Model):
(debit_moves + credit_moves).read([field])
to_create = []
cash_basis = debit_moves and debit_moves[0].account_id.internal_type in ('receivable', 'payable') or False
cash_basis_percentage_before_rec = []
cash_basis_percentage_before_rec = {}
while (debit_moves and credit_moves):
debit_move = debit_moves[0]
credit_move = credit_moves[0]
@@ -839,7 +839,7 @@ class AccountMoveLine(models.Model):
if cash_basis:
tmp_set = debit_move | credit_move
cash_basis_percentage_before_rec.append(tmp_set._get_matched_percentage())
cash_basis_percentage_before_rec.update(tmp_set._get_matched_percentage())
to_create.append({
'debit_move_id': debit_move.id,
@@ -850,14 +850,12 @@ class AccountMoveLine(models.Model):
})
part_rec = self.env['account.partial.reconcile']
index = 0
with self.env.norecompute():
for partial_rec_dict in to_create:
new_rec = self.env['account.partial.reconcile'].create(partial_rec_dict)
part_rec += new_rec
if cash_basis:
new_rec.create_tax_cash_basis_entry(cash_basis_percentage_before_rec[index])
index += 1
new_rec.create_tax_cash_basis_entry(cash_basis_percentage_before_rec)
self.recompute()
return debit_moves+credit_moves
@@ -1468,10 +1466,14 @@ class AccountPartialReconcile(models.Model):
#move_date is the max of the 2 reconciled items
if move_date < move.date:
move_date = move.date
percentage_before = percentage_before_rec[move.id]
percentage_after = move.line_ids[0]._get_matched_percentage()[move.id]
# update the percentage before as the move can be part of
# multiple partial reconciliations
percentage_before_rec[move.id] = percentage_after
for line in move.line_ids:
if not line.tax_exigible:
percentage_before = percentage_before_rec[move.id]
percentage_after = line._get_matched_percentage()[move.id]
#amount is the current cash_basis amount minus the one before the reconciliation
amount = line.balance * percentage_after - line.balance * percentage_before
rounded_amt = self._get_amount_tax_cash_basis(amount, line)
@@ -1543,6 +1545,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,
})
if newly_created_move:
if move_date > (self.company_id.period_lock_date or date.min) and newly_created_move.date != move_date:
# The move date should be the maximum date between payment and invoice (in case
+4 -1
View File
@@ -736,12 +736,15 @@ class account_payment(models.Model):
""" Return dict to create the payment move
"""
journal = journal or self.journal_id
return {
move_vals = {
'date': self.payment_date,
'ref': self.communication or '',
'company_id': self.company_id.id,
'journal_id': journal.id,
}
if self.move_name:
move_vals['name'] = self.move_name
return move_vals
def _get_shared_move_line_vals(self, debit, credit, amount_currency, move_id, invoice_id=False):
""" Returns values common to both move lines (except for debit, credit and amount_currency which are reversed)
@@ -903,11 +903,11 @@ var StatementModel = BasicModel.extend({
});
var company_currency = session.get_currency(line.st_line.currency_id);
var company_precision = company_currency && company_currency.digits[1] || 2;
total = utils.round_precision(total*1000, company_precision)/1000 || 0;
total = utils.round_decimals(total*1000, company_precision)/1000 || 0;
if(isOtherCurrencyId){
var other_currency = session.get_currency(isOtherCurrencyId);
var other_precision = other_currency && other_currency.digits[1] || 2;
amount_currency = utils.round_precision(amount_currency, other_precision)
amount_currency = utils.round_decimals(amount_currency, other_precision)
}
line.balance = {
amount: total,
@@ -103,3 +103,66 @@ class TestAccountCustomerInvoice(AccountTestUsers):
# I clicked on Add Credit Note button.
self.account_invoice_refund_0.invoice_refund()
def test_customer_invoice_tax(self):
self.env.user.company_id.tax_calculation_rounding_method = 'round_globally'
payment_term = self.env.ref('account.account_payment_term_advance')
journalrec = self.env['account.journal'].search([('type', '=', 'sale')])[0]
partner3 = self.env.ref('base.res_partner_3')
account_id = self.env['account.account'].search([('user_type_id', '=', self.env.ref('account.data_account_type_revenue').id)], limit=1).id
tax = self.env['account.tax'].create({
'name': 'Tax 15.0',
'amount': 15.0,
'amount_type': 'percent',
'type_tax_use': 'sale',
})
invoice_line_data = [
(0, 0,
{
'product_id': self.env.ref('product.product_product_1').id,
'quantity': 40.0,
'account_id': account_id,
'name': 'product test 1',
'discount' : 10.00,
'price_unit': 2.27,
'invoice_line_tax_ids': [(6, 0, [tax.id])],
}
),
(0, 0,
{
'product_id': self.env.ref('product.product_product_2').id,
'quantity': 21.0,
'account_id': self.env['account.account'].search([('user_type_id', '=', self.env.ref('account.data_account_type_revenue').id)], limit=1).id,
'name': 'product test 2',
'discount' : 10.00,
'price_unit': 2.77,
'invoice_line_tax_ids': [(6, 0, [tax.id])],
}
),
(0, 0,
{
'product_id': self.env.ref('product.product_product_3').id,
'quantity': 21.0,
'account_id': self.env['account.account'].search([('user_type_id', '=', self.env.ref('account.data_account_type_revenue').id)], limit=1).id,
'name': 'product test 3',
'discount' : 10.00,
'price_unit': 2.77,
'invoice_line_tax_ids': [(6, 0, [tax.id])],
}
)
]
invoice = self.env['account.invoice'].create(dict(
name="Test Customer Invoice",
reference_type="none",
payment_term_id=payment_term.id,
journal_id=journalrec.id,
partner_id=partner3.id,
invoice_line_ids=invoice_line_data
))
self.assertEquals(invoice.amount_untaxed, sum([x.base for x in invoice.tax_line_ids]))
+337
View File
@@ -49,6 +49,58 @@ class TestReconciliation(AccountingTestCase):
'payment_type': 'inbound',
})
self.expense_account = self.env['account.account'].create({
'name': 'EXP',
'code': 'EXP',
'user_type_id': self.env.ref('account.data_account_type_expenses').id,
'company_id': company.id,
})
# cash basis intermediary account
self.tax_waiting_account = self.env['account.account'].create({
'name': 'TAX_WAIT',
'code': 'TWAIT',
'user_type_id': self.env.ref('account.data_account_type_current_liabilities').id,
'reconcile': True,
'company_id': company.id,
})
# cash basis final account
self.tax_final_account = self.env['account.account'].create({
'name': 'TAX_TO_DEDUCT',
'code': 'TDEDUCT',
'user_type_id': self.env.ref('account.data_account_type_current_assets').id,
'company_id': company.id,
})
self.tax_base_amount_account = self.env['account.account'].create({
'name': 'TAX_BASE',
'code': 'TBASE',
'user_type_id': self.env.ref('account.data_account_type_current_assets').id,
'company_id': company.id,
})
# Journals
self.purchase_journal = self.env['account.journal'].create({
'name': 'purchase',
'code': 'PURCH',
'type': 'purchase',
})
self.cash_basis_journal = self.env['account.journal'].create({
'name': 'CABA',
'code': 'CABA',
'type': 'general',
})
# Tax Cash Basis
self.tax_cash_basis = self.env['account.tax'].create({
'name': 'cash basis 20%',
'type_tax_use': 'purchase',
'company_id': company.id,
'amount': 20,
'account_id': self.tax_waiting_account.id,
'tax_exigibility': 'on_payment',
'cash_basis_account_id': self.tax_final_account.id,
'cash_basis_base_account_id': self.tax_base_amount_account.id,
})
def create_invoice(self, type='out_invoice', invoice_amount=50, currency_id=None):
#we create an invoice in given currency
invoice = self.account_invoice_model.create({'partner_id': self.partner_agrolait_id,
@@ -1107,3 +1159,288 @@ class TestReconciliation(AccountingTestCase):
receivable_lines = invoice.move_id.line_ids.filtered(lambda line: line.account_id == self.account_rcv).sorted('date_maturity')[0]
self.assertTrue(receivable_lines.matched_credit_ids)
def test_reconciliation_cash_basis01(self):
# Simulates an expense made up by 2 lines
# one is subject to a cash basis tax
# the other is not subject to tax
company = self.env.ref('base.main_company')
company.tax_cash_basis_journal_id = self.cash_basis_journal
AccountMoveLine = self.env['account.move.line'].with_context(check_move_validity=False)
# Purchase
purchase_move = self.env['account.move'].create({
'name': 'purchase',
'journal_id': self.purchase_journal.id,
})
purchase_payable_line0 = AccountMoveLine.create({
'account_id': self.account_rsa.id,
'credit': 100,
'move_id': purchase_move.id,
})
purchase_payable_line1 = AccountMoveLine.create({
'account_id': self.account_rsa.id,
'credit': 50,
'move_id': purchase_move.id,
})
AccountMoveLine.create({
'name': 'expensNoTax',
'account_id': self.expense_account.id,
'debit': 50,
'move_id': purchase_move.id,
})
AccountMoveLine.create({
'name': 'expenseTaxed',
'account_id': self.expense_account.id,
'debit': 83.33,
'move_id': purchase_move.id,
'tax_ids': [(4, self.tax_cash_basis.id, False)],
})
tax_line = AccountMoveLine.create({
'name': 'TaxLine',
'account_id': self.tax_waiting_account.id,
'debit': 16.67,
'move_id': purchase_move.id,
'tax_line_id': self.tax_cash_basis.id,
})
purchase_move.post()
# Payment Move
payment_move = self.env['account.move'].create({
'name': 'payment',
'journal_id': self.bank_journal_euro.id,
})
payment_payable_line = AccountMoveLine.create({
'account_id': self.account_rsa.id,
'debit': 150,
'move_id': payment_move.id,
})
AccountMoveLine.create({
'account_id': self.account_euro.id,
'credit': 150,
'move_id': payment_move.id,
})
payment_move.post()
to_reconcile = (purchase_move + payment_move).mapped('line_ids').filtered(lambda l: l.account_id.internal_type == 'payable')
to_reconcile.reconcile()
cash_basis_moves = self.env['account.move'].search([('journal_id', '=', self.cash_basis_journal.id)])
self.assertEqual(len(cash_basis_moves), 2)
self.assertTrue(cash_basis_moves.exists())
# check reconciliation in Payable account
self.assertTrue(purchase_payable_line0.full_reconcile_id.exists())
self.assertEqual(purchase_payable_line0.full_reconcile_id.reconciled_line_ids,
purchase_payable_line0 + purchase_payable_line1 + payment_payable_line)
cash_basis_aml_ids = cash_basis_moves.mapped('line_ids')
# check reconciliation in the tax waiting account
self.assertTrue(tax_line.full_reconcile_id.exists())
self.assertEqual(tax_line.full_reconcile_id.reconciled_line_ids,
cash_basis_aml_ids.filtered(lambda l: l.account_id == self.tax_waiting_account) + tax_line)
self.assertEqual(len(cash_basis_aml_ids), 8)
# check amounts
cash_basis_move1 = cash_basis_moves.filtered(lambda m: m.amount == 33.34)
cash_basis_move2 = cash_basis_moves.filtered(lambda m: m.amount == 66.66)
self.assertTrue(cash_basis_move1.exists())
self.assertTrue(cash_basis_move2.exists())
# For first move
move_lines = cash_basis_move1.line_ids
base_amount_tax_lines = move_lines.filtered(lambda l: l.account_id == self.tax_base_amount_account)
self.assertEqual(len(base_amount_tax_lines), 2)
self.assertAlmostEqual(sum(base_amount_tax_lines.mapped('credit')), 27.78)
self.assertAlmostEqual(sum(base_amount_tax_lines.mapped('debit')), 27.78)
self.assertAlmostEqual((move_lines - base_amount_tax_lines).filtered(lambda l: l.account_id == self.tax_waiting_account).credit,
5.56)
self.assertAlmostEqual((move_lines - base_amount_tax_lines).filtered(lambda l: l.account_id == self.tax_final_account).debit,
5.56)
# For second move
move_lines = cash_basis_move2.line_ids
base_amount_tax_lines = move_lines.filtered(lambda l: l.account_id == self.tax_base_amount_account)
self.assertEqual(len(base_amount_tax_lines), 2)
self.assertAlmostEqual(sum(base_amount_tax_lines.mapped('credit')), 55.55)
self.assertAlmostEqual(sum(base_amount_tax_lines.mapped('debit')), 55.55)
self.assertAlmostEqual((move_lines - base_amount_tax_lines).filtered(lambda l: l.account_id == self.tax_waiting_account).credit,
11.11)
self.assertAlmostEqual((move_lines - base_amount_tax_lines).filtered(lambda l: l.account_id == self.tax_final_account).debit,
11.11)
def test_reconciliation_cash_basis02(self):
# Simulates an invoice made up by 2 lines
# both subjected to cash basis taxes
# with 2 payment terms
# And partial payment not martching any payment term
company = self.env.ref('base.main_company')
company.tax_cash_basis_journal_id = self.cash_basis_journal
tax_cash_basis10percent = self.tax_cash_basis.copy({'amount': 10})
tax_waiting_account10 = self.tax_waiting_account.copy({
'name': 'TAX WAIT 10',
'code': 'TWAIT1',
})
AccountMoveLine = self.env['account.move.line'].with_context(check_move_validity=False)
# Purchase
purchase_move = self.env['account.move'].create({
'name': 'invoice',
'journal_id': self.purchase_journal.id,
})
purchase_payable_line0 = AccountMoveLine.create({
'account_id': self.account_rsa.id,
'credit': 105,
'move_id': purchase_move.id,
})
purchase_payable_line1 = AccountMoveLine.create({
'account_id': self.account_rsa.id,
'credit': 50,
'move_id': purchase_move.id,
})
AccountMoveLine.create({
'name': 'expenseTaxed 10%',
'account_id': self.expense_account.id,
'debit': 50,
'move_id': purchase_move.id,
'tax_ids': [(4, tax_cash_basis10percent.id, False)],
})
tax_line0 = AccountMoveLine.create({
'name': 'TaxLine0',
'account_id': tax_waiting_account10.id,
'debit': 5,
'move_id': purchase_move.id,
'tax_line_id': tax_cash_basis10percent.id,
})
AccountMoveLine.create({
'name': 'expenseTaxed 20%',
'account_id': self.expense_account.id,
'debit': 83.33,
'move_id': purchase_move.id,
'tax_ids': [(4, self.tax_cash_basis.id, False)],
})
tax_line1 = AccountMoveLine.create({
'name': 'TaxLine1',
'account_id': self.tax_waiting_account.id,
'debit': 16.67,
'move_id': purchase_move.id,
'tax_line_id': self.tax_cash_basis.id,
})
purchase_move.post()
# Payment Move
payment_move0 = self.env['account.move'].create({
'name': 'payment',
'journal_id': self.bank_journal_euro.id,
})
payment_payable_line0 = AccountMoveLine.create({
'account_id': self.account_rsa.id,
'debit': 40,
'move_id': payment_move0.id,
})
AccountMoveLine.create({
'account_id': self.account_euro.id,
'credit': 40,
'move_id': payment_move0.id,
})
payment_move0.post()
# Payment Move
payment_move1 = self.env['account.move'].create({
'name': 'payment',
'journal_id': self.bank_journal_euro.id,
})
payment_payable_line1 = AccountMoveLine.create({
'account_id': self.account_rsa.id,
'debit': 115,
'move_id': payment_move1.id,
})
AccountMoveLine.create({
'account_id': self.account_euro.id,
'credit': 115,
'move_id': payment_move1.id,
})
payment_move1.post()
(purchase_move + payment_move0).mapped('line_ids').filtered(lambda l: l.account_id.internal_type == 'payable').reconcile()
(purchase_move + payment_move1).mapped('line_ids').filtered(lambda l: l.account_id.internal_type == 'payable').reconcile()
cash_basis_moves = self.env['account.move'].search([('journal_id', '=', self.cash_basis_journal.id)])
self.assertEqual(len(cash_basis_moves), 3)
self.assertTrue(cash_basis_moves.exists())
# check reconciliation in Payable account
self.assertTrue(purchase_payable_line0.full_reconcile_id.exists())
self.assertEqual(purchase_payable_line0.full_reconcile_id.reconciled_line_ids,
purchase_payable_line0 + purchase_payable_line1 + payment_payable_line0 + payment_payable_line1)
cash_basis_aml_ids = cash_basis_moves.mapped('line_ids')
# check reconciliation in the tax waiting account
self.assertTrue(tax_line0.full_reconcile_id.exists())
self.assertEqual(tax_line0.full_reconcile_id.reconciled_line_ids,
cash_basis_aml_ids.filtered(lambda l: l.account_id == tax_waiting_account10) + tax_line0)
self.assertTrue(tax_line1.full_reconcile_id.exists())
self.assertEqual(tax_line1.full_reconcile_id.reconciled_line_ids,
cash_basis_aml_ids.filtered(lambda l: l.account_id == self.tax_waiting_account) + tax_line1)
self.assertEqual(len(cash_basis_aml_ids), 24)
# check amounts
expected_move_amounts = [
{'base_20': 56.45, 'tax_20': 11.29, 'base_10': 33.87, 'tax_10': 3.39},
{'base_20': 21.50, 'tax_20': 4.30, 'base_10': 12.90, 'tax_10': 1.29},
{'base_20': 5.38, 'tax_20': 1.08, 'base_10': 3.23, 'tax_10': 0.32},
]
index = 0
for cb_move in cash_basis_moves.sorted('amount', reverse=True):
expected = expected_move_amounts[index]
move_lines = cb_move.line_ids
base_amount_tax_lines20per = move_lines.filtered(lambda l: l.account_id == self.tax_base_amount_account and '20%' in l.name)
base_amount_tax_lines10per = move_lines.filtered(lambda l: l.account_id == self.tax_base_amount_account and '10%' in l.name)
self.assertEqual(len(base_amount_tax_lines20per), 2)
self.assertAlmostEqual(sum(base_amount_tax_lines20per.mapped('credit')), expected['base_20'])
self.assertAlmostEqual(sum(base_amount_tax_lines20per.mapped('debit')), expected['base_20'])
self.assertEqual(len(base_amount_tax_lines10per), 2)
self.assertAlmostEqual(sum(base_amount_tax_lines10per.mapped('credit')), expected['base_10'])
self.assertAlmostEqual(sum(base_amount_tax_lines10per.mapped('debit')), expected['base_10'])
self.assertAlmostEqual(
(move_lines - base_amount_tax_lines20per - base_amount_tax_lines10per)
.filtered(lambda l: l.account_id == self.tax_waiting_account).credit,
expected['tax_20']
)
self.assertAlmostEqual(
(move_lines - base_amount_tax_lines20per - base_amount_tax_lines10per)
.filtered(lambda l: 'TaxLine1' in l.name).debit,
expected['tax_20']
)
self.assertAlmostEqual(
(move_lines - base_amount_tax_lines20per - base_amount_tax_lines10per)
.filtered(lambda l: l.account_id == tax_waiting_account10).credit,
expected['tax_10']
)
self.assertAlmostEqual(
(move_lines - base_amount_tax_lines20per - base_amount_tax_lines10per)
.filtered(lambda l: 'TaxLine0' in l.name).debit,
expected['tax_10']
)
index += 1
+1 -1
View File
@@ -291,7 +291,7 @@
<field name="inbound_payment_method_ids" widget="many2many_checkboxes"/>
<field name="outbound_payment_method_ids" widget="many2many_checkboxes"/>
</group>
<group string="Accounting App Options" attrs="{'invisible': [('type', 'not in', ['sale', 'purchase', 'bank', 'cash'])]}">
<group string="Accounting App Options">
<field name="profit_account_id" attrs="{'invisible': [('type', '!=', 'cash')]}"/>
<field name="loss_account_id" attrs="{'invisible': [('type', '!=', 'cash')]}"/>
<field name="group_invoice_lines" attrs="{'invisible': [('type', 'not in', ['sale', 'purchase'])]}"/>
@@ -4,7 +4,7 @@ odoo.define('barcodes.field', function(require) {
var AbstractField = require('web.AbstractField');
var basicFields = require('web.basic_fields');
var fieldRegistry = require('web.field_registry');
var is_special_key = require('barcodes.BarcodeEvents').BarcodeEvents.is_special_key;
var BarcodeEvents = require('barcodes.BarcodeEvents').BarcodeEvents;
// Field in which the user can both type normally and scan barcodes
@@ -42,7 +42,7 @@ var FieldFloatScannable = basicFields.FieldFloat.extend({
_onKeypress: function (e) {
/* only simulate a keypress if it has been previously prevented */
if (e.dispatched_by_barcode_reader !== true) {
if (!is_special_key(e)) {
if (!BarcodeEvents.is_special_key(e)) {
e.preventDefault();
}
return;
+7 -2
View File
@@ -16,8 +16,13 @@ class StockQuantPackage(models.Model):
@api.depends('quant_ids')
def _compute_weight(self):
weight = 0.0
for quant in self.quant_ids:
weight += quant.quantity * quant.product_id.weight
if self.env.context.get('picking_id'):
current_picking_move_line_ids = self.env['stock.move.line'].search([('result_package_id', '=', self.id), ('picking_id', '=', self.env.context['picking_id'])])
for ml in current_picking_move_line_ids:
weight += ml.product_uom_id._compute_quantity(ml.qty_done,ml.product_id.uom_id) * ml.product_id.weight
else:
for quant in self.quant_ids:
weight += quant.quantity * quant.product_id.weight
self.weight = weight
def _get_default_weight_uom(self):
+5
View File
@@ -658,6 +658,11 @@ msgstr ""
msgid "Analyze from"
msgstr ""
#. module: hr_holidays
#: model:ir.model.fields,field_description:hr_holidays.field_hr_leave_type__double_validation
msgid "Apply Double Validation"
msgstr ""
#. module: hr_holidays
#: model_terms:ir.ui.view,arch_db:hr_holidays.view_evaluation_report_graph
msgid "Appraisal Analysis"
+2 -3
View File
@@ -427,8 +427,7 @@ class HolidaysRequest(models.Model):
employee = self.env['hr.employee'].browse(employee_id)
return employee.get_work_days_data(date_from, date_to)['days']
time_delta = date_to - date_from
return math.ceil(time_delta.days + float(time_delta.seconds) / 86400)
return self.env.user.company_id.resource_calendar_id.get_work_hours_count() / HOURS_PER_DAY
####################################################
# ORM Overrides methods
@@ -612,7 +611,7 @@ class HolidaysRequest(models.Model):
'date_from': self.date_from,
'date_to': self.date_to,
'notes': self.notes,
'number_of_days': self.number_of_days,
'number_of_days': employee.get_work_days_data(self.date_from, self.date_to)['days'],
'parent_id': self.id,
'employee_id': employee.id
}
@@ -67,6 +67,14 @@ class HolidaysType(models.Model):
('hr', 'Payroll Officer'),
('manager', 'Team Leader'),
('both', 'Team Leader and Payroll Officer')], default='hr', string='Validation By')
# TODO: remove me in master, the behavior is exactly the same if you choose 'hr' or 'manager'
# in the validation_type field. This field is used only to hide this possibility to the user
# to avoid misunderstandings. This field and its corresponding's functions must be removed once
# the functional part is implemented.
double_validation = fields.Boolean(string='Apply Double Validation',
compute='_compute_validation_type', inverse='_inverse_validation_type',
help="When selected, the Allocation/Leave Requests for this type require a second validation to be approved.")
allocation_type = fields.Selection([
('fixed', 'Fixed by HR'),
('fixed_allocation', 'Fixed by HR + allocation request'),
@@ -88,6 +96,25 @@ class HolidaysType(models.Model):
leave_notif_subtype_id = fields.Many2one('mail.message.subtype', string='Leave Notification Subtype')
allocation_notif_subtype_id = fields.Many2one('mail.message.subtype', string='Allocation Notification Subtype')
# TODO: remove me in master
@api.depends('validation_type')
def _compute_validation_type(self):
for holiday_type in self:
if holiday_type.validation_type == 'both':
holiday_type.double_validation = True
else:
holiday_type.double_validation = False
# TODO: remove me in master
def _inverse_validation_type(self):
for holiday_type in self:
if holiday_type.double_validation == True:
holiday_type.validation_type = 'both'
else:
#IF to preserve the information (hr or manager)
if holiday_type.validation_type == 'both':
holiday_type.validation_type = 'hr'
@api.multi
@api.constrains('validity_start', 'validity_stop')
def _check_validity_dates(self):
@@ -49,7 +49,8 @@
<field name="time_type" groups="base.group_no_one"/>
</group>
<group name="validation" string="Validation">
<field name="validation_type" widget="radio"/>
<field name="validation_type" widget="radio" invisible="1"/>
<field name="double_validation"/>
</group>
<group name="visibility" string="Allocation">
<field name="allocation_type" widget="radio" force_save="1"/>
@@ -110,7 +111,8 @@
<field name="display_name"/>
<field name="code"/>
<field name="allocation_type"/>
<field name="validation_type"/>
<field name="validation_type" invisible="1"/>
<field name="double_validation"/>
<field name="validity_start"/>
<field name="validity_stop"/>
</tree>
@@ -144,13 +144,13 @@ class HrContract(models.Model):
)
@api.depends('yearly_cost_before_charges', 'social_security_contributions', 'wage',
'social_security_contributions', 'double_holidays', 'warrants_cost', 'meal_voucher_paid_by_employer')
'social_security_contributions', 'warrants_cost', 'meal_voucher_paid_by_employer')
def _compute_final_yearly_costs(self):
for contract in self:
contract.final_yearly_costs = (
contract.yearly_cost_before_charges +
contract.social_security_contributions +
contract.double_holidays +
contract.wage * 0.92 +
contract.warrants_cost +
(220.0 * contract.meal_voucher_paid_by_employer)
)
@@ -197,11 +197,11 @@ class HrContract(models.Model):
for contract in self:
contract.monthly_yearly_costs = contract.final_yearly_costs / 12.0
@api.depends('wage')
@api.depends('wage_with_holidays')
def _compute_holidays_advantages(self):
for contract in self:
contract.double_holidays = contract.wage * 0.92
contract.thirteen_month = contract.wage
contract.double_holidays = contract.wage_with_holidays * 0.92
contract.thirteen_month = contract.wage_with_holidays
@api.onchange('transport_mode_car', 'transport_mode_public', 'transport_mode_others')
def _onchange_transport_mode(self):
@@ -112,7 +112,7 @@
<record id="position_tax_intracom_2_9" model="account.fiscal.position.tax.template">
<field name="position_id" ref="fiscal_position_template_eu"/>
<field name="tax_src_id" ref="btw_9"/>
<field name="tax_dest_id" ref="btw_X0"/>
<field name="tax_dest_id" ref="btw_X0_diensten"/>
</record>
<record id="position_tax_intracom_3" model="account.fiscal.position.tax.template">
<field name="position_id" ref="fiscal_position_template_eu"/>
@@ -83,7 +83,7 @@ var MailFailure = Class.extend(Mixins.EventDispatcherMixin, ServicesMixin, {
documentModel: this._documentModel,
id: 'mail_failure',
imageSRC: this._moduleIcon,
title: this._recordName,
title: this._modelName,
};
return preview;
},
@@ -94,17 +94,17 @@ var ThreadTypingMixin = {
* Get the text to display when some partners are typing something on the
* thread:
*
* - single typing partner:
* - single typing partner:
*
* A is typing...
* A is typing...
*
* - two typing partners:
* - two typing partners:
*
* A and B are typing...
* A and B are typing...
*
* - three or more typing partners:
* - three or more typing partners:
*
* A, B and more are typing...
* A, B and more are typing...
*
* The choice of the members name for display is not random: it displays
* the user that have been typing for the longest time. Also, this function
@@ -490,7 +490,7 @@ MailManager.include({
var self = this;
var partnerID = data.partner_id;
this.call('bus_service', 'sendNotification', data.title, data.message, function ( ){
self.call('mail_service', 'openDmWindow', partnerID);
self.call('mail_service', 'openDMChatWindowFromBlankThreadWindow', partnerID);
});
},
/**
@@ -531,9 +531,6 @@ MailManager.include({
* @private
*/
_repositionThreadWindows: function () {
if (this._areAllThreadWindowsHidden()) {
return;
}
this._computeAvailableSlotsForThreadWindows();
this._repositionVisibleThreadWindows();
@@ -575,9 +572,6 @@ MailManager.include({
* the focus is on the thread window.
*/
_updateThreadWindowsFromMessage: function (message, options) {
if (this._areAllThreadWindowsHidden()) {
return;
}
_.each(this._threadWindows, function (threadWindow) {
if (_.contains(message.getThreadIDs(), threadWindow.getID())) {
threadWindow.update(options);
@@ -132,9 +132,9 @@ var ThreadWindow = AbstractThreadWindow.extend({
*
* Do not display the input in the following cases:
*
* - no thread related to this window
* - window of a mailbox (temp: let us have mailboxes in window mode)
* - window of a thread with mass mailing
* - no thread related to this window
* - window of a mailbox (temp: let us have mailboxes in window mode)
* - window of a thread with mass mailing
*
* Any other threads show the input in the window.
*
+2 -1
View File
@@ -25,7 +25,8 @@ var MrpBomReport = stock_report_generic.extend({
return this._rpc({
model: 'report.mrp.report_bom_structure',
method: 'get_html',
args: args
args: args,
context: this.given_context,
})
.then(function (result) {
self.data = result;
@@ -112,7 +112,7 @@ var PartnerAutocompleteMixin = {
var self = this;
var removeUselessFields = function (company) {
var fields = 'label,description,domain,logo,legal_name'.split(',');
var fields = 'label,description,domain,logo,legal_name,ignored'.split(',');
fields.forEach(function (field) {
delete company[field];
});
+13 -12
View File
@@ -871,24 +871,25 @@ class PaymentTransaction(models.Model):
def create(self, values):
# call custom create method if defined (i.e. ogone_create for ogone)
acquirer = self.env['payment.acquirer'].browse(values['acquirer_id'])
partner = self.env['res.partner'].browse(values['partner_id'])
if values.get('partner_id'):
partner = self.env['res.partner'].browse(values['partner_id'])
values.update({
'partner_name': partner.name,
'partner_lang': partner.lang or 'en_US',
'partner_email': partner.email,
'partner_zip': partner.zip,
'partner_address': _partner_format_address(partner.street or '', partner.street2 or ''),
'partner_city': partner.city,
'partner_country_id': partner.country_id.id or self._get_default_partner_country_id(),
'partner_phone': partner.phone,
})
values.update({
'partner_name': partner.name,
'partner_lang': partner.lang or 'en_US',
'partner_email': partner.email,
'partner_zip': partner.zip,
'partner_address': _partner_format_address(partner.street or '', partner.street2 or ''),
'partner_city': partner.city,
'partner_country_id': partner.country_id.id or self._get_default_partner_country_id(),
'partner_phone': partner.phone,
})
# compute fees
custom_method_name = '%s_compute_fees' % acquirer.provider
if hasattr(acquirer, custom_method_name):
fees = getattr(acquirer, custom_method_name)(
values.get('amount', 0.0), values.get('currency_id'), partner.country_id.id)
values.get('amount', 0.0), values.get('currency_id'), values['partner_country_id'])
values['fees'] = fees
# custom create
@@ -149,7 +149,7 @@ class AuthorizeAPI():
root = self._base_tree('createCustomerProfileRequest')
profile = etree.SubElement(root, "profile")
etree.SubElement(profile, "merchantCustomerId").text = 'ODOO-%s-%s' % (partner.id, uuid4().hex[:8])
etree.SubElement(profile, "email").text = partner.email
etree.SubElement(profile, "email").text = partner.email or ''
payment_profile = etree.SubElement(profile, "paymentProfiles")
etree.SubElement(payment_profile, "customerType").text = 'business' if partner.is_company else 'individual'
billTo = etree.SubElement(payment_profile, "billTo")
@@ -188,7 +188,7 @@ class AuthorizeForm(AuthorizeCommon):
self.assertEqual(tx.state, 'cancel', 'Authorize: erroneous validation did not put tx into error state')
@odoo.tests.tagged('post_install', '-at_install', '-standard', 'external')
@odoo.tests.tagged('post_install', '-at_install', '-standard')
class AuthorizeForm(AuthorizeCommon):
def test_30_authorize_s2s(self):
# be sure not to do stupid thing
+19 -17
View File
@@ -32,12 +32,12 @@ class BuckarooForm(BuckarooCommon):
# ----------------------------------------
form_values = {
'add_returndata': None,
'add_returndata': '',
'Brq_websitekey': self.buckaroo.brq_websitekey,
'Brq_amount': '2240.0',
'Brq_currency': 'EUR',
'Brq_invoicenumber': 'SO004',
'Brq_signature': '1b8c10074c622d965272a91a9e88b5b3777d2474', # update me
'Brq_signature': 'aa6fe072afdd9d1b463d55f43e6df4b272d3529a', # update me
'brq_test': 'True',
'Brq_return': urls.url_join(base_url, BuckarooController._return_url),
'Brq_returncancel': urls.url_join(base_url, BuckarooController._cancel_url),
@@ -50,13 +50,14 @@ class BuckarooForm(BuckarooCommon):
res = self.buckaroo.render(
'SO004', 2240.0, self.currency_euro.id,
partner_id=None,
partner_values=self.buyer_values)
values=self.buyer_values)
# check form result
tree = objectify.fromstring(res)
self.assertEqual(tree.get('action'), 'https://testcheckout.buckaroo.nl/html/', 'Buckaroo: wrong form POST url')
data_set = tree.xpath("//input[@name='data_set']")
self.assertEqual(len(data_set), 1, 'paypal: Found %d "data_set" input instead of 1' % len(data_set))
self.assertEqual(data_set[0].get('data-action-url'), 'https://testcheckout.buckaroo.nl/html/', 'Buckaroo: wrong form POST url')
for form_input in tree.input:
if form_input.get('name') in ['submit']:
if form_input.get('name') in ['submit', 'data_set']:
continue
self.assertEqual(
form_input.get('value'),
@@ -78,17 +79,18 @@ class BuckarooForm(BuckarooCommon):
})
# render the button
res = self.buckaroo_id.render(
res = self.buckaroo.render(
'should_be_erased', 2240.0, self.currency_euro,
tx_id=tx.id,
partner_id=None,
partner_values=self.buyer_values)
values=self.buyer_values)
# check form result
tree = objectify.fromstring(res)
self.assertEqual(tree.get('action'), 'https://testcheckout.buckaroo.nl/html/', 'Buckaroo: wrong form POST url')
data_set = tree.xpath("//input[@name='data_set']")
self.assertEqual(len(data_set), 1, 'paypal: Found %d "data_set" input instead of 1' % len(data_set))
self.assertEqual(data_set[0].get('data-action-url'), 'https://testcheckout.buckaroo.nl/html/', 'Buckaroo: wrong form POST url')
for form_input in tree.input:
if form_input.get('name') in ['submit']:
if form_input.get('name') in ['submit', 'data_set']:
continue
self.assertEqual(
form_input.get('value'),
@@ -116,7 +118,7 @@ class BuckarooForm(BuckarooCommon):
'BRQ_SERVICE_PAYPAL_PAYERLASTNAME': u'Tester',
'BRQ_SERVICE_PAYPAL_PAYERMIDDLENAME': u'de',
'BRQ_SERVICE_PAYPAL_PAYERSTATUS': u'verified',
'Brq_signature': u'175d82dd53a02bad393fee32cb1eafa3b6fbbd91',
'Brq_signature': u'9e75e51cbcb6fbc798b40d1787cc5b7c36557d56',
'BRQ_STATUSCODE': u'190',
'BRQ_STATUSCODE_DETAIL': u'S001',
'BRQ_STATUSMESSAGE': u'Transaction successfully processed',
@@ -134,7 +136,7 @@ class BuckarooForm(BuckarooCommon):
'amount': 2240.0,
'acquirer_id': self.buckaroo.id,
'currency_id': self.currency_euro.id,
'reference': 'SO004-1',
'reference': 'SO004',
'partner_name': 'Norbert Buyer',
'partner_country_id': self.country_france.id})
@@ -153,14 +155,14 @@ class BuckarooForm(BuckarooCommon):
'partner_name': 'Norbert Buyer',
'partner_country_id': self.country_france.id})
buckaroo_post_data['BRQ_INVOICENUMBER'] = 'SO004-2'
# now buckaroo post is ok: try to modify the SHASIGN
buckaroo_post_data['BRQ_SIGNATURE'] = '54d928810e343acf5fb0c3ee75fd747ff159ef7a'
buckaroo_post_data['Brq_signature'] = '54d928810e343acf5fb0c3ee75fd747ff159ef7a'
with self.assertRaises(ValidationError):
tx.form_feedback(buckaroo_post_data, 'buckaroo')
# simulate an error
buckaroo_post_data['BRQ_STATUSCODE'] = 2
buckaroo_post_data['BRQ_SIGNATURE'] = '4164b52adb1e6a2221d3d8a39d8c3e18a9ecb90b'
buckaroo_post_data['BRQ_STATUSCODE'] = '2'
buckaroo_post_data['Brq_signature'] = '9138e2bf09a708a4eb485ce7777e5406898b689d'
tx.form_feedback(buckaroo_post_data, 'buckaroo')
# check state
@@ -2,24 +2,26 @@
<odoo>
<data noupdate="1">
<template id="buckaroo_form">
<input type="hidden" name="data_set" t-att-data-action-url="tx_url" data-remove-me=""/>
<input type="hidden" name="Brq_websitekey" t-att-value="Brq_websitekey"/>
<input type="hidden" name="Brq_amount" t-att-value="Brq_amount"/>
<input type="hidden" name="Brq_currency" t-att-value="Brq_currency"/>
<input type="hidden" name="Brq_invoicenumber" t-att-value="Brq_invoicenumber"/>
<input type="hidden" name="Brq_signature" t-att-value="Brq_signature"/>
<input type="hidden" name="brq_test" t-att-value="brq_test"/>
<input type="hidden" name="Brq_culture" t-att-value="Brq_culture"/>
<!-- URLs -->
<input t-if="Brq_return" type="hidden" name='Brq_return'
t-att-value="Brq_return"/>
<input t-if="Brq_returncancel" type="hidden" name='Brq_returncancel'
t-att-value="Brq_returncancel"/>
<input t-if="Brq_returnerror" type="hidden" name='Brq_returnerror'
t-att-value="Brq_returnerror"/>
<input t-if="Brq_returnreject" type="hidden" name='Brq_returnreject'
t-att-value="Brq_returnreject"/>
<input type="hidden" name='add_returndata' t-att-value="add_returndata"/>
<div>
<input type="hidden" name="data_set" t-att-data-action-url="tx_url" data-remove-me=""/>
<input type="hidden" name="Brq_websitekey" t-att-value="Brq_websitekey"/>
<input type="hidden" name="Brq_amount" t-att-value="Brq_amount"/>
<input type="hidden" name="Brq_currency" t-att-value="Brq_currency"/>
<input type="hidden" name="Brq_invoicenumber" t-att-value="Brq_invoicenumber"/>
<input type="hidden" name="Brq_signature" t-att-value="Brq_signature"/>
<input type="hidden" name="brq_test" t-att-value="brq_test"/>
<input type="hidden" name="Brq_culture" t-att-value="Brq_culture"/>
<!-- URLs -->
<input t-if="Brq_return" type="hidden" name='Brq_return'
t-att-value="Brq_return"/>
<input t-if="Brq_returncancel" type="hidden" name='Brq_returncancel'
t-att-value="Brq_returncancel"/>
<input t-if="Brq_returnerror" type="hidden" name='Brq_returnerror'
t-att-value="Brq_returnerror"/>
<input t-if="Brq_returnreject" type="hidden" name='Brq_returnreject'
t-att-value="Brq_returnreject"/>
<input type="hidden" name='add_returndata' t-att-value="add_returndata"/>
</div>
</template>
</data>
</odoo>
+2 -2
View File
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
from odoo import fields
from odoo.addons.payment.models.payment_acquirer import ValidationError
from odoo.addons.payment.tests.common import PaymentAcquirerCommon
from odoo.addons.payment_paypal.controllers.main import PaypalController
@@ -182,7 +183,6 @@ class PaypalForm(PaypalCommon):
self.assertEqual(tx.state, 'pending', 'paypal: wrong state after receiving a valid pending notification')
self.assertEqual(tx.state_message, 'multi_currency', 'paypal: wrong state message after receiving a valid pending notification')
self.assertEqual(tx.acquirer_reference, '08D73520KX778924N', 'paypal: wrong txn_id after receiving a valid pending notification')
self.assertFalse(tx.date, 'paypal: validation date should not be updated whenr receiving pending notification')
# update tx
tx.write({
@@ -196,4 +196,4 @@ class PaypalForm(PaypalCommon):
# check
self.assertEqual(tx.state, 'done', 'paypal: wrong state after receiving a valid pending notification')
self.assertEqual(tx.acquirer_reference, '08D73520KX778924N', 'paypal: wrong txn_id after receiving a valid pending notification')
self.assertEqual(tx.date, '2013-11-18 11:21:19', 'paypal: wrong validation date')
self.assertEqual(fields.Datetime.to_string(tx.date), '2013-11-18 11:21:19', 'paypal: wrong validation date')
+2 -4
View File
@@ -59,9 +59,6 @@ class StripeTest(StripeCommon):
# render the button
res = self.stripe.render('SO404', 320.0, self.currency_euro.id, values=self.buyer_values).decode('utf-8')
popup_script_src = 'script src="https://checkout.stripe.com/checkout.js"'
# check form result
self.assertIn(popup_script_src, res, "Stripe: popup script not found in template render")
# Generated and received
self.assertIn(self.buyer_values.get('partner_email'), res, 'Stripe: email input not found in rendered template')
@@ -87,7 +84,7 @@ class StripeTest(StripeCommon):
u'id': u'ch_172xfnGMfVJxozLwEjSfpfxD',
u'invoice': None,
u'livemode': False,
u'metadata': {u'reference': u'SO100'},
u'metadata': {u'reference': u'SO100-1'},
u'object': u'charge',
u'paid': True,
u'receipt_email': None,
@@ -137,6 +134,7 @@ class StripeTest(StripeCommon):
tx.form_feedback(stripe_post_data, 'stripe')
self.assertEqual(tx.state, 'done', 'Stripe: validation did not put tx into done state')
self.assertEqual(tx.acquirer_reference, stripe_post_data.get('id'), 'Stripe: validation did not update tx id')
stripe_post_data['metadata']['reference'] = u'SO100-2'
# reset tx
tx = self.env['payment.transaction'].create({
'amount': 4700,
+6 -9
View File
@@ -839,21 +839,18 @@ class PosOrder(models.Model):
lots_necessary = True
if picking_type:
lots_necessary = picking_type and picking_type.use_existing_lots
qty = 0
qty_done = 0
pack_lots = []
pos_pack_lots = PosPackOperationLot.search([('order_id', '=', order.id), ('product_id', '=', move.product_id.id)])
pack_lot_names = [pos_pack.lot_name for pos_pack in pos_pack_lots]
if pack_lot_names and lots_necessary:
for lot_name in list(set(pack_lot_names)):
stock_production_lot = StockProductionLot.search([('name', '=', lot_name), ('product_id', '=', move.product_id.id)])
if pos_pack_lots and lots_necessary:
for pos_pack_lot in pos_pack_lots:
stock_production_lot = StockProductionLot.search([('name', '=', pos_pack_lot.lot_name), ('product_id', '=', move.product_id.id)])
if stock_production_lot:
# 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':
# if a lot nr is set through the frontend it will refer to the full quantity
qty = move.product_uom_qty
else: # serial numbers
qty = 1.0
qty = pos_pack_lot.pos_order_line_id.qty
qty_done += qty
pack_lots.append({'lot_id': stock_production_lot.id, 'qty': qty})
else:
@@ -1943,16 +1943,11 @@ var PaymentScreenWidget = ScreenWidget.extend({
$('body').keypress(this.keyboard_handler);
// that one comes from the pos, but we prefer to cover all the basis
$('body').keydown(this.keyboard_keydown_handler);
// legacy vanilla JS listeners
window.document.body.addEventListener('keypress',this.keyboard_handler);
window.document.body.addEventListener('keydown',this.keyboard_keydown_handler);
this._super();
},
hide: function(){
$('body').off('keypress', this.keyboard_handler);
$('body').off('keydown', this.keyboard_keydown_handler);
window.document.body.removeEventListener('keypress',this.keyboard_handler);
window.document.body.removeEventListener('keydown',this.keyboard_keydown_handler);
this._super();
},
// sets up listeners to watch for order changes
+31 -4
View File
@@ -156,10 +156,7 @@ class ProductTemplate(models.Model):
@api.multi
def _compute_currency_id(self):
try:
main_company = self.sudo().env.ref('base.main_company')
except ValueError:
main_company = self.env['res.company'].sudo().search([], limit=1, order="id")
main_company = self.env['res.company']._get_main_company()
for template in self:
template.currency_id = template.company_id.sudo().currency_id.id or main_company.currency_id.id
@@ -929,6 +926,36 @@ class ProductTemplate(models.Model):
return result
combination = combination[:-1]
@api.multi
def _get_current_company(self, **kwargs):
"""Get the most appropriate company for this product.
If the company is set on the product, directly return it. Otherwise,
fallback to a contextual company.
:param kwargs: kwargs forwarded to the fallback method.
:return: the most appropriate company for this product
:rtype: recordset of one `res.company`
"""
self.ensure_one()
return self.company_id or self._get_current_company_fallback(**kwargs)
@api.multi
def _get_current_company_fallback(self, **kwargs):
"""Fallback to get the most appropriate company for this product.
This should only be called from `_get_current_company` but is defined
separately to allow override.
The final fallback will be the current user's company.
:return: the fallback company for this product
:rtype: recordset of one `res.company`
"""
self.ensure_one()
return self.env.user.company_id
@api.model
def get_empty_list_help(self, help):
self = self.with_context(
@@ -12,7 +12,7 @@
<tr>
<td>
<strong>Price:</strong>
<strong t-field="product.list_price" t-options="{'widget': 'monetary', 'display_currency': product.company_id.currency_id}"/>
<strong t-field="product.lst_price" t-options="{'widget': 'monetary', 'display_currency': product.company_id.currency_id}"/>
</td>
</tr>
<t t-if="product.barcode">
+5
View File
@@ -1580,6 +1580,11 @@ msgstr ""
msgid "Invoicing address:"
msgstr ""
#. module: sale
#: model_terms:ir.ui.view,arch_db:sale.sale_order_portal_content
msgid "Invoicing and Shipping Address"
msgstr ""
#. module: sale
#: model_terms:ir.ui.view,arch_db:sale.report_saleorder_document
msgid "Invoicing and shipping address:"
+9 -1
View File
@@ -261,7 +261,7 @@ class ProductTemplate(models.Model):
if pricelist and pricelist.currency_id != product_template.currency_id:
list_price = product_template.currency_id._convert(
list_price, pricelist.currency_id, product_template.company_id,
list_price, pricelist.currency_id, product_template._get_current_company(pricelist=pricelist),
fields.Date.today()
)
@@ -296,3 +296,11 @@ class ProductTemplate(models.Model):
return False
combination = self._get_first_possible_combination(parent_combination)
return True if combination else self._is_combination_possible(combination, parent_combination)
@api.multi
def _get_current_company_fallback(self, **kwargs):
"""Override: if a pricelist is given, fallback to the company of the
pricelist if it is set, otherwise use the one from parent method."""
res = super(ProductTemplate, self)._get_current_company_fallback(**kwargs)
pricelist = kwargs.get('pricelist')
return pricelist and pricelist.company_id or res
@@ -281,6 +281,7 @@ var ProductConfiguratorMixin = {
/**
* Will return a deferred:
*
* - If the product already exists, immediately resolves it with the product_id
* - If the product does not exist yet ("dynamic" variant creation), this method will
* create the product first and then resolve the deferred with the created product's id
@@ -28,6 +28,7 @@ var ProductConfiguratorFormRenderer = FormRenderer.extend(ProductConfiguratorMix
* Renders the product configurator within the form
*
* Will also:
*
* - add events handling for variant changes
* - trigger variant change to compute the price and other
* variant specific changes
@@ -185,6 +185,11 @@ class TestSaleProductAttributeValueConfig(TestSaleProductAttributeValueSetup):
do_test(self)
def test_02_get_combination_info(self):
# If using multi-company, company_id will be False, and this code should
# still work.
# The case with a company_id will be implicitly tested on website_sale.
self.computer.company_id = False
computer_ssd_256 = self._get_product_template_attribute_value(self.ssd_256)
computer_ram_8 = self._get_product_template_attribute_value(self.ram_8)
computer_hdd_1 = self._get_product_template_attribute_value(self.hdd_1)
+8 -5
View File
@@ -381,13 +381,16 @@
</div>
<div class="row">
<div class="col-lg-6">
<strong class="d-block mb-1">Invoicing Address</strong>
<strong t-if="sale_order.partner_shipping_id == sale_order.partner_invoice_id" class="d-block mb-1">Invoicing and Shipping Address</strong>
<strong t-if="sale_order.partner_shipping_id != sale_order.partner_invoice_id" class="d-block mb-1">Invoicing Address</strong>
<address t-field="sale_order.partner_invoice_id" t-options='{"widget": "contact", "fields": ["address", "name"], "no_marker": True}'/>
</div>
<div id="shipping_address" class="col-lg-6" groups="sale.group_delivery_invoice_address">
<strong class="d-block mb-1">Shipping Address</strong>
<address t-field="sale_order.partner_shipping_id" t-options='{"widget": "contact", "fields": ["address", "name"], "no_marker": True}'/>
</div>
<t t-if="sale_order.partner_shipping_id != sale_order.partner_invoice_id">
<div id="shipping_address" class="col-lg-6">
<strong class="d-block mb-1">Shipping Address</strong>
<address t-field="sale_order.partner_shipping_id" t-options='{"widget": "contact", "fields": ["address", "name"], "no_marker": True}'/>
</div>
</t>
</div>
<t t-set="invoices" t-value="[i for i in sale_order.invoice_ids if i.state not in ['draft', 'cancel']]"/>
@@ -73,7 +73,13 @@ var ProjectPlan = AbstractAction.extend({
* @param {string|html} dom
*/
_refreshPlan: function (dom) {
this.$('.o_content').html(dom);
// TODO: Not forward port this fix on master
var $dom = $(dom);
$dom.find('div.o_timesheet_plan_sale_timesheet_dashboard > table.table, ' +
'div.o_timesheet_plan_sale_timesheet_people_time > table.table, ' +
'div.o_project_plan_project_timesheet_forecast > table.table')
.wrap('<div class="table-responsive"></div>');
this.$('.o_content').html($dom);
},
/**
@@ -48,7 +48,7 @@ QUnit.module('Timesheet Plan', {
mockRPC: function (route) {
assert.step(route);
if (route === '/timesheet/plan') {
return $.when({html_content: 'Banach-Tarski'});
return $.when({html_content: '<p>Banach-Tarski</p>'});
}
return this._super.apply(this, arguments);
},
@@ -81,7 +81,7 @@ QUnit.module('Timesheet Plan', {
action: {name: 'Fibonacci'},
mockRPC: function (route) {
if (route === '/timesheet/plan') {
return $.when({html_content: 'Banach-Tarski'});
return $.when({html_content: '<p>Banach-Tarski</p>'});
}
return this._super.apply(this, arguments);
},
+4
View File
@@ -533,6 +533,10 @@ class Picking(models.Model):
def action_assign_owner(self):
self.move_line_ids.write({'owner_id': self.owner_id.id})
def action_assign_partner(self):
for picking in self:
picking.move_lines.write({'partner_id': picking.partner_id.id})
@api.multi
def do_print_picking(self):
self.write({'printed': True})
@@ -248,6 +248,9 @@
<group>
<group>
<field name="partner_id"/>
<button name="action_assign_partner" string="Assign Partner" type="object" help="Assign the partner to the Operations. Use only if the partner is manually changed!"
attrs="{'invisible': ['|', '|', ('move_line_exist', '=', False), ('state', 'not in', ('draft', 'assigned', 'confirmed')), ('picking_type_code', '=', 'internal')]}"
class="oe_link oe_edit_only"/>
<field name="picking_type_id" attrs="{'readonly': [('state', '!=', 'draft')]}"/>
<field name="location_id" groups="stock.group_stock_multi_locations" attrs="{'invisible': [('picking_type_code', '=', 'incoming')]}"/>
<field name="location_dest_id" groups="stock.group_stock_multi_locations" attrs="{'invisible': [('picking_type_code', '=', 'outgoing')]}"/>
+14
View File
@@ -1572,6 +1572,13 @@ msgstr ""
msgid "Group by: %s"
msgstr ""
#. module: web
#. openerp-web
#: code:addons/web/static/src/js/views/graph/graph_renderer.js:214
#, python-format
msgid "Grouped"
msgstr ""
#. module: web
#: model:ir.model,name:web.model_ir_http
msgid "HTTP Routing"
@@ -2963,6 +2970,13 @@ msgstr ""
msgid "Special:"
msgstr ""
#. module: web
#. openerp-web
#: code:addons/web/static/src/js/views/graph/graph_renderer.js:215
#, python-format
msgid "Stacked"
msgstr ""
#. module: web
#. openerp-web
#: code:addons/web/static/src/js/core/misc.js:14
@@ -363,7 +363,7 @@ function formatPercentage(value, field, options) {
if (options.humanReadable && options.humanReadable(value * 100)) {
return result + "%";
}
return parseFloat(result) + "%";
return (parseFloat(result) + "%").replace('.', _t.database.parameters.decimal_point);
}
/**
* Returns a string representing the value of the selection.
@@ -215,6 +215,10 @@ return AbstractRenderer.extend({
margin: {left: 80, bottom: 100, top: 80, right: 0},
delay: 100,
transition: 10,
controlLabels: {
'grouped': _t('Grouped'),
'stacked': _t('Stacked'),
},
showLegend: _.size(data) <= MAX_LEGEND_LENGTH,
showXAxis: true,
showYAxis: true,
@@ -440,7 +440,8 @@ var KanbanRenderer = BasicRenderer.extend({
_setState: function (state) {
this.state = state;
var groupByField = state.groupedBy[0];
// split groupedBy field with ':' as it can be date/datetime field
var groupByField = state.groupedBy.length && state.groupedBy[0].split(":")[0];
var groupByFieldAttrs = state.fields[groupByField];
var groupByFieldInfo = state.fieldsInfo.kanban[groupByField];
// Deactivate the drag'n'drop if the groupedBy field:
@@ -31,6 +31,7 @@ var PieChart = Widget.extend({
Domain.prototype.stringToArray(modifiers.domain || '[]'));
var arch = qweb.render('web.PieChart', {
modifiers: modifiers,
title: node.attrs.title || modifiers.title || modifiers.measure,
});
this.subViewParams = {
+1 -1
View File
@@ -1,7 +1,7 @@
<template>
<t t-name="web.PieChart">
<graph type="pie" t-att-title="modifiers.title || modifiers.measure">
<graph type="pie" t-att-title="title">
<field t-att-name="modifiers.groupby.split(':')[0]" t-att-interval="modifiers.groupby.split(':')[1]"/>
<field t-if="modifiers.measure" t-att-name="modifiers.measure" type="measure"/>
</graph>
@@ -135,7 +135,9 @@ QUnit.test('format binary', function (assert) {
});
QUnit.test('format percentage', function (assert) {
assert.expect(9);
assert.expect(11);
var originalParameters = _.clone(core._t.database.parameters);
assert.strictEqual(fieldUtils.format.percentage(0), '0%');
assert.strictEqual(fieldUtils.format.percentage(0.5), '50%');
@@ -151,6 +153,16 @@ QUnit.test('format percentage', function (assert) {
assert.strictEqual(fieldUtils.format.percentage(50, null,
{humanReadable: function (val) {return true;}}), '5k%'
);
_.extend(core._t.database.parameters, {
grouping: [3, 0],
decimal_point: ',',
thousands_sep: '.'
});
assert.strictEqual(fieldUtils.format.percentage(0.125), '12,5%');
assert.strictEqual(fieldUtils.format.percentage(0.666666), '66,67%');
core._t.database.parameters = originalParameters;
});
QUnit.test('parse float', function(assert) {
@@ -262,7 +274,9 @@ QUnit.test('parse monetary', function(assert) {
});
QUnit.test('parse percentage', function(assert) {
assert.expect(7);
assert.expect(9);
var originalParameters = _.clone(core._t.database.parameters);
assert.strictEqual(fieldUtils.parse.percentage(""), 0);
assert.strictEqual(fieldUtils.parse.percentage("0"), 0);
@@ -271,6 +285,18 @@ QUnit.test('parse percentage', function(assert) {
assert.strictEqual(fieldUtils.parse.percentage("1"), 1);
assert.strictEqual(fieldUtils.parse.percentage("2%"), 0.02);
assert.strictEqual(fieldUtils.parse.percentage("100%"), 1);
_.extend(core._t.database.parameters, {
grouping: [3, 0],
decimal_point: ',',
thousands_sep: '.'
});
assert.strictEqual(fieldUtils.parse.percentage("0,04"), 0.04);
assert.strictEqual(fieldUtils.parse.percentage("6,02%"), 0.0602);
core._t.database.parameters = originalParameters;
})
});
@@ -2580,6 +2580,45 @@ QUnit.module('Views', {
kanban.destroy();
});
QUnit.test('prevent drag and drop if grouped by date/datetime field', function (assert) {
assert.expect(5);
this.data.partner.records[0].date = '2017-01-08';
this.data.partner.records[1].date = '2017-01-09';
this.data.partner.records[2].date = '2017-02-08';
this.data.partner.records[3].date = '2017-02-10';
var kanban = createView({
View: KanbanView,
model: 'partner',
data: this.data,
arch: '<kanban class="o_kanban_test">' +
'<field name="bar"/>' +
'<templates><t t-name="kanban-box">' +
'<div><field name="foo"/></div>' +
'</t></templates></kanban>',
groupBy: ['date:month'],
});
assert.strictEqual(kanban.$('.o_kanban_group').length, 2, "should have 2 columns");
assert.strictEqual(kanban.$('.o_kanban_group:nth-child(1) .o_kanban_record').length, 2,
"1st column should contain 2 records of January month");
assert.strictEqual(kanban.$('.o_kanban_group:nth-child(2) .o_kanban_record').length , 2,
"2nd column should contain 2 records of February month");
// drag&drop a record in another column
var $record = kanban.$('.o_kanban_group:nth-child(1) .o_kanban_record:first');
var $group = kanban.$('.o_kanban_group:nth-child(2)');
testUtils.dragAndDrop($record, $group);
// should not drag&drop record
assert.strictEqual(kanban.$('.o_kanban_group:nth-child(1) .o_kanban_record').length , 2,
"Should remain same records in first column(2 records)");
assert.strictEqual(kanban.$('.o_kanban_group:nth-child(2) .o_kanban_record').length , 2,
"Should remain same records in 2nd column(2 record)");
kanban.destroy();
});
QUnit.test('kanban view with default_group_by', function (assert) {
assert.expect(7);
this.data.partner.records.product_id = 1;
+1 -1
View File
@@ -282,7 +282,7 @@
<h4 class="mt0 mb0 text-uppercase" t-field="company.report_header"/>
</div>
<div class="col-1">
<ul t-if="report_type != 'html'" class="list-inline pagenumber float-right text-center">
<ul t-if="report_type == 'pdf'" class="list-inline pagenumber float-right text-center">
<li class="list-inline-item"><strong><span class="page"/></strong></li>
</ul>
</div>
@@ -773,6 +773,7 @@ var IconWidget = MediaWidget.extend({
this.$('div.font-icons-icons').html(
QWeb.render('web_editor.dialog.font-icons.icons', {iconsParser: iconsParser})
);
return $.when();
},
//--------------------------------------------------------------------------
@@ -1299,6 +1300,7 @@ var MediaDialog = Dialog.extend({
/**
* @private
*/
ev.preventDefault();
_onSaveRequest: function (ev) {
ev.stopPropagation();
this.save();
+40
View File
@@ -532,6 +532,26 @@ msgstr ""
msgid "<i class=\"fa fa-trash-o\"/>Remove Slide"
msgstr ""
#. module: website
#: model_terms:ir.ui.view,arch_db:website.list_website_pages
msgid "<i title=\"Is the page SEO optimized?\" class=\"fa fa-search\"/>"
msgstr ""
#. module: website
#: model_terms:ir.ui.view,arch_db:website.list_website_pages
msgid "<i title=\"Is the page included in the main menu?\" class=\"fa fa-thumb-tack\"/>"
msgstr ""
#. module: website
#: model_terms:ir.ui.view,arch_db:website.list_website_pages
msgid "<i title=\"Is the page indexed by search engines?\" class=\"fa fa-globe\"/>"
msgstr ""
#. module: website
#: model_terms:ir.ui.view,arch_db:website.list_website_pages
msgid "<i title=\"Is the page published?\" class=\"fa fa-eye\"/>"
msgstr ""
#. module: website
#: model_terms:ir.ui.view,arch_db:website.s_comparisons
msgid "<i>Instant setup, satisfied or reimbursed.</i>"
@@ -569,6 +589,16 @@ msgid "<span class=\"carousel-control-prev-icon\"/>\n"
" <span class=\"sr-only\">Previous</span>"
msgstr ""
#. module: website
#: model_terms:ir.ui.view,arch_db:website.res_config_settings_view_form
msgid "<span class=\"fa fa-lg fa-globe\" title=\"Values set here are website-specific.\" groups=\"website.group_multi_website\"/>"
msgstr ""
#. module: website
#: model_terms:ir.ui.view,arch_db:website.company_description
msgid "<span class=\"fa fa-map-marker fa-fw mt16\" role=\"img\" aria-label=\"Address\" title=\"Address\"/>"
msgstr ""
#. module: website
#: model_terms:ir.ui.view,arch_db:website.user_navbar
msgid "<span class=\"fa fa-pencil\"/>Edit"
@@ -579,6 +609,11 @@ msgstr ""
msgid "<span class=\"fa fa-plus\"/>New"
msgstr ""
#. module: website
#: model_terms:ir.ui.view,arch_db:website.list_website_pages
msgid "<span class=\"fa fa-sort fa-lg\" role=\"img\" aria-label=\"Sort\" title=\"Sort\"/>"
msgstr ""
#. module: website
#: model_terms:ir.ui.view,arch_db:website.s_facebook_page
msgid "<span class=\"o_add_facebook_page\">\n"
@@ -617,6 +652,11 @@ msgstr ""
msgid "<span class=\"sr-only\">Toggle Dropdown</span>"
msgstr ""
#. module: website
#: model_terms:ir.ui.view,arch_db:website.user_navbar
msgid "<span title=\"Mobile preview\" role=\"img\" aria-label=\"Mobile preview\" class=\"fa fa-mobile\"/>"
msgstr ""
#. module: website
#: model_terms:ir.ui.view,arch_db:website.user_navbar
msgid "<span/>\n"
+15
View File
@@ -143,8 +143,12 @@ class View(models.Model):
return self.filtered(lambda view: not view.website_id)
for view in self:
# specific view: add it if it's for the current website and ignore
# it if it's for another website
if view.website_id and view.website_id.id == current_website_id:
most_specific_views |= view
# generic view: add it only if, for the current website, there is no
# specific view for this view (based on the same `key` attribute)
elif not view.website_id and not any(view.key == view2.key and view2.website_id and view2.website_id.id == current_website_id for view2 in self):
most_specific_views |= view
@@ -201,6 +205,17 @@ class View(models.Model):
@api.model
@tools.ormcache_context('self._uid', 'xml_id', keys=('website_id',))
def get_view_id(self, xml_id):
"""If a website_id is in the context and the given xml_id is not an int
then try to get the id of the specific view for that website, but
fallback to the id of the generic view if there is no specific.
If no website_id is in the context, it might randomly return the generic
or the specific view, so it's probably not recommanded to use this
method. `viewref` is probably more suitable.
Archived views are ignored (unless the active_test context is set, but
then the ormcache_context will not work as expected).
"""
if 'website_id' in self._context and not isinstance(xml_id, int):
current_website = self.env['website'].browse(self._context.get('website_id'))
domain = ['&', ('key', '=', xml_id)] + current_website.website_domain()
+19 -10
View File
@@ -503,12 +503,18 @@ class Website(models.Model):
def viewref(self, view_id, raise_if_not_found=True):
''' Given an xml_id or a view_id, return the corresponding view record.
In case of website context, return the most specific one.
If no website_id is in the context, it will return the generic view,
instead of a random one like `get_view_id`.
Look also for archived views, no matter the context.
:param view_id: either a string xml_id or an integer view_id
:param raise_if_not_found: should the method raise an error if no view found
:return: The view record or empty recordset
'''
View = self.env['ir.ui.view']
view = None
view = View
if isinstance(view_id, str):
if 'website_id' in self._context:
domain = [('key', '=', view_id)] + self.env['website'].website_domain(self._context.get('website_id'))
@@ -520,20 +526,20 @@ class Website(models.Model):
if views:
view = views.filter_duplicate()
else:
view = self.env.ref(view_id)
# we handle the raise below
view = self.env.ref(view_id, raise_if_not_found=False)
# self.env.ref might return something else than an ir.ui.view (eg: a theme.ir.ui.view)
if view._name != 'ir.ui.view':
view = None
if not view or view._name != 'ir.ui.view':
# make sure we always return a recordset
view = View
elif isinstance(view_id, int):
view = View.browse(view_id)
else:
raise ValueError('Expecting a string or an integer, not a %s.' % (type(view_id)))
if view:
return view
if raise_if_not_found:
if not view and raise_if_not_found:
raise ValueError('No record found for unique ID %s. It may have been deleted.' % (view_id))
return None
return view
@api.model
def get_template(self, template):
@@ -1151,9 +1157,12 @@ class Menu(models.Model):
@api.multi
def unlink(self):
default_menu = self.env.ref('website.main_menu', raise_if_not_found=False)
menus_to_remove = self
for menu in self.filtered(lambda m: default_menu and m.parent_id.id == default_menu.id):
self.env['website.menu'].search([('url', '=', menu.url), ('id', '!=', menu.id)]).unlink()
return super(Menu, self).unlink()
menus_to_remove |= self.env['website.menu'].search([('url', '=', menu.url),
('website_id', '!=', False),
('id', '!=', menu.id)])
return super(Menu, menus_to_remove).unlink()
@api.one
def _compute_visible(self):
@@ -31,3 +31,7 @@ $font-family-sans-serif: nth($o-theme-fonts, $o-theme-font-number);
$headings-font-family: nth($o-theme-fonts, $o-theme-headings-font-number);
$headings-color: color('h1');
// Jumbotron
$jumbotron-bg: transparent !default;
+3 -3
View File
@@ -847,7 +847,7 @@
<div class="container accordion mb32 mt32" id="debug_infos">
<div class="card" t-if="exception">
<h4 class="card-header">
<a data-toggle="collapse" data-parent="#debug_infos" href="#error_main">Error</a>
<a data-toggle="collapse" href="#error_main">Error</a>
</h4>
<div id="error_main" class="collapse in">
<div class="card-body">
@@ -861,7 +861,7 @@
</div>
<div class="card" t-if="qweb_exception">
<h4 class="card-header">
<a data-toggle="collapse" data-parent="#adebug_infos" href="#error_qweb">QWeb</a>
<a data-toggle="collapse" href="#error_qweb">QWeb</a>
</h4>
<div id="error_qweb" class="collapse">
<div class="card-body">
@@ -881,7 +881,7 @@
</div>
<div class="card" t-if="traceback">
<h4 class="card-header">
<a data-toggle="collapse" data-parent="#adebug_infos" href="#error_traceback">Traceback</a>
<a data-toggle="collapse" href="#error_traceback">Traceback</a>
</h4>
<div id="error_traceback" class="collapse">
<div class="card-body">
@@ -47,7 +47,7 @@
<field name="name"/>
<field name="active" invisible="1"/>
<field name="blog_id"/>
<field name="website_id"/>
<field name="website_id" groups="website.group_multi_website"/>
<field name="visits"/>
<field name="create_uid" invisible="1"/>
<field name="write_uid"/>
@@ -40,6 +40,7 @@
class="oe_stat_button"
icon="fa-globe">
<field name="website_published" widget="website_button"/>
<field name="is_published" invisible="1"/>
</button>
</button>
<label for="is_online" position="after">
@@ -10,6 +10,7 @@
<button class="oe_stat_button" name="website_publish_button"
type="object" icon="fa-globe" groups="hr.group_hr_user">
<field name="website_published" widget="website_button"/>
<field name="is_published" invisible="1"/>
</button>
</button>
<xpath expr="//page[@name='public']/group" position="inside">
@@ -20,6 +20,7 @@
<button class="oe_stat_button" name="website_publish_button"
type="object" icon="fa-globe">
<field name="website_published" widget="website_button"/>
<field name="is_published" invisible="1"/>
</button>
</div>
</field>
+4 -3
View File
@@ -150,9 +150,10 @@ class WebsiteSale(ProductConfiguratorController):
return pricelist_context, pricelist
def _get_compute_currency(self, pricelist, product=None):
from_currency = (product or request.env.user.company_id).currency_id
company = product and product._get_current_company(pricelist=pricelist, website=request.website) or pricelist.company_id or request.website.company_id
from_currency = (product or request.env['res.company']._get_main_company()).currency_id
to_currency = pricelist.currency_id
return lambda price: from_currency._convert(price, to_currency, (product or request.env.user).company_id, fields.Date.today())
return lambda price: from_currency._convert(price, to_currency, company, fields.Date.today())
def _get_search_order(self, post):
# OrderBy will be parsed in orm and so no direct sql injection
@@ -312,7 +313,7 @@ class WebsiteSale(ProductConfiguratorController):
pricelist = request.website.get_current_pricelist()
def compute_currency(price):
return product.currency_id._convert(price, pricelist.currency_id, product.company_id, fields.Date.today())
return product.currency_id._convert(price, pricelist.currency_id, product._get_current_company(pricelist=pricelist, website=request.website), fields.Date.today())
if not product_context.get('pricelist'):
product_context['pricelist'] = pricelist.id
+8
View File
@@ -287,6 +287,14 @@ class ProductTemplate(models.Model):
"""
return self._create_product_variant(self._get_first_possible_combination(), log_warning)
@api.multi
def _get_current_company_fallback(self, **kwargs):
"""Override: if a website is set on the product or given, fallback to
the company of the website. Otherwise use the one from parent method."""
res = super(ProductTemplate, self)._get_current_company_fallback(**kwargs)
website = self.website_id or kwargs.get('website')
return website and website.company_id or res
def _default_website_sequence(self):
self._cr.execute("SELECT MIN(website_sequence) FROM %s" % self._table)
min_sequence = self._cr.fetchone()[0]
@@ -690,7 +690,7 @@ a.no-decoration {
.o_website_sale_animate {
opacity: 0.7;
position: absolute;
position: absolute !important;
height: 150px;
width: 150px;
z-index: 1020;
@@ -88,6 +88,7 @@
<button class="oe_stat_button" name="website_publish_button"
type="object" icon="fa-globe" attrs="{'invisible': [('sale_ok','=',False)]}">
<field name="website_published" widget="website_button"/>
<field name="is_published" invisible="1"/>
</button>
</xpath>
<xpath expr="//page[@name='sales']" position="after">
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import test_website_sale_comparison
@@ -0,0 +1,57 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import odoo.tests
@odoo.tests.tagged('-at_install', 'post_install')
class TestWebsiteSaleComparison(odoo.tests.TransactionCase):
def test_01_website_sale_comparison_remove(self):
""" This tour makes sure the product page still works after the module
`website_sale_comparison` has been removed.
Technically it tests the removal of copied views by the base method
`_remove_copied_views`. The problematic view that has to be removed is
`product_add_to_compare` because it has a reference to `add_to_compare`.
"""
Website0 = self.env['website'].with_context(website_id=None)
Website1 = self.env['website'].with_context(website_id=1)
# Create a generic inherited view, with a key not starting with
# `website_sale_comparison` otherwise the unlink will work just based on
# the key, but we want to test also for `MODULE_UNINSTALL_FLAG`.
product_add_to_compare = Website0.viewref('website_sale_comparison.product_add_to_compare')
test_view_key = 'my_test.my_key'
self.env['ir.ui.view'].with_context(website_id=None).create({
'name': 'test inherited view',
'key': test_view_key,
'inherit_id': product_add_to_compare.id,
'arch': '<div/>',
})
# Retrieve the generic view
product = Website0.viewref('website_sale.product')
# Trigger COW to create specific views of the whole tree
product.with_context(website_id=1).write({'name': 'Trigger COW'})
# Verify initial state: the specific views exist
self.assertEquals(Website1.viewref('website_sale.product').website_id.id, 1)
self.assertEquals(Website1.viewref('website_sale_comparison.product_add_to_compare').website_id.id, 1)
self.assertEquals(Website1.viewref(test_view_key).website_id.id, 1)
# Remove the module (use `module_uninstall` because it is enough to test
# what we want here, no need/can't use `button_immediate_uninstall`
# because it would commit the test transaction)
website_sale_comparison = self.env['ir.module.module'].search([('name', '=', 'website_sale_comparison')])
website_sale_comparison.module_uninstall()
# Check that the generic view is correctly removed
self.assertFalse(Website0.viewref('website_sale_comparison.product_add_to_compare', raise_if_not_found=False))
# Check that the specific view is correctly removed
self.assertFalse(Website1.viewref('website_sale_comparison.product_add_to_compare', raise_if_not_found=False))
# Check that the generic inherited view is correctly removed
self.assertFalse(Website0.viewref(test_view_key, raise_if_not_found=False))
# Check that the specific inherited view is correctly removed
self.assertFalse(Website1.viewref(test_view_key, raise_if_not_found=False))
@@ -9,4 +9,3 @@ class DeliveryCarrier(models.Model):
_inherit = ['delivery.carrier', 'website.published.multi.mixin']
website_description = fields.Text(related='product_id.description_sale', string='Description for Online Quotations', readonly=False)
website_published = fields.Boolean(default=False)
@@ -75,6 +75,7 @@ class SaleOrder(models.Model):
def _get_delivery_methods(self):
address = self.partner_shipping_id
# searching on website_published will also search for available website (_search method on computed field)
return self.env['delivery.carrier'].sudo().search([('website_published', '=', True)]).available_carriers(address)
@api.multi
@@ -6,6 +6,9 @@
<field name="model">delivery.carrier</field>
<field name="inherit_id" ref="delivery.view_delivery_carrier_form"/>
<field name="arch" type="xml">
<field name="company_id" position='after'>
<field name="website_id" groups="website.group_multi_website" options="{'no_open': True, 'no_create_edit': True}"/>
</field>
<xpath expr="//page[@name='destination']" position='after'>
<page string="Description">
<field name="website_description" placeholder="Description displayed on the eCommerce and on online quotations."/>
@@ -26,6 +29,7 @@
<field name="arch" type="xml">
<field name="delivery_type" position="after">
<field name="website_published"/>
<field name="website_id" groups="website.group_multi_website"/>
</field>
</field>
</record>
@@ -52,6 +52,7 @@
<button class="oe_stat_button" name="website_publish_button"
type="object" icon="fa-globe">
<field name="website_published" widget="website_button"/>
<field name="is_published" invisible="1"/>
</button>
<button name="toggle_active" type="object" confirm="(Un)archiving a channel automatically (un)archives its slides. Do you want to proceed?" class="oe_stat_button" icon="fa-archive">
<field name="active" widget="boolean_button" options='{"terminology": "archive"}'/>
@@ -446,7 +446,7 @@
tabindex="0"
data-toggle="popover">
<i class="fa fa-thumbs-up fa-1x" role="img" aria-label="Likes" title="Likes"/>
<t t-esc="slide.likes"/>
<span class="o_wslides_like_dislike_count" t-esc="slide.likes"/>
</span>
<span
class="oe_slide_js_unlike"
@@ -457,7 +457,7 @@
tabindex="0"
data-toggle="popover">
<i class="fa fa-thumbs-down fa-1x" role="img" aria-label="Dislikes" title="Dislikes"/>
<t t-esc="slide.dislikes"/>
<span class="o_wslides_like_dislike_count" t-esc="slide.dislikes"/>
</span>
</div>
</div>
@@ -492,7 +492,7 @@
</li>
</ul>
<div class="tab-content" style="padding: 20px 5px 5px 5px; word-wrap: break-word;">
<div role="tabpanel" t-att-class="not comments and 'tab-pane fade active in' or 'tab-pane fade'" id="about">
<div role="tabpanel" t-att-class="not comments and 'tab-pane fade in show active' or 'tab-pane fade'" id="about">
<div t-field="slide.description"/>
</div>
<div role="tabpanel" class="tab-pane fade" t-if="slide.website_published" id="share">
@@ -513,7 +513,7 @@
The social sharing module will be unlocked when a moderator will allow your publication.
</h4>
</div>
<div role="tabpanel" t-att-class="comments and 'tab-pane fade active in' or 'tab-pane fade'" id="discuss">
<div role="tabpanel" t-att-class="comments and 'tab-pane fade in show active' or 'tab-pane fade'" id="discuss">
<t t-call="portal.message_thread">
<t t-set="object" t-value="slide"/>
</t>
@@ -6,6 +6,7 @@ import os
from collections import OrderedDict
from odoo import api, fields, models
from odoo.addons.base.models.ir_model import MODULE_UNINSTALL_FLAG
from odoo.exceptions import MissingError
from odoo.http import request
@@ -206,7 +207,7 @@ class IrModuleModule(models.Model):
for model_name in self._theme_model_names:
template = self._get_module_data(model_name)
models = template.with_context(active_test=False).mapped('copy_ids').filtered(lambda m: m.website_id == website)
models = template.with_context(**{'active_test': False, MODULE_UNINSTALL_FLAG: True}).mapped('copy_ids').filtered(lambda m: m.website_id == website)
models.unlink()
self._theme_cleanup(model_name, website)
@@ -235,8 +236,9 @@ class IrModuleModule(models.Model):
if model_name in ('website.page', 'website.menu'):
return model
orphans = model.with_context(active_test=False).search([
# use active_test to also unlink archived models
# and use MODULE_UNINSTALL_FLAG to also unlink inherited models
orphans = model.with_context(**{'active_test': False, MODULE_UNINSTALL_FLAG: True}).search([
('key', '=like', self.name + '.%'),
('website_id', '=', website.id),
('theme_template_id', '=', False),
@@ -334,9 +336,6 @@ class IrModuleModule(models.Model):
"""
Remove any existing theme on the current website and install the theme ``self`` instead.
The removal of the previous theme will however keep the existing data
that will also be used in the new theme.
The actual loading of the theme on the current website will be done
automatically on ``write`` thanks to the upgrade and/or install.
+9
View File
@@ -0,0 +1,9 @@
China, 2019-01-08
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,
Alex Zhang zhilight@gmail.com https://github.com/Alexcn
+9
View File
@@ -0,0 +1,9 @@
Ukraine, 2019-01-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,
Vyshnevska Iryna i.vyshnevska@mobilunity.com https://github.com/i-vyshnevska/
+1
View File
@@ -281,6 +281,7 @@ Inherited XML
~~~~~~~~~~~~~
The naming pattern of inherited view is
#. Extension mode: Use the same xml id than the original view you are extending, and suffix it by :samp:`{_inherit}` . For instance, the view :samp:`project.project_view_form` can be extended by :samp:`project_forecast.project_view_form_inherit`.
#. Primary mode: Keep the original xml id.
+7 -4
View File
@@ -121,10 +121,13 @@ PostgreSQL server :
# apt-get install postgresql -y
In order to print PDF reports, you must install wkhtmltopdf_ yourself:
the version of wkhtmltopdf_ available in debian repositories does not support
headers and footers so it can not be installed automatically.
The recommended version is 0.12.1 and is available on `the wkhtmltopdf download page`_,
in the archive section.
the version of wkhtmltopdf_ available in Debian repositories does
not support headers and footers so it is not used as a direct dependency.
The recommended version is 0.12.5 and is available on
`the wkhtmltopdf download page`_, in the archive section. Previously
recommended version 0.12.1 is a good alternative.
More details on the various versions and their respective quirks can be
found in our `wiki <https://github.com/odoo/odoo/wiki/Wkhtmltopdf>`_.
Repository
^^^^^^^^^^
@@ -609,6 +609,7 @@ class IrActionsReport(models.Model):
def render_qweb_pdf(self, res_ids=None, data=None):
if not data:
data = {}
data.setdefault('report_type', 'pdf')
# remove editor feature in pdf generation
data.update(enable_editor=False)
@@ -703,6 +704,9 @@ class IrActionsReport(models.Model):
@api.model
def render_qweb_text(self, docids, data=None):
if not data:
data = {}
data.setdefault('report_type', 'text')
data = self._get_rendering_context(docids, data)
return self.render_template(self.report_name, data), 'text'
@@ -710,6 +714,9 @@ class IrActionsReport(models.Model):
def render_qweb_html(self, docids, data=None):
"""This method generates and returns html version of a report.
"""
if not data:
data = {}
data.setdefault('report_type', 'html')
data = self._get_rendering_context(docids, data)
return self.render_template(self.report_name, data), 'html'
+12 -1
View File
@@ -1540,7 +1540,8 @@ class IrModelData(models.Model):
raise AccessError(_('Administrator access is required to uninstall a module'))
# enable model/field deletion
self = self.with_context(**{MODULE_UNINSTALL_FLAG: True})
# we deactivate prefetching to not try to read a column that has been deleted
self = self.with_context(**{MODULE_UNINSTALL_FLAG: True, 'prefetch_fields': False})
datas = self.search([('module', 'in', modules_to_remove)])
to_unlink = tools.OrderedSet()
@@ -1625,6 +1626,16 @@ class IrModelData(models.Model):
for (id, xmlid, model, res_id) in self._cr.fetchall():
if xmlid not in loaded_xmlids:
if model in self.env:
if self.search_count([
("model", "=", model),
("res_id", "=", res_id),
("id", "!=", id),
("id", "not in", bad_imd_ids),
]):
# another external id is still linked to the same record, only deleting the old imd
bad_imd_ids.append(id)
continue
_logger.info('Deleting %s@%s (%s)', res_id, model, xmlid)
record = self.env[model].browse(res_id)
if record.exists():
+17
View File
@@ -22,7 +22,9 @@ import lxml.html
import odoo
from odoo import api, fields, models, modules, tools, _
from odoo.addons.base.models.ir_model import MODULE_UNINSTALL_FLAG
from odoo.exceptions import AccessDenied, UserError
from odoo.osv import expression
from odoo.tools.parse_version import parse_version
from odoo.tools.misc import topological_sort
from odoo.http import request
@@ -454,9 +456,24 @@ class Module(models.Model):
"""
modules_to_remove = self.mapped('name')
self.env['ir.model.data']._module_data_uninstall(modules_to_remove)
self._remove_copied_views()
self.write({'state': 'uninstalled', 'latest_version': False})
return True
@api.multi
def _remove_copied_views(self):
""" Remove the copies of the views installed by the modules in `self`.
Those copies do not have an external id so they will not be cleaned by
`_module_data_uninstall`. This is why we rely on `key` instead.
It is important to remove these copies because using them will crash if
they rely on data that don't exist anymore if the module is removed.
"""
domain = expression.OR([[('key', '=like', m.name + '.%')] for m in self])
orphans = self.env['ir.ui.view'].with_context(**{'active_test': False, MODULE_UNINSTALL_FLAG: True}).search(domain)
orphans.unlink()
@api.multi
@api.returns('self')
def downstream_dependencies(self, known_deps=None,
+10 -4
View File
@@ -140,7 +140,8 @@ class IrTranslationImport(object):
WHERE type = 'code'
AND noupdate IS NOT TRUE
ON CONFLICT (type, lang, md5(src)) WHERE type = 'code'
DO UPDATE SET (name, lang, res_id, src, type, value, module, state, comments) = (EXCLUDED.name, EXCLUDED.lang, EXCLUDED.res_id, EXCLUDED.src, EXCLUDED.type, EXCLUDED.value, EXCLUDED.module, EXCLUDED.state, EXCLUDED.comments);
DO UPDATE SET (name, lang, res_id, src, type, value, module, state, comments) = (EXCLUDED.name, EXCLUDED.lang, EXCLUDED.res_id, EXCLUDED.src, EXCLUDED.type, EXCLUDED.value, EXCLUDED.module, EXCLUDED.state, EXCLUDED.comments)
WHERE EXCLUDED.value IS NOT NULL AND EXCLUDED.value != '';
""" % (self._model_table, self._table))
count += cr.rowcount
cr.execute(""" INSERT INTO %s(name, lang, res_id, src, type, value, module, state, comments)
@@ -149,7 +150,8 @@ class IrTranslationImport(object):
WHERE type = 'model'
AND noupdate IS NOT TRUE
ON CONFLICT (type, lang, name, res_id) WHERE type = 'model'
DO UPDATE SET (name, lang, res_id, src, type, value, module, state, comments) = (EXCLUDED.name, EXCLUDED.lang, EXCLUDED.res_id, EXCLUDED.src, EXCLUDED.type, EXCLUDED.value, EXCLUDED.module, EXCLUDED.state, EXCLUDED.comments);
DO UPDATE SET (name, lang, res_id, src, type, value, module, state, comments) = (EXCLUDED.name, EXCLUDED.lang, EXCLUDED.res_id, EXCLUDED.src, EXCLUDED.type, EXCLUDED.value, EXCLUDED.module, EXCLUDED.state, EXCLUDED.comments)
WHERE EXCLUDED.value IS NOT NULL AND EXCLUDED.value != '';
""" % (self._model_table, self._table))
count += cr.rowcount
cr.execute(""" INSERT INTO %s(name, lang, res_id, src, type, value, module, state, comments)
@@ -158,7 +160,8 @@ class IrTranslationImport(object):
WHERE type IN ('selection', 'constraint', 'sql_constraint')
AND noupdate IS NOT TRUE
ON CONFLICT (type, lang, name, md5(src)) WHERE type IN ('selection', 'constraint', 'sql_constraint')
DO UPDATE SET (name, lang, res_id, src, type, value, module, state, comments) = (EXCLUDED.name, EXCLUDED.lang, EXCLUDED.res_id, EXCLUDED.src, EXCLUDED.type, EXCLUDED.value, EXCLUDED.module, EXCLUDED.state, EXCLUDED.comments);
DO UPDATE SET (name, lang, res_id, src, type, value, module, state, comments) = (EXCLUDED.name, EXCLUDED.lang, EXCLUDED.res_id, EXCLUDED.src, EXCLUDED.type, EXCLUDED.value, EXCLUDED.module, EXCLUDED.state, EXCLUDED.comments)
WHERE EXCLUDED.value IS NOT NULL AND EXCLUDED.value != '';
""" % (self._model_table, self._table))
count += cr.rowcount
cr.execute(""" INSERT INTO %s(name, lang, res_id, src, type, value, module, state, comments)
@@ -167,7 +170,8 @@ class IrTranslationImport(object):
WHERE type = 'model_terms'
AND noupdate IS NOT TRUE
ON CONFLICT (type, name, lang, res_id, md5(src))
DO UPDATE SET (name, lang, res_id, src, type, value, module, state, comments) = (EXCLUDED.name, EXCLUDED.lang, EXCLUDED.res_id, EXCLUDED.src, EXCLUDED.type, EXCLUDED.value, EXCLUDED.module, EXCLUDED.state, EXCLUDED.comments);
DO UPDATE SET (name, lang, res_id, src, type, value, module, state, comments) = (EXCLUDED.name, EXCLUDED.lang, EXCLUDED.res_id, EXCLUDED.src, EXCLUDED.type, EXCLUDED.value, EXCLUDED.module, EXCLUDED.state, EXCLUDED.comments)
WHERE EXCLUDED.value IS NOT NULL AND EXCLUDED.value != '';
""" % (self._model_table, self._table))
count += cr.rowcount
cr.execute(""" INSERT INTO %s(name, lang, res_id, src, type, value, module, state, comments)
@@ -483,6 +487,8 @@ class IrTranslation(models.Model):
for translation in translations:
if not translation.value:
discarded += translation
# consider it done to avoid being matched against another term
done.add((translation.src, translation.lang))
elif translation.src in terms:
done.add((translation.src, translation.lang))
else:
+3
View File
@@ -1174,6 +1174,9 @@ actual arch.
""" Return the view ID corresponding to ``template``, which may be a
view ID or an XML ID. Note that this method may be overridden for other
kinds of template values.
This method could return the ID of something that is not a view (when
using fallback to `xmlid_to_res_id`).
"""
if isinstance(template, int):
return template
+9
View File
@@ -296,3 +296,12 @@ class Company(models.Model):
def action_save_onboarding_company_step(self):
if bool(self.street):
self.set_onboarding_step_done('base_onboarding_company_state')
@api.model
def _get_main_company(self):
try:
main_company = self.sudo().env.ref('base.main_company')
except ValueError:
main_company = self.env['res.company'].sudo().search([], limit=1, order="id")
return main_company
+20 -2
View File
@@ -922,8 +922,26 @@ class GroupsImplied(models.Model):
if values.get('users') or values.get('implied_ids'):
# add all implied groups (to all users of each group)
for group in self:
vals = {'users': list(zip(repeat(4), group.with_context(active_test=False).users.ids))}
super(GroupsImplied, group.trans_implied_ids).write(vals)
self._cr.execute("""
WITH RECURSIVE group_imply(gid, hid) AS (
SELECT gid, hid
FROM res_groups_implied_rel
UNION
SELECT i.gid, r.hid
FROM res_groups_implied_rel r
JOIN group_imply i ON (i.hid = r.gid)
)
INSERT INTO res_groups_users_rel (gid, uid)
SELECT i.hid, r.uid
FROM group_imply i, res_groups_users_rel r
WHERE r.gid = i.gid
AND i.gid = %(gid)s
EXCEPT
SELECT r.gid, r.uid
FROM res_groups_users_rel r
JOIN group_imply i ON (r.gid = i.hid)
WHERE i.gid = %(gid)s
""", dict(gid=group.id))
return res
class UsersImplied(models.Model):
@@ -13,10 +13,6 @@
padding-left: 80px;
}
.o_modules_kanban.o_kanban_view h4.o_kanban_record_title {
line-height: 1;
}
.o_modules_kanban.o_kanban_view .o_kanban_record .o_dropdown_kanban {
visibility: visible;
opacity: 0.5;
+40 -30
View File
@@ -2,7 +2,9 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import os
import platform
import psutil
import unittest
from odoo.exceptions import CacheMiss
from odoo.tests.common import TransactionCase
@@ -51,33 +53,35 @@ class TestRecordCache(TransactionCase):
self.assertCountEqual(cache.get_missing_ids(foo2 + bar2, name), [1, 2])
# set values in one environment only
for rec in [foo1, bar1]:
cache.set(rec, name, 'NAME1')
cache.set(rec, ref, 'REF1')
check(foo1, 'NAME1', 'REF1')
cache.set(foo1, name, 'FOO1_NAME')
cache.set(foo1, ref, 'FOO1_REF')
cache.set(bar1, name, 'BAR1_NAME')
cache.set(bar1, ref, 'BAR1_REF')
check(foo1, 'FOO1_NAME', 'FOO1_REF')
check(foo2, None, None)
check(bar1, 'NAME1', 'REF1')
check(bar1, 'BAR1_NAME', 'BAR1_REF')
check(bar2, None, None)
self.assertCountEqual(cache.get_missing_ids(foo1 + bar1, name), [])
self.assertCountEqual(cache.get_missing_ids(foo2 + bar2, name), [1, 2])
# set values in both environments
for rec in [foo2, bar2]:
cache.set(rec, name, 'NAME2')
cache.set(rec, ref, 'REF2')
check(foo1, 'NAME1', 'REF1')
check(foo2, 'NAME2', 'REF2')
check(bar1, 'NAME1', 'REF1')
check(bar2, 'NAME2', 'REF2')
cache.set(foo2, name, 'FOO2_NAME')
cache.set(foo2, ref, 'FOO2_REF')
cache.set(bar2, name, 'BAR2_NAME')
cache.set(bar2, ref, 'BAR2_REF')
check(foo1, 'FOO1_NAME', 'FOO1_REF')
check(foo2, 'FOO2_NAME', 'FOO2_REF')
check(bar1, 'BAR1_NAME', 'BAR1_REF')
check(bar2, 'BAR2_NAME', 'BAR2_REF')
self.assertCountEqual(cache.get_missing_ids(foo1 + bar1, name), [])
self.assertCountEqual(cache.get_missing_ids(foo2 + bar2, name), [])
# remove value in one environment
cache.remove(foo1, name)
check(foo1, None, 'REF1')
check(foo2, 'NAME2', 'REF2')
check(bar1, 'NAME1', 'REF1')
check(bar2, 'NAME2', 'REF2')
check(foo1, None, 'FOO1_REF')
check(foo2, 'FOO2_NAME', 'FOO2_REF')
check(bar1, 'BAR1_NAME', 'BAR1_REF')
check(bar2, 'BAR2_NAME', 'BAR2_REF')
self.assertCountEqual(cache.get_missing_ids(foo1 + bar1, name), [1])
self.assertCountEqual(cache.get_missing_ids(foo2 + bar2, name), [])
@@ -85,8 +89,8 @@ class TestRecordCache(TransactionCase):
cache.invalidate([(name, None), (ref, foo1.ids)])
check(foo1, None, None)
check(foo2, None, None)
check(bar1, None, 'REF1')
check(bar2, None, 'REF2')
check(bar1, None, 'BAR1_REF')
check(bar2, None, 'BAR2_REF')
# total invalidation
cache.invalidate()
@@ -96,28 +100,34 @@ class TestRecordCache(TransactionCase):
check(bar2, None, None)
# set a special value
cache.set_special(foo1, name, lambda: '42')
cache.set_special(foo1, name, lambda: 'FOO1_SPECIAL_NAME')
self.assertTrue(cache.contains(foo1, name))
self.assertFalse(cache.contains_value(foo1, name))
self.assertEqual(cache.get(foo1, name), '42')
self.assertEqual(cache.get(foo1, name), 'FOO1_SPECIAL_NAME')
self.assertIsNone(cache.get_value(foo1, name))
self.assertIsNotNone(cache.get_special(foo1, name))
# copy cache
for rec in [foo1, bar1]:
cache.set(rec, name, 'NAME1')
cache.set(rec, ref, 'REF1')
check(foo1, 'NAME1', 'REF1')
check(foo2, None, None)
check(bar1, 'NAME1', 'REF1')
cache.set(foo1, name, 'FOO1_NAME')
cache.set(foo1, ref, 'FOO1_REF')
cache.set(bar1, name, 'BAR1_NAME')
cache.set(bar1, ref, 'BAR1_REF')
cache.set(foo2, name, 'FOO2_NAME')
check(foo1, 'FOO1_NAME', 'FOO1_REF')
check(foo2, 'FOO2_NAME', None)
check(bar1, 'BAR1_NAME', 'BAR1_REF')
check(bar2, None, None)
cache.copy(foo1 + bar1, foo2.env)
check(foo1, 'NAME1', 'REF1')
check(foo2, 'NAME1', 'REF1')
check(bar1, 'NAME1', 'REF1')
check(bar2, 'NAME1', 'REF1')
check(foo1, 'FOO1_NAME', 'FOO1_REF')
check(foo2, 'FOO1_NAME', 'FOO1_REF')
check(bar1, 'BAR1_NAME', 'BAR1_REF')
check(bar2, 'BAR1_NAME', 'BAR1_REF')
@unittest.skipIf(
not(platform.system() == 'Linux' and platform.machine() == 'x86_64'),
"This test only makes sense on 64-bit Linux-like systems",
)
def test_memory(self):
""" Check memory consumption of the cache. """
NB_RECORDS = 100000
+28
View File
@@ -405,3 +405,31 @@ class TestXMLTranslation(TransactionCase):
self.assertEqual(view.with_env(env_en).arch_db, archf % terms_en)
self.assertEqual(view.with_env(env_fr).arch_db, archf % terms_fr)
self.assertEqual(view.with_env(env_nl).arch_db, archf % terms_nl)
def test_sync_update(self):
""" Check translations after minor change in source terms. """
archf = '<form string="X"><div>%s</div><div>%s</div></form>'
terms_src = ('Subtotal', 'Subtotal:')
terms_en = ('Subtotal', 'Sub total:')
view = self.create_view(archf, terms_src, en_US=terms_en)
translations = self.env['ir.translation'].search([
('type', '=', 'model_terms'),
('name', '=', "ir.ui.view,arch_db"),
('res_id', '=', view.id),
])
self.assertEqual(len(translations), 2)
# modifying the arch should sync existing translations without errors
view.write({
"arch": archf % ('Subtotal', 'Subtotal:<br/>')
})
translations = self.env['ir.translation'].search([
('type', '=', 'model_terms'),
('name', '=', "ir.ui.view,arch_db"),
('res_id', '=', view.id),
])
# 'Subtotal' being src==value, it will be discared
# 'Subtotal:' will be discarded as it match 'Subtotal' instead of 'Subtotal:<br/>'
self.assertEqual(len(translations), 0)
@@ -32,5 +32,6 @@ class BaseUpdateTranslations(models.TransientModel):
lang_name = self._get_lang_name(this.lang)
with tempfile.NamedTemporaryFile() as buf:
tools.trans_export(this.lang, ['all'], buf, 'po', self._cr)
tools.trans_load_data(self._cr, buf, 'po', this.lang, lang_name=lang_name)
context = {'create_empty_translation': True}
tools.trans_load_data(self._cr, buf, 'po', this.lang, lang_name=lang_name, context=context)
return {'type': 'ir.actions.act_window_close'}
@@ -26,3 +26,14 @@ class test_inherits(common.TransactionCase):
pallet = self.env.ref('test_inherits.pallet_a')
pallet.write({'name': 'C'})
self.assertEqual(pallet.name, 'C')
def test_ir_model_data_inherits(self):
""" Check the existence of the correct ir.model.data """
IrModelData = self.env['ir.model.data']
field = IrModelData.search([('name', '=', 'field_test_unit__name')])
self.assertEqual(len(field), 1)
self.assertEqual(field.module, 'test_inherits')
field = IrModelData.search([('name', '=', 'field_test_box__name')])
self.assertEqual(len(field), 1)
self.assertEqual(field.module, 'test_inherits')
@@ -0,0 +1,3 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import models
@@ -0,0 +1,12 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
'name': 'test-inherits-depends',
'version': '0.1',
'category': 'Tests',
'description': """A module to verify the inheritance using _inherits in non-original modules.""",
'depends': ['test_inherits'],
'data': [],
'installable': True,
'auto_install': False,
}
@@ -0,0 +1,10 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models, fields
# We add a field on this model
class Unit(models.Model):
_inherit = 'test.unit'
second_name = fields.Char()
@@ -0,0 +1,3 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import test_inherits
@@ -0,0 +1,29 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests import common
class test_inherits(common.TransactionCase):
def test_ir_model_data_inherits_again(self):
""" Re-run test_inherits test to make sure another imd hasn't been created """
IrModelData = self.env['ir.model.data']
field = IrModelData.search([('name', '=', 'field_test_unit__name')])
self.assertEqual(len(field), 1)
self.assertEqual(field.module, 'test_inherits')
field = IrModelData.search([('name', '=', 'field_test_box__name')])
self.assertEqual(len(field), 1)
self.assertEqual(field.module, 'test_inherits')
def test_ir_model_data_inherits_depends(self):
""" Check the existence of the correct ir.model.data """
IrModelData = self.env['ir.model.data']
field = IrModelData.search([('name', '=', 'field_test_unit__second_name')])
self.assertEqual(len(field), 1)
self.assertEqual(field.module, 'test_inherits_depends')
field = IrModelData.search([('name', '=', 'field_test_box__second_name')])
self.assertEqual(len(field), 1)
self.assertEqual(field.module, 'test_inherits_depends')
@@ -92,17 +92,23 @@ class TestTermCount(common.TransactionCase):
def test_export_empty_string(self):
"""When the string and the translation is equal the translation is empty"""
# Export the translations
def update_translations():
def update_translations(context=None):
context = dict(context or {}, overwrite=True)
with closing(io.BytesIO()) as bufferobj:
odoo.tools.trans_export('fr_FR', ['test_translation_import'], bufferobj, 'po', self.cr)
bufferobj.name = 'test_translation_import/i18n/fr.po'
odoo.tools.trans_load_data(self.cr, bufferobj, 'po', 'fr_FR', verbose=False, context={'overwrite': True})
odoo.tools.trans_load_data(self.cr, bufferobj, 'po', 'fr_FR', verbose=False, context=context)
# Check if the not translated key is empty string
# Check that the not translated key is not created
update_translations()
translation = self.env['ir.translation'].search_count([('src', '=', 'Efgh'), ('value', '=', '')])
self.assertFalse(translation, 'An empty translation is not imported')
# Check that "Generate Missing Terms" create empty string for not translated key
update_translations({'create_empty_translation': True})
translation = self.env['ir.translation'].search_count([('src', '=', 'Efgh'), ('value', '=', '')])
self.assertTrue(translation, 'The translation of "Efgh" should be empty')
# Modify the value translated for the equal value of the key
menu = self.env.ref('test_translation_import.menu_test_translation_import')
menu.name = "New Name"
+3
View File
@@ -560,6 +560,9 @@ class Field(MetaField('DummyField', (object,), {})):
if self.inherited and field.required:
self.required = True
if self.inherited:
self._modules.update(field._modules)
def traverse_related(self, record):
""" Traverse the fields of the related field `self` except for the last
one, and return it as a pair `(last_record, last_field)`. """
+2 -2
View File
@@ -1098,8 +1098,8 @@ def trans_load_data(cr, fileobj, fileformat, lang, lang_name=None, verbose=True,
dic['lang'] = lang
dic.update(zip(fields, row))
# ignore empty value that may be set from base language
if not dic['value']:
# do not import empty values
if not env.context.get('create_empty_translation', False) and not dic['value']:
return
if use_pot_reference: