[MERGE] forward port branch 12.0 up to 6a0675d36d
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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];
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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:"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
@@ -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')]}"/>
|
||||
|
||||
@@ -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,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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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/
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)`. """
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user