[MERGE] forward port branch saas-16 up to 85571bb78c

This commit is contained in:
Christophe Simonis
2017-08-10 17:01:05 +02:00
73 changed files with 956 additions and 203 deletions
@@ -307,7 +307,10 @@ class AccountBankStatement(models.Model):
#try to assign partner to bank_statement_line
stl_to_assign_partner = [stl.id for stl in st_lines_left if not stl.partner_id]
refs = list(set([st.name for st in st_lines_left if not stl.partner_id]))
if st_lines_left and stl_to_assign_partner and refs:
if st_lines_left and stl_to_assign_partner and refs\
and st_lines_left[0].journal_id.default_credit_account_id\
and st_lines_left[0].journal_id.default_debit_account_id:
sql_query = """SELECT aml.partner_id, aml.ref, stl.id
FROM account_move_line aml
JOIN account_account acc ON acc.id = aml.account_id
@@ -605,13 +608,14 @@ class AccountBankStatementLine(models.Model):
acc_type = "acc.internal_type IN ('payable', 'receivable')" if (self.partner_id or overlook_partner) else "acc.reconcile = true"
select_clause = "SELECT aml.id "
from_clause = "FROM account_move_line aml JOIN account_account acc ON acc.id = aml.account_id "
where_clause = """WHERE aml.company_id = %(company_id)s
AND (
(aml.statement_id IS NULL AND aml.account_id IN %(account_payable_receivable)s
AND aml.payment_id IS NOT NULL)
OR
("""+acc_type+""" AND aml.reconciled = false)
)"""
account_clause = ''
if self.journal_id.default_credit_account_id and self.journal_id.default_debit_account_id:
account_clause = "(aml.statement_id IS NULL AND aml.account_id IN %(account_payable_receivable)s AND aml.payment_id IS NOT NULL) OR"
where_clause = """WHERE aml.company_id = %(company_id)s
AND (
""" + account_clause + """
("""+acc_type+""" AND aml.reconciled = false)
)"""
where_clause = where_clause + ' AND aml.partner_id = %(partner_id)s' if self.partner_id else where_clause
where_clause = where_clause + ' AND aml.id NOT IN %(excluded_ids)s' if excluded_ids else where_clause
if split:
+2 -2
View File
@@ -837,7 +837,6 @@ class AccountInvoice(models.Model):
if tax.amount_type == "group":
for child_tax in tax.children_tax_ids:
done_taxes.append(child_tax.id)
done_taxes.append(tax.id)
res.append({
'invoice_tax_line_id': tax_line.id,
'tax_line_id': tax_line.tax_id.id,
@@ -849,8 +848,9 @@ class AccountInvoice(models.Model):
'account_id': tax_line.account_id.id,
'account_analytic_id': tax_line.account_analytic_id.id,
'invoice_id': self.id,
'tax_ids': [(6, 0, done_taxes)] if tax_line.tax_id.include_base_amount else []
'tax_ids': [(6, 0, list(done_taxes))] if tax_line.tax_id.include_base_amount else []
})
done_taxes.append(tax.id)
return res
def inv_line_characteristic_hashcode(self, invoice_line):
+98 -26
View File
@@ -539,6 +539,7 @@ class AccountMoveLine(models.Model):
{2}
WHERE
a.reconcile IS TRUE
AND l.full_reconcile_id is NULL
{3}
{4}
{5}
@@ -1557,6 +1558,50 @@ class AccountPartialReconcile(models.Model):
move.post()
return line_to_reconcile, partial_rec
# Do not forwardport in master as of 2017-07-20
def _fix_multiple_exchange_rates_diff(self, amls_to_fix, amount_diff, diff_in_currency, currency, move):
self.ensure_one()
move_lines = self.env['account.move.line'].with_context(check_move_validity=False)
partial_reconciles = self.with_context(skip_full_reconcile_check=True)
amount_diff = self.company_id.currency_id.round(amount_diff)
diff_in_currency = currency.round(diff_in_currency)
for aml in amls_to_fix:
account_payable_line = move_lines.create({
'name': _('Currency exchange rate difference'),
'debit': amount_diff < 0 and -aml.amount_residual or 0.0,
'credit': amount_diff > 0 and aml.amount_residual or 0.0,
'account_id': self.debit_move_id.account_id.id,
'move_id': move.id,
'currency_id': currency.id,
'amount_currency': -aml.amount_residual_currency,
'partner_id': self.debit_move_id.partner_id.id,
})
move_lines.create({
'name': _('Currency exchange rate difference'),
'debit': amount_diff > 0 and aml.amount_residual or 0.0,
'credit': amount_diff < 0 and -aml.amount_residual or 0.0,
'account_id': amount_diff > 0 and self.company_id.currency_exchange_journal_id.default_debit_account_id.id or self.company_id.currency_exchange_journal_id.default_credit_account_id.id,
'move_id': move.id,
'currency_id': currency.id,
'amount_currency': aml.amount_residual_currency,
'partner_id': self.debit_move_id.partner_id.id})
partial_rec = super(AccountPartialReconcile, partial_reconciles).create({
'debit_move_id': aml.credit and account_payable_line.id or aml.id,
'credit_move_id': aml.debit and account_payable_line.id or aml.id,
'amount': abs(aml.amount_residual),
'amount_currency': abs(aml.amount_residual_currency),
'currency_id': currency.id,
})
move_lines |= account_payable_line
partial_reconciles |= partial_rec
partial_reconciles._compute_partial_lines()
return move_lines, partial_reconciles
def create_tax_cash_basis_entry(self, percentage_before_rec):
self.ensure_one()
move_date = self.debit_move_id.date
@@ -1659,30 +1704,16 @@ class AccountPartialReconcile(models.Model):
}
return self.env['account.move'].create(move_vals)
@api.model
def create(self, vals):
aml = []
if vals.get('debit_move_id', False):
aml.append(vals['debit_move_id'])
if vals.get('credit_move_id', False):
aml.append(vals['credit_move_id'])
# Get value of matched percentage from both move before reconciliating
lines = self.env['account.move.line'].browse(aml)
if lines[0].account_id.internal_type in ('receivable', 'payable'):
percentage_before_rec = lines._get_matched_percentage()
# Reconcile
res = super(AccountPartialReconcile, self).create(vals)
# if the reconciliation is a matching on a receivable or payable account, eventually create a tax cash basis entry
if lines[0].account_id.internal_type in ('receivable', 'payable'):
res.create_tax_cash_basis_entry(percentage_before_rec)
# Do not forwardport in master as of 2017-07-20
def _compute_partial_lines(self):
if self._context.get('skip_full_reconcile_check'):
#when running the manual reconciliation wizard, don't check the partials separately for full
#reconciliation or exchange rate because it is handled manually after the whole processing
return res
return self
#check if the reconcilation is full
#first, gather all journal items involved in the reconciliation just created
partial_rec_set = OrderedDict.fromkeys([x for x in res])
aml_set = self.env['account.move.line']
partial_rec_set = OrderedDict.fromkeys([x for x in self])
aml_set = aml_to_balance = self.env['account.move.line']
total_debit = 0
total_credit = 0
total_amount_currency = 0
@@ -1690,7 +1721,7 @@ class AccountPartialReconcile(models.Model):
#possible to compute the exchange difference entry and it has to be done manually.
currency = list(partial_rec_set)[0].currency_id
maxdate = None
aml_to_balance = None
for partial_rec in partial_rec_set:
if partial_rec.currency_id != currency:
#no exchange rate entry will be created
@@ -1698,7 +1729,7 @@ class AccountPartialReconcile(models.Model):
for aml in [partial_rec.debit_move_id, partial_rec.credit_move_id]:
if aml not in aml_set:
if aml.amount_residual or aml.amount_residual_currency:
aml_to_balance = aml
aml_to_balance |= aml
maxdate = max(aml.date, maxdate)
total_debit += aml.debit
total_credit += aml.credit
@@ -1716,16 +1747,21 @@ class AccountPartialReconcile(models.Model):
aml_ids = aml_set.ids
#then, if the total debit and credit are equal, or the total amount in currency is 0, the reconciliation is full
digits_rounding_precision = aml_set[0].company_id.currency_id.rounding
if (currency and float_is_zero(total_amount_currency, precision_rounding=currency.rounding)) or float_compare(total_debit, total_credit, precision_rounding=digits_rounding_precision) == 0:
exchange_move_id = False
exchange_partial_rec_id = False
if currency and aml_to_balance:
exchange_move = (self.env['account.move']
.create(self.env['account.full.reconcile']
._prepare_exchange_diff_move(move_date=maxdate, company=aml_to_balance[0].company_id)))
#eventually create a journal entry to book the difference due to foreign currency's exchange rate that fluctuates
rate_diff_aml, rate_diff_partial_rec = partial_rec.create_exchange_rate_entry(aml_to_balance, total_debit - total_credit, total_amount_currency, currency, maxdate)
aml_ids.append(rate_diff_aml.id)
partial_rec_ids.append(rate_diff_partial_rec.id)
exchange_move_id = rate_diff_aml.move_id.id
exchange_partial_rec_id = rate_diff_partial_rec.id
rate_diff_amls, rate_diff_partial_recs = partial_rec._fix_multiple_exchange_rates_diff(aml_to_balance, total_debit - total_credit, total_amount_currency, currency, exchange_move)
aml_ids += rate_diff_amls.ids
partial_rec_ids += rate_diff_partial_recs.ids
exchange_move.post()
exchange_move_id = exchange_move.id
exchange_partial_rec_id = rate_diff_partial_recs[-1:].id
#mark the reference of the full reconciliation on the partial ones and on the entries
self.env['account.full.reconcile'].with_context(check_move_validity=False).create({
'partial_reconcile_ids': [(4, p_id) for p_id in partial_rec_ids],
@@ -1733,6 +1769,25 @@ class AccountPartialReconcile(models.Model):
'exchange_move_id': exchange_move_id,
'exchange_partial_rec_id': exchange_partial_rec_id,
})
# Do not forwardport in master as of 2017-07-20
@api.model
def create(self, vals):
aml = []
if vals.get('debit_move_id', False):
aml.append(vals['debit_move_id'])
if vals.get('credit_move_id', False):
aml.append(vals['credit_move_id'])
# Get value of matched percentage from both move before reconciliating
lines = self.env['account.move.line'].browse(aml)
if lines[0].account_id.internal_type in ('receivable', 'payable'):
percentage_before_rec = lines._get_matched_percentage()
# Reconcile
res = super(AccountPartialReconcile, self).create(vals)
# if the reconciliation is a matching on a receivable or payable account, eventually create a tax cash basis entry
if lines[0].account_id.internal_type in ('receivable', 'payable'):
res.create_tax_cash_basis_entry(percentage_before_rec)
res._compute_partial_lines()
return res
@api.multi
@@ -1778,3 +1833,20 @@ class AccountFullReconcile(models.Model):
rec.exchange_move_id = False
to_reverse.reverse_moves()
return super(AccountFullReconcile, self).unlink()
# Do not forwardport in master as of 2017-07-20
@api.model
def _prepare_exchange_diff_move(self, move_date, company):
if not company.currency_exchange_journal_id:
raise UserError(_("You should configure the 'Exchange Rate Journal' in the accounting settings, to manage automatically the booking of accounting entries related to differences between exchange rates."))
if not company.income_currency_exchange_account_id.id:
raise UserError(_("You should configure the 'Gain Exchange Rate Account' in the accounting settings, to manage automatically the booking of accounting entries related to differences between exchange rates."))
if not company.expense_currency_exchange_account_id.id:
raise UserError(_("You should configure the 'Loss Exchange Rate Account' in the accounting settings, to manage automatically the booking of accounting entries related to differences between exchange rates."))
res = {'journal_id': company.currency_exchange_journal_id.id}
# The move date should be the maximum date between payment and invoice
# (in case of payment in advance). However, we should make sure the
# move date is not recorded after the end of year closing.
if move_date > company.fiscalyear_lock_date:
res['date'] = move_date
return res
+144
View File
@@ -477,3 +477,147 @@ class TestReconciliation(AccountingTestCase):
self.assertEquals(round(aml.credit, 2), line['credit'])
self.assertEquals(round(aml.amount_currency, 2), line['amount_currency'])
self.assertEquals(aml.currency_id.id, line['currency_id'])
def test_partial_reconcile_currencies(self):
# client Account (payable, rsa)
# Debit Credit
# --------------------------------------------------------
# Pay a : 25/0.5 = 50 | Inv a : 50/0.5 = 100
# Pay b: 50/0.75 = 66.66 | Inv b : 50/0.75 = 66.66
# Pay c: 25/0.8 = 31.25 |
#
# Debit_currency = 100 | Credit currency = 100
# Debit = 147.91 | Credit = 166.66
# Balance Debit = 18.75
# Counterpart Credit goes in Exchange diff
dest_journal_id = self.env['account.journal'].search([('type', '=', 'purchase'), ('company_id', '=', self.env.ref('base.main_company').id)], limit=1)
account_expenses = self.env['account.account'].search([('user_type_id', '=', self.env.ref('account.data_account_type_expenses').id)], limit=1)
self.bank_journal_euro.write({'default_debit_account_id': self.account_rsa.id,
'default_credit_account_id': self.account_rsa.id})
dest_journal_id.write({'default_debit_account_id': self.account_rsa.id,
'default_credit_account_id': self.account_rsa.id})
# Setting up rates for USD (main_company is in EUR)
self.env['res.currency.rate'].create({'name': time.strftime('%Y') + '-' + '07' + '-01',
'rate': 0.5,
'currency_id': self.currency_usd_id,
'company_id': self.env.ref('base.main_company').id})
self.env['res.currency.rate'].create({'name': time.strftime('%Y') + '-' + '08' + '-01',
'rate': 0.75,
'currency_id': self.currency_usd_id,
'company_id': self.env.ref('base.main_company').id})
self.env['res.currency.rate'].create({'name': time.strftime('%Y') + '-' + '09' + '-01',
'rate': 0.80,
'currency_id': self.currency_usd_id,
'company_id': self.env.ref('base.main_company').id})
# Preparing Invoices (from vendor)
invoice_a = self.account_invoice_model.create({'partner_id': self.partner_agrolait_id,
'reference_type': 'none',
'currency_id': self.currency_usd_id,
'name': 'invoice to vendor',
'account_id': self.account_rsa.id,
'type': 'in_invoice',
'date_invoice': time.strftime('%Y') + '-' + '07' + '-01',
})
self.account_invoice_line_model.create({'product_id': self.product.id,
'quantity': 1,
'price_unit': 50,
'invoice_id': invoice_a.id,
'name': 'product that cost ' + str(50),
'account_id': account_expenses.id,
})
invoice_b = self.account_invoice_model.create({'partner_id': self.partner_agrolait_id,
'reference_type': 'none',
'currency_id': self.currency_usd_id,
'name': 'invoice to vendor',
'account_id': self.account_rsa.id,
'type': 'in_invoice',
'date_invoice': time.strftime('%Y') + '-' + '08' + '-01',
})
self.account_invoice_line_model.create({'product_id': self.product.id,
'quantity': 1,
'price_unit': 50,
'invoice_id': invoice_b.id,
'name': 'product that cost ' + str(50),
'account_id': account_expenses.id,
})
invoice_a.action_invoice_open()
invoice_b.action_invoice_open()
# Preparing Payments
# One partial for invoice_a (fully assigned to it)
payment_a = self.env['account.payment'].create({'payment_type': 'outbound',
'amount': 25,
'currency_id': self.currency_usd_id,
'journal_id': self.bank_journal_euro.id,
'company_id': self.env.ref('base.main_company').id,
'payment_date': time.strftime('%Y') + '-' + '07' + '-01',
'partner_id': self.partner_agrolait_id,
'payment_method_id': self.env.ref('account.account_payment_method_manual_out').id,
'destination_journal_id': dest_journal_id.id,
'partner_type': 'supplier'})
# One that will complete the payment of a, the rest goes to b
payment_b = self.env['account.payment'].create({'payment_type': 'outbound',
'amount': 50,
'currency_id': self.currency_usd_id,
'journal_id': self.bank_journal_euro.id,
'company_id': self.env.ref('base.main_company').id,
'payment_date': time.strftime('%Y') + '-' + '08' + '-01',
'partner_id': self.partner_agrolait_id,
'payment_method_id': self.env.ref('account.account_payment_method_manual_out').id,
'destination_journal_id': dest_journal_id.id,
'partner_type': 'supplier'})
# The last one will complete the payment of b
payment_c = self.env['account.payment'].create({'payment_type': 'outbound',
'amount': 25,
'currency_id': self.currency_usd_id,
'journal_id': self.bank_journal_euro.id,
'company_id': self.env.ref('base.main_company').id,
'payment_date': time.strftime('%Y') + '-' + '09' + '-01',
'partner_id': self.partner_agrolait_id,
'payment_method_id': self.env.ref('account.account_payment_method_manual_out').id,
'destination_journal_id': dest_journal_id.id,
'partner_type': 'supplier'})
payment_a.post()
payment_b.post()
payment_c.post()
# Assigning payments to invoices
debit_line_a = payment_a.move_line_ids.filtered(lambda l: l.debit and l.account_id == dest_journal_id.default_debit_account_id)
debit_line_b = payment_b.move_line_ids.filtered(lambda l: l.debit and l.account_id == dest_journal_id.default_debit_account_id)
debit_line_c = payment_c.move_line_ids.filtered(lambda l: l.debit and l.account_id == dest_journal_id.default_debit_account_id)
invoice_a.assign_outstanding_credit(debit_line_a.id)
invoice_a.assign_outstanding_credit(debit_line_b.id)
invoice_b.assign_outstanding_credit(debit_line_b.id)
invoice_b.assign_outstanding_credit(debit_line_c.id)
# Asserting correctness (only in the payable account)
full_reconcile = False
for inv in (invoice_a + invoice_b):
self.assertTrue(inv.reconciled)
for aml in (inv.payment_move_line_ids + inv.move_id.line_ids).filtered(lambda l: l.account_id == self.account_rsa):
self.assertEqual(aml.amount_residual, 0.0)
self.assertEqual(aml.amount_residual_currency, 0.0)
self.assertTrue(aml.reconciled)
if not full_reconcile:
full_reconcile = aml.full_reconcile_id
else:
self.assertTrue(aml.full_reconcile_id == full_reconcile)
full_rec_move = full_reconcile.exchange_move_id
# Globally check whether the amount is correct
self.assertEqual(full_rec_move.amount, 18.75)
# Checking if the direction of the move is correct
full_rec_payable = full_rec_move.line_ids.filtered(lambda l: l.account_id == self.account_rsa)
self.assertEqual(full_rec_payable.balance, 18.75)
@@ -16,7 +16,7 @@
<form string="Configure Accounting" class="oe_form_configuration">
<header>
<button string="Save" type="object" name="execute" class="oe_highlight"/>
<button string="Cancel" type="object" name="cancel" class="oe_link"/>
<button string="Cancel" type="object" name="cancel" class="oe_link" special="cancel"/>
</header>
<sheet>
<field name="company_id" invisible="1"/>
@@ -108,3 +108,6 @@ class AccountInvoiceLine(models.Model):
elif invoice.type == 'in_invoice':
self.asset_category_id = self.product_id.product_tmpl_id.asset_category_id.id
super(AccountInvoiceLine, self)._set_additional_fields(invoice)
def get_invoice_line_account(self, type, product, fpos, company):
return product.asset_category_id.account_asset_id or super(AccountInvoiceLine, self).get_invoice_line_account(type, product, fpos, company)
@@ -125,7 +125,7 @@ QUnit.test('basic rendering', function (assert) {
});
QUnit.test('click events are correctly triggered', function (assert) {
assert.expect(3);
assert.expect(2);
var form = createView({
View: FormView,
@@ -146,8 +146,7 @@ QUnit.test('click events are correctly triggered', function (assert) {
intercepts: {
execute_action: function (event) {
var data = event.data;
assert.strictEqual(data.model, 'line', "should have correct model");
assert.deepEqual(data.res_ids, [4], "should have correct res_ids");
assert.strictEqual(data.env.model, 'line', "should have correct model");
assert.strictEqual(data.action_data.name, 'create_move',
"should call correct method");
},
@@ -7,7 +7,7 @@
<form string="General Settings" class="oe_form_configuration">
<header>
<button string="Save" type="object" name="execute" class="oe_highlight"/>
<button string="Cancel" type="object" name="cancel" class="oe_link"/>
<button string="Cancel" type="object" name="cancel" class="oe_link" special="cancel"/>
</header>
<sheet>
<div id="business_documents">
+3
View File
@@ -316,6 +316,9 @@ class EventEvent(models.Model):
for attendee in self.registration_ids.filtered(filter_func):
self.env['mail.template'].browse(template_id).send_mail(attendee.id, force_send=force_send)
@api.multi
def _is_event_registrable(self):
return True
class EventRegistration(models.Model):
_name = 'event.registration'
@@ -8,7 +8,7 @@
<form String="Event settings" class="oe_form_configuration">
<header>
<button string="Save" type="object" name="execute" class="oe_highlight"/>
<button string="Cancel" type="object" name="cancel" class="oe_link"/>
<button string="Cancel" type="object" name="cancel" class="oe_link" special="cancel"/>
</header>
<sheet>
<h2>Advertising</h2>
+6
View File
@@ -53,6 +53,12 @@ class Event(models.Model):
})
for ticket in self.event_type_id.event_ticket_ids]
@api.multi
def _is_event_registrable(self):
self.ensure_one()
if not self.event_ticket_ids:
return True
return all(self.event_ticket_ids.with_context(active_test=False).mapped(lambda t: t.product_id.active))
class EventTicket(models.Model):
_name = 'event.event.ticket'
+11 -10
View File
@@ -98,7 +98,7 @@
</table>
% else:
<span style="color:#A8A8A8;font-size:13px;">
The following message contains the current progress for the challenge <strong>${object.name}</strong>.
Challenge: <strong>${object.name}</strong>.
</span>
% for line in challenge_lines:
<!-- Header + Button table -->
@@ -113,7 +113,7 @@
% if len(line['goals'])>2:
<table cellspacing="0" cellpadding="0" width="100%" style="margin-top:10px;">
<tr><td style="padding:0 30px;">
<table cellspacing="0" cellpadding="0" width="100%">
<table cellspacing="0" cellpadding="0" width="100%" style="table-layout: fixed;">
<tr>
% set top_goals = [line['goals'][1], line['goals'][0], line['goals'][2]]
% for goal in top_goals:
@@ -144,9 +144,10 @@
<div style="height:${heightA}px;">
${extra_div | safe}
<div style="height:55px;">
<img style="margin-bottom:5px;width:50px;height:50px;border-radius:50%;" src="/web/image?model=res.users&amp;field=image_small&amp;id=${goal['user_id']}" alt=""/>
% set path = object.env['res.users'].browse(goal['user_id']).partner_id.image_small
<img style="margin-bottom:5px;width:50px;height:50px;border-radius:50%;" src="data:image/jpeg;base64,${path}" alt="${goal['name']}"/>
</div>
<div align="center" style ="color:${bgColor};">
<div align="center" style ="color:${bgColor};height:20px">
${goal['name']}
</div
</div>
@@ -177,8 +178,8 @@
<table cellspacing="0" cellpadding="0" width="100%" style="margin-top:30px;margin-bottom:5px;color:#666666;">
<thead>
<tr style="color:#9A6C8E; font-size:12px;">
<th style="width:10%;text-align:center;">Rank</th>
<th style="width:15%;text-align:left;">Name</th>
<th style="width:15%;text-align:center;">Rank</th>
<th style="width:25%;text-align:left;">Name</th>
<th style="width:30%;text-align:right;">Performance
% if line['suffix']:
(${line['suffix']})
@@ -199,12 +200,12 @@
% set tdColor = 'gray'
% set mutedColor = '#AAAAAA'
% set tdPercentageColor = '#9A6C8E'
<td width="10%" align="center" valign="middle" style="background-color:${tdBgColor};padding :5px 0;font-size:20px;">${goal['rank']+1}
<td width="15%" align="center" valign="middle" style="background-color:${tdBgColor};padding :5px 0;font-size:20px;">${goal['rank']+1}
</td>
<td width="15%" align="left" valign="middle" style="background-color:${tdBgColor};padding :5px 0;font-size:13px;">${goal['name']}</td>
<td width="20%" align="right" style="background-color:${tdBgColor};padding:5px 0;line-height:1;">${"%.2f" % goal['current']}<br/><span style="font-size:13px;color:${mutedColor};">on ${"%.2f" % line['target']}</span>
<td width="25%" align="left" valign="middle" style="background-color:${tdBgColor};padding :5px 0;font-size:13px;">${goal['name']}</td>
<td width="30%" align="right" style="background-color:${tdBgColor};padding:5px 0;line-height:1;">${"%.2f" % goal['current']}<br/><span style="font-size:13px;color:${mutedColor};">on ${"%.2f" % line['target']}</span>
</td>
<td width="20%" style="color:${tdPercentageColor};background-color:${tdBgColor};padding-right:15px;font-size:22px;" align="right"><strong>${goal['completeness'] | int}%</strong></td>
<td width="30%" style="color:${tdPercentageColor};background-color:${tdBgColor};padding-right:15px;font-size:22px;" align="right"><strong>${goal['completeness'] | int}%</strong></td>
</tr>
<tr>
<td colspan="5" style="height:1px;background-color:#DADADA;"></td>
@@ -7,7 +7,7 @@
<form string="Configuration" class= "oe_form_configuration">
<header>
<button string="Save" type="object" name="execute" class="oe_highlight"/>
<button string="Cancel" type="object" name="cancel" class="oe_link"/>
<button string="Cancel" type="object" name="cancel" class="oe_link" special="cancel"/>
</header>
<sheet>
<h2>Check-In/Out</h2>
@@ -64,4 +64,4 @@
<menuitem id="hr_attendance.menu_hr_attendance_settings" name="Configuration" parent="menu_hr_attendance_root"
sequence="99" action="action_hr_attendance_settings" groups="hr_attendance.group_hr_attendance_manager"/>
</odoo>
</odoo>
+1 -1
View File
@@ -385,7 +385,7 @@ class HrExpenseSheet(models.Model):
_order = "accounting_date desc, id desc"
name = fields.Char(string='Expense Report Summary', required=True)
expense_line_ids = fields.One2many('hr.expense', 'sheet_id', string='Expense Lines', states={'done': [('readonly', True)], 'post': [('readonly', True)]}, copy=False)
expense_line_ids = fields.One2many('hr.expense', 'sheet_id', string='Expense Lines', states={'approve': [('readonly', True)], 'done': [('readonly', True)], 'post': [('readonly', True)]}, copy=False)
state = fields.Selection([('submit', 'Submitted'),
('approve', 'Approved'),
('post', 'Posted'),
@@ -9,7 +9,7 @@
<form class="oe_form_configuration">
<header>
<button string="Save" type="object" name="execute" class="oe_highlight"/>
<button string="Cancel" type="object" name="cancel" class="oe_link"/>
<button string="Cancel" type="object" name="cancel" class="oe_link" special="cancel"/>
</header>
<sheet>
<h2>Recording</h2>
+1 -1
View File
@@ -392,7 +392,7 @@
<field name="accounting_date"/>
<field name="employee_id"/>
<field name="currency_id" invisible="1"/>
<field name="total_amount" sum="Total Amount" widget="monetary" options="{'currency_field': 'currency_id'}"/>
<field name="total_amount" sum="Total Amount"/>
<field name="state"/>
<field name="message_unread" invisible="1"/>
</tree>
@@ -15,7 +15,7 @@
<form string="Configure Payroll" class="oe_form_configuration">
<header>
<button string="Apply" type="object" name="execute" class="oe_highlight"/>
<button string="Cancel" type="object" name="cancel" class="oe_link"/>
<button string="Cancel" type="object" name="cancel" class="oe_link" special="cancel"/>
</header>
<field name="module_l10n_fr_hr_payroll" invisible="1"/>
<field name="module_l10n_be_hr_payroll" invisible="1"/>
@@ -8,7 +8,7 @@
<form string="Configure HR Recruitment" class="oe_form_configuration">
<header>
<button string="Save" type="object" name="execute" class="oe_highlight"/>
<button string="Cancel" type="object" name="cancel" class="oe_link"/>
<button string="Cancel" type="object" name="cancel" class="oe_link" special="cancel"/>
</header>
<sheet>
<h2>Job Posting</h2>
+1 -1
View File
@@ -10,7 +10,7 @@ class AccountAnalyticLine(models.Model):
@api.model
def default_get(self, field_list):
result = super(AccountAnalyticLine, self).default_get(field_list)
if 'employee_id' in field_list and result.get('user_id') and result.get('project_id'):
if 'employee_id' in field_list and result.get('user_id'):
result['employee_id'] = self.env['hr.employee'].search([('user_id', '=', result['user_id'])], limit=1).id
return result
+1
View File
@@ -571,6 +571,7 @@ class MailTemplate(models.Model):
'name': attachment[0],
'datas_fname': attachment[0],
'datas': attachment[1],
'type': 'binary',
'res_model': 'mail.message',
'res_id': mail.mail_message_id.id,
}
+5 -3
View File
@@ -950,13 +950,14 @@ class MailThread(models.AbstractModel):
# Alias: check alias_contact settings
if alias:
obj = None
if thread_id:
obj = record_set[0]
elif alias.alias_parent_thread_id:
elif alias.alias_parent_model_id and alias.alias_parent_thread_id:
obj = self.env[alias.alias_parent_model_id.model].browse(alias.alias_parent_thread_id)
elif model and hasattr(record_set, '_alias_check_contact'):
elif model:
obj = self.env[model]
else:
if not hasattr(obj, '_alias_check_contact'):
obj = self.env['mail.alias.mixin']
check_result = obj._alias_check_contact(message, message_dict, alias)
if check_result is not True:
@@ -1727,6 +1728,7 @@ class MailThread(models.AbstractModel):
data_attach = {
'name': name,
'datas': base64.b64encode(str(content)),
'type': 'binary',
'datas_fname': name,
'description': name,
'res_model': message_data['model'],
@@ -7,7 +7,7 @@
<form string="Configure Mass Mailing" class="oe_form_configuration">
<header>
<button string="Save" type="object" name="execute" class="oe_highlight"/>
<button string="Cancel" type="object" name="cancel" class="oe_link"/>
<button string="Cancel" type="object" name="cancel" class="oe_link" special="cancel"/>
</header>
<sheet>
<h2>Design</h2>
@@ -8,7 +8,7 @@
<form string="Configure Manufacturing" class= "oe_form_configuration">
<header>
<button string="Save" type="object" name="execute" class="oe_highlight"/>
<button string="Cancel" type="object" name="cancel" class="oe_link"/>
<button string="Cancel" type="object" name="cancel" class="oe_link" special="cancel"/>
</header>
<sheet>
<h2>Operations</h2>
+1 -1
View File
@@ -122,7 +122,7 @@ class PadCommon(models.AbstractModel):
# Update the pad if the `pad_content_field` is modified
for k, field in pycompat.items(self._fields):
if hasattr(field, 'pad_content_field') and vals.get(field.pad_content_field):
if hasattr(field, 'pad_content_field') and vals.get(field.pad_content_field) and self[k]:
company = self.env.user.sudo().company_id
myPad = EtherpadLiteClient(company.pad_key, company.pad_server + '/api')
path = self[k].split('/p/')[1]
+1 -1
View File
@@ -55,7 +55,7 @@ var FieldPad = AbstractField.extend({
// Guess what we decided...
var url = {};
url.toJSON = _.constant(this.url);
this._setValue(url);
this._setValue(url, {doNotSetDirty: true});
}
return this._super.apply(this, arguments);
+36
View File
@@ -193,5 +193,41 @@ QUnit.module('pad widget', {
delete FieldPad.prototype.isPadConfigured;
});
QUnit.test('pad widget is not considered dirty at edition', function (assert) {
assert.expect(2);
var form = createView({
View: FormView,
model: 'task',
data: this.data,
arch:'<form>' +
'<sheet>' +
'<group>' +
'<field name="description" widget="pad"/>' +
'</group>' +
'</sheet>' +
'</form>',
res_id: 2,
mockRPC: function (route, args) {
if (!args.method) {
return $.when(true);
}
return this._super.apply(this, arguments);
},
session: {
userName: "batman",
},
});
form.$buttons.find('.o_form_button_edit').click();
var def = form.canBeDiscarded();
assert.strictEqual($('.modal').length, 0,
"should have no confirmation modal opened");
assert.strictEqual(def.state(), 'resolved',
"can be discarded was succesfully resolved");
form.destroy();
delete FieldPad.prototype.isPadConfigured;
});
});
+7 -1
View File
@@ -15,6 +15,12 @@ _logger = logging.getLogger(__name__)
# changelog https://stripe.com/docs/upgrades#api-changelog
STRIPE_HEADERS = {'Stripe-Version': '2016-03-07'}
# The following currencies are integer only, see https://stripe.com/docs/currencies#zero-decimal
INT_CURRENCIES = [
'BIF', 'XAF', 'XPF', 'CLP', 'KMF', 'DJF', 'GNF', 'JPY', 'MGA', 'PYGí', 'RWF', 'KRW', 'VUV',
'VND', 'XOF'
];
class PaymentAcquirerStripe(models.Model):
_inherit = 'payment.acquirer'
@@ -84,7 +90,7 @@ class PaymentTransactionStripe(models.Model):
def _create_stripe_charge(self, acquirer_ref=None, tokenid=None, email=None):
api_url_charge = 'https://%s/charges' % (self.acquirer_id._get_stripe_api_url())
charge_params = {
'amount': int(self.amount*100), # Stripe takes amount in cents (https://support.stripe.com/questions/which-zero-decimal-currencies-does-stripe-support)
'amount': int(self.amount if self.currency_id.name in INT_CURRENCIES else self.amount*100),
'currency': self.currency_id.name,
'metadata[reference]': self.reference
}
+26 -6
View File
@@ -1,6 +1,14 @@
odoo.define('payment_stripe.stripe', function(require) {
"use strict";
var ajax = require('web.ajax');
// The following currencies are integer only, see
// https://stripe.com/docs/currencies#zero-decimal
var int_currencies = [
'BIF', 'XAF', 'XPF', 'CLP', 'KMF', 'DJF', 'GNF', 'JPY', 'MGA', 'PYGí',
'RWF', 'KRW', 'VUV', 'VND', 'XOF'
];
var handler = StripeCheckout.configure({
key: $("input[name='stripe_key']").val(),
image: $("input[name='stripe_image']").val(),
@@ -50,19 +58,31 @@ odoo.define('payment_stripe.stripe', function(require) {
e.preventDefault();
if ($('.o_website_payment').length !== 0) {
var currency = $("input[name='currency']").val();
var amount = parseFloat($("input[name='amount']").val() || '0.0');
if (!_.contains(int_currencies, currency)) {
amount = amount*100;
}
ajax.jsonRpc('/website_payment/transaction', 'call', {
reference: $("input[name='invoice_num']").val(),
amount: $("input[name='amount']").val(),
currency_id: $("input[name='currency_id']").val(),
amount: amount,
currency_id: currency,
acquirer_id: acquirer_id
})
handler.open({
name: $("input[name='merchant']").val(),
description: $("input[name='invoice_num']").val(),
currency: $("input[name='currency']").val(),
amount: $("input[name='amount']").val()*100
currency: currency,
amount: amount,
});
} else {
var currency = $("input[name='currency']").val();
var amount = parseFloat($("input[name='amount']").val() || '0.0');
if (!_.contains(int_currencies, currency)) {
amount = amount*100;
}
ajax.jsonRpc('/shop/payment/transaction/' + acquirer_id, 'call', {
so_id: so_id,
access_token: access_token
@@ -71,8 +91,8 @@ odoo.define('payment_stripe.stripe', function(require) {
handler.open({
name: $("input[name='merchant']").val(),
description: $("input[name='invoice_num']").val(),
currency: $("input[name='currency']").val(),
amount: $("input[name='amount']").val()*100
currency: currency,
amount: amount,
});
});
}
+1 -1
View File
@@ -870,7 +870,7 @@ class PosOrderLine(models.Model):
def create(self, values):
if values.get('order_id') and not values.get('name'):
# set name based on the sequence specified on the config
config_id = self.env['pos.order'].browse(values['order_id']).session_id.config_id.id
config_id = self.order_id.browse(values['order_id']).session_id.config_id.id
# HACK: sequence created in the same transaction as the config
# cf TODO master is pos.config create
# remove me saas-15
@@ -397,12 +397,13 @@ class TestPointOfSaleFlow(TestPointOfSaleCommon):
self.assertFalse(self.pos_order_pos1.invoice_id, 'Invoice should not be attached to order.')
# I generate an invoice from the order
self.invoice = self.pos_order_pos1.action_pos_order_invoice()
res = self.pos_order_pos1.action_pos_order_invoice()
self.assertIn('res_id', res, "No invoice created")
# I test that the total of the attached invoice is correct
self.amount_total = self.pos_order_pos1.amount_total
invoice = self.env['account.invoice'].browse(res['res_id'])
self.assertEqual(
float_compare(self.amount_total, 1752.75, precision_digits=2), 0, "Invoice not correct")
float_compare(invoice.amount_total, 1752.75, precision_digits=2), 0, "Invoice not correct")
"""In order to test the reports on Bank Statement defined in point_of_sale module, I create a bank statement line, confirm it and print the reports"""
@@ -7,7 +7,7 @@
<form class="oe_form_configuration">
<header>
<button string="Save" type="object" name="execute" class="oe_highlight"/>
<button string="Cancel" type="object" name="cancel" class="oe_link"/>
<button string="Cancel" type="object" name="cancel" class="oe_link" special="cancel"/>
</header>
<sheet>
<h2>Point of Sale</h2>
+3 -1
View File
@@ -295,7 +295,9 @@ class ProductProduct(models.Model):
@api.model
def create(self, vals):
product = super(ProductProduct, self.with_context(create_product_product=True)).create(vals)
product._set_standard_price(vals.get('standard_price', 0.0))
# When a unique variant is created from tmpl then the standard price is set by _set_standard_price
if not (self.env.context.get('create_from_tmpl') and len(product.product_tmpl_id.product_variant_ids) == 1):
product._set_standard_price(vals.get('standard_price') or 0.0)
return product
@api.multi
+1 -2
View File
@@ -113,8 +113,7 @@ class Pricelist(models.Model):
uom_id = self._context['uom']
if uom_id:
# rebrowse with uom if given
product_ids = [item[0].id for item in products_qty_partner]
products = self.env['product.product'].with_context(uom=uom_id).browse(product_ids)
products = [item[0].with_context(uom=uom_id) for item in products_qty_partner]
products_qty_partner = [(products[index], data_struct[1], data_struct[2]) for index, data_struct in enumerate(products_qty_partner)]
else:
products = [item[0] for item in products_qty_partner]
+4 -2
View File
@@ -172,7 +172,9 @@ class ProductTemplate(models.Model):
# Support context pricelists specified as display_name or ID for compatibility
if isinstance(pricelist_id_or_name, basestring):
pricelist = self.env['product.pricelist'].name_search(pricelist_id_or_name, operator='=', limit=1)
pricelist_data = self.env['product.pricelist'].name_search(pricelist_id_or_name, operator='=', limit=1)
if pricelist_data:
pricelist = self.env['product.pricelist'].browse(pricelist_data[0][0])
elif isinstance(pricelist_id_or_name, pycompat.integer_types):
pricelist = self.env['product.pricelist'].browse(pricelist_id_or_name)
@@ -288,7 +290,7 @@ class ProductTemplate(models.Model):
tools.image_resize_images(vals)
template = super(ProductTemplate, self).create(vals)
if "create_product_product" not in self._context:
template.create_variant_ids()
template.with_context(create_from_tmpl=True).create_variant_ids()
# This is needed to set given values to first variant after creation
related_vals = {}
@@ -8,7 +8,7 @@
<form class="oe_form_configuration">
<header>
<button string="Save" type="object" name="execute" class="oe_highlight"/>
<button string="Cancel" type="object" name="cancel" class="oe_link"/>
<button string="Cancel" type="object" name="cancel" class="oe_link" special="cancel"/>
</header>
<sheet>
<h2>Task Management</h2>
@@ -7,7 +7,7 @@
<form string="Configure Purchases" class="oe_form_configuration">
<header>
<button string="Save" type="object" name="execute" class="oe_highlight"/>
<button string="Cancel" type="object" name="cancel" class="oe_link"/>
<button string="Cancel" type="object" name="cancel" class="oe_link" special="cancel"/>
</header>
<sheet>
<field name="po_double_validation" invisible="1"/>
@@ -8,7 +8,7 @@
<form string="Configure Sales" class="oe_form_configuration" name="sale_config_form">
<header>
<button string="Save" type="object" name="execute" class="oe_highlight"/>
<button string="Cancel" type="object" name="cancel" class="oe_link"/>
<button string="Cancel" type="object" name="cancel" class="oe_link" special="cancel"/>
</header>
<sheet>
<h2 id="integration_h2" invisible="1">Integrations</h2>
@@ -84,4 +84,4 @@
<field name="view_mode">form</field>
<field name="target">inline</field>
</record>
</odoo>
</odoo>
+1 -1
View File
@@ -69,7 +69,7 @@ class Location(models.Model):
_sql_constraints = [('barcode_company_uniq', 'unique (barcode,company_id)', 'The barcode for a location must be unique per company !')]
@api.one
@api.depends('name', 'location_id')
@api.depends('name', 'location_id.name')
def _compute_complete_name(self):
""" Forms complete name of location from parent location to child location. """
name = self.name
@@ -8,7 +8,7 @@
<form string="Configure Warehouse" class="oe_form_configuration">
<header>
<button string="Save" type="object" name="execute" class="oe_highlight"/>
<button string="Cancel" type="object" name="cancel" class="oe_link"/>
<button string="Cancel" type="object" name="cancel" class="oe_link" special="cancel"/>
</header>
<sheet>
<field name="company_id" invisible="1"/>
+6 -6
View File
@@ -684,7 +684,7 @@ class Database(http.Controller):
request.session.authenticate(name, post['login'], password)
return http.local_redirect('/web/')
except Exception as e:
error = "Database creation error: %s" % str(e) or repr(e)
error = "Database creation error: %s" % (str(e) or repr(e))
return self._render_template(error=error)
@http.route('/web/database/duplicate', type='http', auth="none", methods=['POST'], csrf=False)
@@ -695,7 +695,7 @@ class Database(http.Controller):
dispatch_rpc('db', 'duplicate_database', [master_pwd, name, new_name])
return http.local_redirect('/web/database/manager')
except Exception as e:
error = "Database duplication error: %s" % str(e) or repr(e)
error = "Database duplication error: %s" % (str(e) or repr(e))
return self._render_template(error=error)
@http.route('/web/database/drop', type='http', auth="none", methods=['POST'], csrf=False)
@@ -705,7 +705,7 @@ class Database(http.Controller):
request._cr = None # dropping a database leads to an unusable cursor
return http.local_redirect('/web/database/manager')
except Exception as e:
error = "Database deletion error: %s" % str(e) or repr(e)
error = "Database deletion error: %s" % (str(e) or repr(e))
return self._render_template(error=error)
@http.route('/web/database/backup', type='http', auth="none", methods=['POST'], csrf=False)
@@ -723,7 +723,7 @@ class Database(http.Controller):
return response
except Exception as e:
_logger.exception('Database.backup')
error = "Database backup error: %s" % str(e) or repr(e)
error = "Database backup error: %s" % (str(e) or repr(e))
return self._render_template(error=error)
@http.route('/web/database/restore', type='http', auth="none", methods=['POST'], csrf=False)
@@ -733,7 +733,7 @@ class Database(http.Controller):
dispatch_rpc('db', 'restore', [master_pwd, name, data, str2bool(copy)])
return http.local_redirect('/web/database/manager')
except Exception as e:
error = "Database restore error: %s" % str(e) or repr(e)
error = "Database restore error: %s" % (str(e) or repr(e))
return self._render_template(error=error)
@http.route('/web/database/change_password', type='http', auth="none", methods=['POST'], csrf=False)
@@ -742,7 +742,7 @@ class Database(http.Controller):
dispatch_rpc('db', 'change_admin_password', [master_pwd, master_pwd_new])
return http.local_redirect('/web/database/manager')
except Exception as e:
error = "Master password update error: %s" % str(e) or repr(e)
error = "Master password update error: %s" % (str(e) or repr(e))
return self._render_template(error=error)
@http.route('/web/database/list', type='json', auth='none')
+18 -15
View File
@@ -20,7 +20,7 @@ var ViewManager = Widget.extend(ControlPanelMixin, {
custom_events: {
execute_action: function(event) {
var data = event.data;
this.do_execute_action(data.action_data, data.model, data.res_ids, data.on_closed)
this.do_execute_action(data.action_data, data.env, data.on_closed)
.then(data.on_success, data.on_fail);
},
search: function(event) {
@@ -554,16 +554,19 @@ var ViewManager = Widget.extend(ControlPanelMixin, {
* @param {string} [action_data.effect] if given, a visual effect (a
* rainbowman by default) will be displayed when the action is complete,
* with the string (evaluated) given as options.
* @param {DataSet} dataset a dataset object used to communicate with the server
* @param {integer[]} [res_ids] the res_ids of the objects on which the action is to be applied (fallback on env.ids if not set)
* @param {Object} env
* @param {string} env.model the model of the record(s) triggering the action
* @param {integer[]} [env.resIDs] the current ids in the environment where the action is triggered
* @param {integer} [env.currentID] the id of the record triggering the action
* @param {Object} [env.context] a context to pass to the action
* @param {Function} on_closed callback to execute when dialog is closed or when the action does not generate any result (no new action)
*/
do_execute_action: function (action_data, model, res_ids, on_closed) {
do_execute_action: function (action_data, env, on_closed) {
var self = this;
var result_handler = on_closed || function () {};
var context = new Context(this.env.context, action_data.context || {});
var context = new Context(env.context, action_data.context || {});
// OR NULL hereunder: pyeval waits specifically for a null value, different from undefined
var record_id = res_ids && res_ids[0] || null;
var recordID = env.currentID || null;
// response handler
var handler = function (action) {
@@ -585,11 +588,11 @@ var ViewManager = Widget.extend(ControlPanelMixin, {
}))
);
ncontext.add(action_data.context || {});
ncontext.add({active_model: self.env.modelName});
if (res_ids) {
ncontext.add({active_model: env.model});
if (recordID) {
ncontext.add({
active_id: record_id,
active_ids: res_ids,
active_id: recordID,
active_ids: [recordID],
});
}
ncontext.add(action.context || {});
@@ -611,7 +614,7 @@ var ViewManager = Widget.extend(ControlPanelMixin, {
if (action_data.special) {
return handler({"type":"ir.actions.act_window_close"});
} else if (action_data.type === "object") {
var args = res_ids ? [res_ids] : [this.env.ids];
var args = recordID ? [[recordID]] : [env.resIDs];
if (action_data.args) {
try {
// Warning: quotes and double quotes problem due to json and xml clash
@@ -623,13 +626,13 @@ var ViewManager = Widget.extend(ControlPanelMixin, {
}
}
args.push(context);
var dataset = new data.DataSet(this, model, this.env.context);
var dataset = new data.DataSet(this, env.model, env.context);
return dataset.call_button(action_data.name, args).then(handler);
} else if (action_data.type === "action") {
return data_manager.load_action(action_data.name, _.extend(pyeval.eval('context', context), {
active_model: this.env.modelName,
active_ids: this.env.ids,
active_id: record_id,
active_model: env.model,
active_ids: env.resIDs,
active_id: recordID,
})).then(handler);
}
},
@@ -219,10 +219,16 @@ var FieldMany2One = AbstractField.extend({
/**
* @private
* @param {string} name
* @returns {Deferred} resolved after the name_create or when the slowcreate
* modal is closed.
*/
_quickCreate: function (name) {
var self = this;
var slowCreate = this._searchCreatePopup.bind(this, "form", false, this._createContext(name));
var def = $.Deferred();
var slowCreate = function () {
var dialog = self._searchCreatePopup.bind(self, "form", false, self._createContext(name));
dialog.on('closed', self, def.resolve.bind(def));
};
if (this.nodeOptions.quick_create) {
this.trigger_up('mutexify', {
action: function () {
@@ -235,12 +241,16 @@ var FieldMany2One = AbstractField.extend({
if (self.mode === "edit") {
self.reinitialize({id: result[0], display_name: result[1]});
}
}, slowCreate);
def.resolve();
}).fail(function () {
slowCreate();
});
},
});
} else {
slowCreate();
}
return def;
},
/**
* @private
@@ -29,6 +29,7 @@ var ajax = require('web.ajax');
var AbstractModel = require('web.AbstractModel');
var AbstractRenderer = require('web.AbstractRenderer');
var AbstractController = require('web.AbstractController');
var Context = require('web.Context');
var AbstractView = Class.extend({
// name displayed in view switchers
@@ -152,9 +153,9 @@ var AbstractView = Class.extend({
});
},
/**
* Returns the view model or create an instance of it if there is noney
* Returns the view model or create an instance of it if none
*
* @param {Widget} parent the parent of the model
* @param {Widget} parent the parent of the model, if it has to be created
* @return {Object} instance of the view model
*/
getModel: function (parent) {
@@ -167,7 +168,7 @@ var AbstractView = Class.extend({
/**
* Returns the a new view renderer instance
*
* @param {Widget} parent the parent of the model
* @param {Widget} parent the parent of the model, if it has to be created
* @param {Object} state the information related to the rendered view
* @return {Object} instance of the view renderer
*/
@@ -239,7 +240,7 @@ var AbstractView = Class.extend({
* Loads the subviews for x2many fields when they are not inline
*
* @private
* @param {Widget} parent the parent of the model
* @param {Widget} parent the parent of the model, if it has to be created
* @returns {Deferred}
*/
_loadSubviews: function (parent) {
@@ -232,13 +232,17 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
return self.isDestroyed() ? $.when() : self.reload();
};
record = record || this.model.get(this.handle);
var recordID = record.data.id;
this.trigger_up('execute_action', {
action_data: _.extend({}, attrs, {
context: record.getContext({additionalContext: attrs.context || {}}),
context: record.getContext({additionalContext: attrs.context || {}}),
}),
model: record.model,
res_ids: [recordID],
env: {
context: record.getContext(),
currentID: record.data.id,
model: record.model,
resIDs: record.res_ids,
},
on_closed: function (reason) {
if (!_.isObject(reason)) {
reload(reason);
@@ -649,7 +649,7 @@ var BasicModel = AbstractModel.extend({
var element = this.localData[id];
if (element.type === 'record') {
if ('currentId' in options && !options.currentId) {
if (!options.currentId && (('currentId' in options) || this.isNew(id))) {
var params = {
context: element.context,
fieldsInfo: element.fieldsInfo,
@@ -2310,12 +2310,20 @@ var BasicModel = AbstractModel.extend({
});
list = this._applyX2ManyOperations(list);
if (type === 'many2many' || list._forceM2MLink) {
var relRecordCreated = _.filter(relRecordAdded, function (rec) {
return typeof rec.res_id === 'string';
});
var realIDs = _.difference(list.res_ids, _.pluck(relRecordCreated, 'res_id'));
// deliberately generate a single 'replace' command instead
// of a 'delete' and a 'link' commands with the exact diff
// because 1) performance-wise it doesn't change anything
// and 2) to guard against concurrent updates (policy: force
// a complete override of the actual value of the m2m)
commands[fieldName].push(x2ManyCommands.replace_with(list.res_ids));
commands[fieldName].push(x2ManyCommands.replace_with(realIDs));
_.each(relRecordCreated, function (relRecord) {
var changes = self._generateChanges(relRecord, options);
commands[fieldName].push(x2ManyCommands.create(changes));
});
// generate update commands for records that have been
// updated (it may happen with editable lists)
_.each(relRecordUpdated, function (relRecord) {
@@ -2435,6 +2443,12 @@ var BasicModel = AbstractModel.extend({
*/
_getDomain: function (element, options) {
if (options && options.fieldName) {
if (element._domains[options.fieldName]) {
return Domain.prototype.stringToArray(
element._domains[options.fieldName],
this._getEvalContext(element, true)
);
}
var viewType = options.viewType || element.viewType;
var fieldInfo = element.fieldsInfo[viewType][options.fieldName];
if (fieldInfo && fieldInfo.domain) {
@@ -2697,6 +2711,7 @@ var BasicModel = AbstractModel.extend({
var dataPoint = {
_cache: type === 'list' ? {} : undefined,
_changes: null,
_domains: {},
aggregateValues: params.aggregateValues || {},
context: params.context || {},
count: params.count || res_ids.length,
@@ -2870,6 +2885,9 @@ var BasicModel = AbstractModel.extend({
r._changes = _.defaults(value[2], r.data);
for (var name in r._changes) {
if (r._changes[name] === null) {
continue;
}
var isFieldInView = name in r.fields;
if (isFieldInView && r.fields[name].type === 'many2one') {
var rec = self._makeDataPoint({
@@ -3021,12 +3039,7 @@ var BasicModel = AbstractModel.extend({
record._warning = true;
}
if (result.domain) {
var fieldsInfo = record.fieldsInfo[viewType || record.viewType];
for (var fieldName in result.domain) {
if (fieldsInfo[fieldName]) {
fieldsInfo[fieldName].domain = result.domain[fieldName];
}
}
record._domains = _.extend(record._domains, result.domain);
}
return self._applyOnChange(result.value, record).then(function () {
return result;
@@ -3083,7 +3096,9 @@ var BasicModel = AbstractModel.extend({
*/
_readGroup: function (list) {
var self = this;
var fields = _.uniq(list.getFieldNames().concat(list.groupedBy));
var groupByField = list.groupedBy[0];
var rawGroupBy = groupByField.split(':')[0];
var fields = _.uniq(list.getFieldNames().concat(rawGroupBy));
return this._rpc({
model: list.model,
method: 'read_group',
@@ -3094,8 +3109,6 @@ var BasicModel = AbstractModel.extend({
lazy: true,
})
.then(function (groups) {
var groupByField = list.groupedBy[0];
var rawGroupBy = groupByField.split(':')[0];
var previousGroups = _.map(list.data, function (groupID) {
return self.localData[groupID];
});
@@ -322,10 +322,11 @@ return AbstractRenderer.extend({
element.addClass($render.attr('class'));
var display_hour = '';
if (!event.allDay) {
display_hour = (event.start.format('HH:mm') === '00:00' ? event.r_start.format('HH:mm') : event.start.format('HH:mm')) + ' - ' +
(event.end && event.end.format('HH:mm') !== '00:00' ? event.end.format('HH:mm') : event.r_end.format('HH:mm'));
var start = event.r_start || event.start;
var end = event.r_end || event.end;
display_hour = start.format('HH:mm') + ' - ' + end.format('HH:mm');
if (display_hour === '00:00 - 00:00') {
display_hour = _t('All the day');
display_hour = _t('All day');
}
}
element.find('.fc-content .fc-time').text(display_hour);
@@ -217,7 +217,7 @@ var FormController = BasicController.extend({
*
* @private
* @override method from field manager mixin
* @param {string} id
* @param {string} id - id of the previously changed record
* @returns {Deferred}
*/
_confirmSave: function (id) {
@@ -228,15 +228,21 @@ var FormController = BasicController.extend({
return this._setMode('readonly');
}
} else {
// a subrecord changed, so update the corresponding relational field
// A subrecord has changed, so update the corresponding relational field
// i.e. the one whose value is a record with the given id or a list
// having a record with the given id in its data
var record = this.model.get(this.handle);
var fieldsChanged = _.findKey(record.data, function (d) {
return _.isObject(d) &&
(d.id === id || _.findWhere(d.data, {id: id}));
});
return this.renderer.confirmChange(record, record.id, [fieldsChanged]);
// Callback function which returns true
// if a value recursively contains a record with the given id.
// This will be used to determine the list of fields to reload.
var containsChangedRecord = function (value) {
return _.isObject(value) &&
(value.id === id || _.find(value.data, containsChangedRecord));
};
var changedFields = _.findKey(record.data, containsChangedRecord);
return this.renderer.confirmChange(record, record.id, [changedFields]);
}
},
/**
@@ -237,8 +237,12 @@ var KanbanController = BasicController.extend({
}
this.trigger_up('execute_action', {
action_data: attrs,
model: record.model,
res_ids: [record.res_id],
env: {
context: record.getContext(),
currentID: record.res_id,
model: record.model,
resIDs: record.res_ids,
},
on_closed: function () {
self.model.reload(record.id).then(function (db_id) {
var data = self.model.get(db_id);
@@ -240,7 +240,7 @@ var KanbanModel = BasicModel.extend({
*/
_readTooltipFields: function (list) {
var self = this;
var groupedByField = list.fields[list.groupedBy[0]];
var groupedByField = list.fields[list.groupedBy[0].split(':')[0]];
if (groupedByField.type !== 'many2one') {
return $.when();
}
@@ -281,15 +281,19 @@ var ListRenderer = BasicRenderer.extend({
this._registerModifiers(node, record, $button);
// TODO this should be moved to a handler
var self = this;
$button.on("click", function (e) {
e.stopPropagation();
self.trigger_up('button_clicked', {
attrs: node.attrs,
record: record,
if (record.res_id) {
// TODO this should be moved to a handler
var self = this;
$button.on("click", function (e) {
e.stopPropagation();
self.trigger_up('button_clicked', {
attrs: node.attrs,
record: record,
});
});
});
} else {
$button.prop('disabled', true);
}
return $button;
},
@@ -922,6 +922,140 @@ QUnit.module('relational_fields', {
});
});
QUnit.test('list in form: create with one2many with many2one', function (assert) {
assert.expect(1);
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form>' +
'<sheet>' +
'<field name="p">' +
'<tree editable="bottom">' +
'<field name="display_name"/>' +
'<field name="trululu"/>' +
'</tree>' +
'</field>' +
'</sheet>' +
'</form>',
mockRPC: function (route, args) {
if (args.method === 'default_get') {
return $.when({p: [[0, 0, {display_name: 'new record'}]]});
} else if (args.method === 'name_get') {
// This should not be called at all and thus is not accounted for
// in the assert.expect. If this is called, you broke this test.
assert.notOk(_.str.startsWith(args.args[0][0], 'virtual_'),
"should not call name_get for the m2o inside o2m which has no value");
}
return this._super.apply(this, arguments);
},
});
assert.strictEqual($('td.o_data_cell:first').text(), 'new record',
"should have created the new record in the o2m with the correct name");
form.destroy();
});
QUnit.test('list in form: default_get with x2many create', function (assert) {
assert.expect(3);
this.data.partner.onchanges.timmy = function (obj) {
assert.deepEqual(
obj.timmy,
[
[6, false, []],
[0, false, {display_name: 'brandon is the new timmy', name: 'brandon'}]
],
"should have properly created the x2many command list");
obj.int_field = obj.timmy.length;
};
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form>' +
'<sheet>' +
'<field name="timmy">' +
'<tree editable="bottom">' +
'<field name="display_name"/>' +
'</tree>' +
'</field>' +
'<field name="int_field"/>' +
'</sheet>' +
'</form>',
mockRPC: function (route, args) {
if (args.method === 'default_get') {
return $.when({timmy: [[0, 0, {display_name: 'brandon is the new timmy', name: 'brandon'}]]});
}
return this._super.apply(this, arguments);
},
});
assert.strictEqual($('td.o_data_cell:first').text(), 'brandon is the new timmy',
"should have created the new record in the m2m with the correct name");
assert.strictEqual($('input.o_field_integer').val(), '2',
"should have called and executed the onchange properly");
form.destroy();
});
QUnit.test('list in form: call button in sub view', function (assert) {
assert.expect(6);
this.data.partner.records[0].p = [2];
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form>' +
'<sheet>' +
'<field name="p">' +
'<tree editable="bottom">' +
'<field name="product_id"/>' +
'</tree>' +
'</field>' +
'</sheet>' +
'</form>',
res_id: 1,
mockRPC: function (route, args) {
if (route === '/web/dataset/call_kw/product/get_formview_id') {
return $.when(false);
}
return this._super.apply(this, arguments);
},
intercepts: {
execute_action: function (event) {
assert.strictEqual(event.data.env.model, 'product',
'should call with correct model in env');
assert.strictEqual(event.data.env.currentID, 37,
'should call with correct currentID in env');
assert.deepEqual(event.data.env.resIDs, [37],
'should call with correct resIDs in env');
},
},
archs: {
'product,false,form': '<form string="Partners">' +
'<header>' +
'<button name="action" type="action" string="Just do it !"/>' +
'<button name="object" type="object" string="Just don\'t do it !"/>' +
'<field name="display_name"/>' +
'</header>' +
'</form>',
},
});
form.$buttons.find('.o_form_button_edit').click();
form.$('td.o_data_cell:first').click(); // edit first one2many line
form.$('.o_external_button').click(); // open product sub view in modal
$('button:contains("Just do it !")').click(); // click on action button
$('button:contains("Just don\'t do it !")').click(); // click on object button
form.destroy();
});
QUnit.test('autocompletion in a many2one, in form view with a domain', function (assert) {
assert.expect(1);
@@ -1418,6 +1552,115 @@ QUnit.module('relational_fields', {
form.destroy();
});
QUnit.test('many2one in one2many: domain updated by an onchange', function (assert) {
assert.expect(3);
this.data.partner.onchanges = {
trululu: function () {},
};
var domain = [];
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form>' +
'<field name="p">' +
'<tree editable="bttom">' +
'<field name="trululu"/>' +
'</tree>' +
'</field>' +
'</form>',
res_id: 1,
mockRPC: function (route, args) {
if (args.method === 'onchange') {
return $.when({
domain: {
trululu: domain,
},
});
}
if (args.method === 'name_search') {
assert.deepEqual(args.kwargs.args, domain,
"sent domain should be correct");
}
return this._super(route, args);
},
viewOptions: {
mode: 'edit',
},
});
// add a first row with a specific domain for the m2o
domain = [['id', 'in', [10]]]; // domain for subrecord 1
form.$('.o_field_x2many_list_row_add a').click(); // triggers the onchange
form.$('.o_field_widget[name=trululu] input').click(); // triggers the name_search
// add a second row with another domain for the m2o
domain = [['id', 'in', [5]]]; // domain for subrecord 2
form.$('.o_field_x2many_list_row_add a').click(); // triggers the onchange
form.$('.o_field_widget[name=trululu] input').click(); // triggers the name_search
// check again the first row to ensure that the domain hasn't change
domain = [['id', 'in', [10]]]; // domain for subrecord 1 should have been kept
form.$('.o_data_row:first .o_data_cell').click();
form.$('.o_field_widget[name=trululu] input').click(); // triggers the name_search
form.destroy();
});
QUnit.test('updating a many2one from a many2many', function (assert) {
assert.expect(4);
this.data.turtle.records[1].turtle_trululu = 1;
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<group>' +
'<field name="turtles">' +
'<tree editable="bottom">' +
'<field name="display_name"/>' +
'<field name="turtle_trululu"/>' +
'</tree>' +
'</field>' +
'</group>' +
'</form>',
res_id: 1,
archs: {
'partner,false,form': '<form string="Trululu"><field name="display_name"/></form>',
},
mockRPC: function (route, args) {
if (args.method === 'get_formview_id') {
assert.deepEqual(args.args[0], [1], "should call get_formview_id with correct id");
return $.when(false);
}
return this._super(route, args);
},
});
// Opening the modal
form.$buttons.find('.o_form_button_edit').click();
form.$('.o_data_row td:contains(first record)').click();
form.$('.o_external_button').click();
assert.strictEqual($('.modal').length, 1,
"should have one modal in body");
// Changing the 'trululu' value
$('.modal input[name="display_name"]').val('test').trigger('input');
$('.modal button.btn-primary').click();
// Test whether the value has changed
assert.strictEqual($('.modal').length, 0,
"the modal should be closed");
assert.equal(form.$('.o_data_cell:contains(test)').text(), 'test',
"the partner name should have been updated to 'test'");
form.destroy();
});
QUnit.module('FieldOne2Many');
QUnit.test('one2many basic properties', function (assert) {
@@ -3847,9 +4090,9 @@ QUnit.module('relational_fields', {
res_id: 1,
intercepts: {
execute_action: function (event) {
assert.deepEqual(event.data.res_ids, [2],
assert.deepEqual(event.data.env.currentID, 2,
'should call with correct id');
assert.strictEqual(event.data.model, 'partner',
assert.strictEqual(event.data.env.model, 'partner',
'should call with correct model');
assert.strictEqual(event.data.action_data.name, 'method_name',
"should call correct method");
@@ -3891,9 +4134,9 @@ QUnit.module('relational_fields', {
res_id: 1,
intercepts: {
execute_action: function (event) {
assert.deepEqual(event.data.res_ids, [2],
assert.deepEqual(event.data.env.currentID, 2,
'should call with correct id');
assert.strictEqual(event.data.model, 'partner',
assert.strictEqual(event.data.env.model, 'partner',
'should call with correct model');
assert.strictEqual(event.data.action_data.name, 'method_name',
"should call correct method");
@@ -956,6 +956,30 @@ QUnit.module('Views', {
model.destroy();
});
QUnit.test('group on date field with magic grouping method', function (assert) {
assert.expect(1);
this.params.fieldNames = ['foo'];
this.params.groupedBy = ['date:month'];
this.params.res_id = undefined;
var model = createModel({
Model: BasicModel,
data: this.data,
mockRPC: function (route, args) {
if (args.method === 'read_group') {
assert.deepEqual(args.kwargs.fields, ['foo', 'date'],
"should have correctly trimmed the magic grouping info from the field name");
}
return this._super.apply(this, arguments);
},
});
model.load(this.params);
model.destroy();
});
QUnit.test('read group when grouped by a selection field', function (assert) {
assert.expect(5);
@@ -1949,4 +1973,39 @@ QUnit.module('Views', {
model.destroy();
});
QUnit.test('reload a new record', function (assert) {
assert.expect(6);
this.params.context = {};
this.params.fieldNames = ['product_id', 'category', 'product_ids'];
this.params.res_id = undefined;
this.params.type = 'record';
var model = createModel({
Model: BasicModel,
data: this.data,
mockRPC: function (route, args) {
assert.step(args.method);
return this._super(route, args);
},
});
model.load(this.params).then(function (recordID) {
model.reload(recordID).then(function (recordID) {
assert.verifySteps(['default_get', 'default_get'],
"two default_get RPCs should have been done");
var record = model.get(recordID);
assert.strictEqual(record.data.product_id, false,
"m2o default value should be false");
assert.deepEqual(record.data.product_ids.data, [],
"o2m default should be []");
assert.deepEqual(record.data.category.data, [],
"m2m default should be []");
});
});
model.destroy();
});
});});
@@ -789,7 +789,7 @@ QUnit.module('Views', {
assert.strictEqual($event1Avatars.find('span').length, 0,
"should not have a span for more attendees since there is only one");
var $event2Avatars = calendar.$('.fc-event:contains(All the day) .o_calendar_avatars');
var $event2Avatars = calendar.$('.fc-event:contains(All day) .o_calendar_avatars');
assert.strictEqual($event2Avatars.find('img').length, 3, "should have 3 avatars");
assert.strictEqual($event2Avatars.find('span').text(), '+2',
"should indicate there are 2 more attendees that we don't show");
@@ -1034,6 +1034,46 @@ QUnit.module('Views', {
"should display 5 events");
calendar.destroy();
});
QUnit.test('events starting at midnight', function (assert) {
assert.expect(2);
var calendar = createView({
View: CalendarView,
model: 'event',
data: this.data,
arch: '<calendar mode="week" date_start="start"/>',
viewOptions: {
initialDate: initialDate,
},
});
// Click on Tuesday 12am
var $view = $('#qunit-fixture').contents();
$view.prependTo('body');
var top = calendar.$('.fc-axis:contains(12am)').offset().top + 5;
var left = calendar.$('.fc-day:eq(2)').offset().left + 5;
try {
testUtils.triggerPositionalMouseEvent(left, top, "mousedown");
testUtils.triggerPositionalMouseEvent(left, top, "mouseup");
} catch (e) {
calendar.destroy();
$view.remove();
throw new Error('The test failed to simulate a click on the screen.' +
'Your screen is probably too small or your dev tools are open.');
}
assert.ok($('.modal-dialog.modal-sm').length,
"should open the quick create dialog");
// Creating the event
$('.modal-body input:first').val('new event in quick create').trigger('input');
$('.modal button.btn:contains(Create)').trigger('click').trigger('click');
assert.strictEqual(calendar.$('.fc-event:contains(new event in quick create)').length, 1,
"should display the new record");
calendar.destroy();
$view.remove();
});
});
});
+8 -7
View File
@@ -903,7 +903,7 @@ QUnit.module('Views', {
testUtils.intercept(form, 'execute_action', function (event) {
assert.strictEqual(event.data.action_data.name, "post",
"should trigger execute_action with correct method name");
assert.deepEqual(event.data.res_ids, [2], "should have correct id in event data");
assert.deepEqual(event.data.env.currentID, 2, "should have correct id in event data");
event.data.on_success();
event.data.on_closed();
});
@@ -956,7 +956,7 @@ QUnit.module('Views', {
testUtils.intercept(form, 'execute_action', function (event) {
assert.step('execute_action');
assert.deepEqual(event.data.res_ids, [resID],
assert.deepEqual(event.data.env.currentID, resID,
"execute action should be done on correct record id");
event.data.on_success();
event.data.on_closed();
@@ -1006,7 +1006,7 @@ QUnit.module('Views', {
testUtils.intercept(form, 'execute_action', function (event) {
assert.step('execute_action');
assert.deepEqual(event.data.res_ids, [resID],
assert.deepEqual(event.data.env.currentID, resID,
"execute action should be done on correct record id");
event.data.on_success();
event.data.on_closed();
@@ -3597,8 +3597,9 @@ QUnit.module('Views', {
},
intercepts: {
execute_action: function (e) {
assert.deepEqual(e.data.action_data.context, {test: 2},
"button context should have been evaluated and given to the action, without previous context");
assert.deepEqual(e.data.action_data.context, {
'test': 2
}, "button context should have been evaluated and given to the action, with magicc without previous context");
},
},
});
@@ -3631,8 +3632,8 @@ QUnit.module('Views', {
},
intercepts: {
execute_action: function (e) {
assert.deepEqual(e.data.action_data.context, {},
"button context should have been evaluated and given to the action, without previous context");
assert.deepEqual(e.data.action_data.context, {
}, "button context should have been evaluated and given to the action, with magic keys but without previous context");
},
},
});
+11 -3
View File
@@ -984,7 +984,7 @@ QUnit.module('Views', {
});
QUnit.test('list view, editable, without data', function (assert) {
assert.expect(9);
assert.expect(11);
this.data.foo.records = [];
@@ -998,6 +998,7 @@ QUnit.module('Views', {
'<field name="date"/>' +
'<field name="m2o"/>' +
'<field name="foo"/>' +
'<button type="object" icon="fa-plus-square" name="method"/>' +
'</tree>',
viewOptions: {
action: {
@@ -1033,7 +1034,14 @@ QUnit.module('Views', {
assert.strictEqual(list.$('tbody tr:eq(0) td:eq(1)').text().trim(), "",
"the date field td should not have any content");
assert.strictEqual(list.$('.o_list_button button').prop('disabled'), true,
"buttons should be disabled while the record is not yet created");
list.$buttons.find('.o_list_button_save').click();
assert.strictEqual(list.$('.o_list_button button').prop('disabled'), false,
"buttons should not be disabled once the record is created");
list.destroy();
});
@@ -1163,9 +1171,9 @@ QUnit.module('Views', {
},
intercepts: {
execute_action: function (event) {
assert.deepEqual(event.data.res_ids, [1],
assert.deepEqual(event.data.env.currentID, 1,
'should call with correct id');
assert.strictEqual(event.data.model, 'foo',
assert.strictEqual(event.data.env.model, 'foo',
'should call with correct model');
assert.strictEqual(event.data.action_data.name, 'button_action',
"should call correct method");
+1 -1
View File
@@ -383,7 +383,7 @@ var FieldTextHtml = AbstractField.extend({
* @override
*/
commitChanges: function () {
if (!this.loaded) {
if (!this.loaded || this.mode === 'readonly') {
return;
}
// switch to WYSIWYG mode if currently in code mode to get all changes
@@ -188,4 +188,34 @@ QUnit.test('field htmlsimple does not crash when commitChanges is called in mode
form.destroy();
});
QUnit.test('html_frame does not crash when saving in readonly', function (assert) {
// The 'Save' action may be triggered even in readonly (e.g. when clicking
// on a button in the form view)
assert.expect(0);
var form = testUtils.createView({
View: FormView,
model: 'mass.mailing',
data: this.data,
arch: '<form string="Partners">' +
'<sheet>' +
'<field name="body" widget="html_frame" options="{\'editor_url\': \'/test\'}"/>' +
'</sheet>' +
'</form>',
res_id: 1,
mockRPC: function (route) {
if (_.str.startsWith(route, '/test')) {
// manually call the callback to simulate that the iframe has
// been correctly loaded
window.odoo[$.deparam(route).callback + '_content'].call();
return $.when();
}
return this._super.apply(this, arguments);
},
});
form.saveRecord(); // before the fix done in this commit, it crashed here
form.destroy();
});
});
@@ -416,8 +416,9 @@ animation.registry.media_video = animation.Class.extend({
if (!this.$target.has('> iframe').length) {
var editor = '<div class="css_editable_mode_display">&nbsp;</div>';
var size = '<div class="media_iframe_video_size">&nbsp;</div>';
this.$target.html(editor+size+'<iframe src="'+_.escape(this.$target.data("src"))+'" frameborder="0" allowfullscreen="allowfullscreen"></iframe>');
this.$target.html(editor+size);
}
this.$target.html(this.$target.html()+'<iframe src="'+_.escape(this.$target.data("src"))+'" frameborder="0" allowfullscreen="allowfullscreen"></iframe>');
return this._super.apply(this, arguments);
},
});
@@ -9,7 +9,7 @@
<form string="Configure Website" class="oe_form_configuration">
<header>
<button string="Save" type="object" name="execute" class="oe_highlight"/>
<button string="Cancel" type="object" name="cancel" class="oe_link"/>
<button string="Cancel" type="object" name="cancel" class="oe_link" special="cancel"/>
</header>
<sheet>
<div>
+2 -1
View File
@@ -170,6 +170,7 @@ class WebsiteEventController(http.Controller):
'event': event,
'main_object': event,
'range': range,
'registrable': event._is_event_registrable()
}
return request.render("website_event.event_description_full", values)
@@ -226,7 +227,7 @@ class WebsiteEventController(http.Controller):
def registration_new(self, event, **post):
tickets = self._process_tickets_details(post)
if not tickets:
return request.redirect("/event/%s" % slug(event))
return False
return request.env['ir.ui.view'].render_template("website_event.registration_attendee_details", {'tickets': tickets, 'event': event})
def _process_registration_details(self, details):
@@ -311,7 +311,8 @@
<template id="event_description_full">
<t t-call="website_event.event_details">
<div class="col-md-8">
<t t-call="website_event.registration_template"/>
<div t-if="event.state == 'done' or not registrable" class="alert alert-info">Registration for this event is now closed</div>
<t t-if="event.state != 'done' and registrable" t-call="website_event.registration_template"/>
<div class="clearfix"/>
<hr/>
<div itemprop="description" t-field="event.description"></div>
@@ -12,12 +12,7 @@ class WebsiteEventSaleController(WebsiteEventController):
@http.route(['/event/<model("event.event"):event>/register'], type='http', auth="public", website=True)
def event_register(self, event, **post):
event = event.with_context(pricelist=request.website.get_current_pricelist().id)
values = {
'event': event,
'main_object': event,
'range': range,
}
return request.render("website_event.event_description_full", values)
return super(WebsiteEventSaleController, self).event_register(event, **post)
def _process_tickets_details(self, data):
ticket_post = {}
+2 -1
View File
@@ -357,7 +357,8 @@ class WebsiteSale(http.Controller):
values['suggested_products'] = _order._cart_accessories()
if post.get('type') == 'popover':
return request.render("website_sale.cart_popover", values)
# force no-cache so IE11 doesn't cache this XHR
return request.render("website_sale.cart_popover", values, headers={'Cache-Control': 'no-cache'})
return request.render("website_sale.cart", values)
+5 -5
View File
@@ -1166,11 +1166,11 @@
<div class="clearfix" />
<t t-if="mode == ('new', 'billing')">
<div t-attf-class="form-group #{error.get('company_name') and 'has-error' or ''} col-md-6">
<label class="control-label font-weight-normal" for="company_name">Company Name</label>
<label class="control-label font-weight-normal label-optional" for="company_name">Company Name</label>
<input type="text" name="company_name" class="form-control" t-att-value="'company_name' in checkout and checkout['company_name']" />
</div>
<div t-attf-class="form-group #{error.get('vat') and 'has-error' or ''} col-md-6 div_vat">
<label class="control-label font-weight-normal" for="vat">TIN / VAT </label>
<label class="control-label font-weight-normal label-optional" for="vat">TIN / VAT </label>
<input type="text" name="vat" class="form-control" t-att-value="'vat' in checkout and checkout['vat']" />
</div>
</t>
@@ -1180,14 +1180,14 @@
<input type="text" name="street" class="form-control" t-att-value="'street' in checkout and checkout['street']" />
</div>
<div t-attf-class="form-group #{error.get('street2') and 'has-error' or ''} col-md-12 div_street2">
<label class="control-label" for="street2">Street 2</label>
<label class="control-label label-optional" for="street2">Street 2</label>
<input type="text" name="street2" class="form-control" t-att-value="'street2' in checkout and checkout['street2']" />
</div>
<div class="clearfix" />
<t t-set='zip_city' t-value='country and [x for x in country.get_address_fields() if x in ["zip", "city"]] or ["city", "zip"]'/>
<t t-if="'zip' in zip_city and zip_city.index('zip') &lt; zip_city.index('city')">
<div t-attf-class="form-group #{error.get('zip') and 'has-error' or ''} col-sm-4 div_zip">
<label class="control-label" for="zip">Zip Code</label>
<label class="control-label label-optional" for="zip">Zip Code</label>
<input type="text" name="zip" class="form-control" t-att-value="'zip' in checkout and checkout['zip']" />
</div>
</t>
@@ -1197,7 +1197,7 @@
</div>
<t t-if="'zip' in zip_city and zip_city.index('zip') &gt; zip_city.index('city')">
<div t-attf-class="form-group #{error.get('zip') and 'has-error' or ''} col-sm-4 div_zip">
<label class="control-label" for="zip">Zip Code</label>
<label class="control-label label-optional" for="zip">Zip Code</label>
<input type="text" name="zip" class="form-control" t-att-value="'zip' in checkout and checkout['zip']" />
</div>
</t>
+14
View File
@@ -0,0 +1,14 @@
Finland, 2017-08-01
Vizucom Oy agrees to the terms of the Odoo Corporate Contributor License Agreement v1.0.
We declare that we are authorized and able to make this agreement and sign this declaration.
Signed,
Timo Talvitie timo.talvitie@vizucom.com https://github.com/timotalvitie
List of contributors:
Timo Talvitie timo.talvitie@vizucom.com https://github.com/timotalvitie
Henri Alasentie henri.alasentie@vizucom.com https://github.com/halasentie
+1 -1
View File
@@ -188,7 +188,7 @@ class CurrencyRate(models.Model):
rate = fields.Float(digits=(12, 6), help='The rate of the currency to the currency of rate 1')
currency_id = fields.Many2one('res.currency', string='Currency', readonly=True)
company_id = fields.Many2one('res.company', string='Company',
default=lambda self: self.env.user._get_company())
default=lambda self: self.env.user.company_id)
_sql_constraints = [
('unique_name_per_day', 'unique (name,currency_id,company_id)', 'Only one currency rate per day allowed!'),
@@ -130,7 +130,7 @@ tour.register('main_flow_tour', {
}, {
// Add second component
trigger: ".o_field_x2many_list_row_add > a",
in_modal: false,
extra_trigger: "body:not(.modal-open)",
content: _t("Click here to add some lines."),
position: "bottom",
}, {
+2 -1
View File
@@ -563,7 +563,7 @@ class Field(MetaField('DummyField', (object,), {})):
""" Traverse the fields of the related field `self` except for the last
one, and return it as a pair `(last_record, last_field)`. """
for name in self.related[:-1]:
record = record[name][:1]
record = record[name][:1].with_prefetch(record._prefetch)
return record, self.related_field
def _compute_related(self, records):
@@ -1028,6 +1028,7 @@ class Field(MetaField('DummyField', (object,), {})):
self.compute_value(record)
else:
recs = record._in_cache_without(self)
recs = recs.with_prefetch(record._prefetch)
self.compute_value(recs)
else:
+4 -2
View File
@@ -275,8 +275,9 @@ class WebRequest(object):
if self._cr:
if exc_type is None and not self._failed:
self._cr.commit()
self.registry.signal_changes()
else:
if self.registry:
self.registry.signal_changes()
elif self.registry:
self.registry.reset_changes()
self._cr.close()
# just to be sure no one tries to re-use the request
@@ -1426,6 +1427,7 @@ class Root(object):
httprequest = werkzeug.wrappers.Request(environ)
httprequest.app = self
httprequest.parameter_storage_class = werkzeug.datastructures.ImmutableOrderedMultiDict
threading.current_thread().url = httprequest.url
explicit_session = self.setup_session(httprequest)
self.setup_db(httprequest)
+8 -9
View File
@@ -2551,10 +2551,6 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
else:
records &= self._in_cache_without(f)
# prefetch at most PREFETCH_MAX records
if len(records) > PREFETCH_MAX:
records = records[:PREFETCH_MAX] | self
# fetch records with read()
assert self in records and field in fs
records = records.with_prefetch(self._prefetch)
@@ -4667,13 +4663,16 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
return RecordCache(self)
@api.model
def _in_cache_without(self, field):
""" Make sure ``self`` is present in cache (for prefetching), and return
the records of model ``self`` in cache that have no value for ``field``
(:class:`Field` instance).
def _in_cache_without(self, field, limit=PREFETCH_MAX):
""" Return records to prefetch that have no value in cache for ``field``
(:class:`Field` instance), including ``self``.
Return at most ``limit`` records.
"""
ids = [it for it in self._prefetch[self._name] - set(self.env.cache[field]) if it]
return self.browse(ids)
recs = self.browse(ids)
if limit and len(recs) > limit:
recs = self + (recs - self)[:(limit - len(self))]
return recs
@api.model
def refresh(self):
+2
View File
@@ -156,6 +156,8 @@ def application_unproxied(environ, start_response):
del threading.current_thread().uid
if hasattr(threading.current_thread(), 'dbname'):
del threading.current_thread().dbname
if hasattr(threading.current_thread(), 'url'):
del threading.current_thread().url
with odoo.api.Environment.manage():
# Try all handlers until one returns some result (i.e. not None).
+7 -1
View File
@@ -409,11 +409,17 @@ class HttpCase(TransactionCase):
t0 = int(time.time())
for thread in threading.enumerate():
if thread.name.startswith('odoo.service.http.request.'):
thread.join_retry_count = 10
while thread.isAlive():
# Need a busyloop here as thread.join() masks signals
# and would prevent the forced shutdown.
thread.join(0.05)
time.sleep(0.05)
thread.join_retry_count -= 1
if thread.join_retry_count < 0:
_logger.warning("Stop waiting for thread %s handling request for url %s",
thread.name, thread.url)
break
time.sleep(0.5)
t1 = int(time.time())
if t0 != t1:
_logger.info('remaining requests')
+5 -3
View File
@@ -907,15 +907,17 @@ def dumpstacks(sig=None, frame=None):
# modified for python 2.5 compatibility
threads_info = {th.ident: {'name': th.name,
'uid': getattr(th, 'uid', 'n/a'),
'dbname': getattr(th, 'dbname', 'n/a')}
'dbname': getattr(th, 'dbname', 'n/a'),
'url': getattr(th, 'url', 'n/a')}
for th in threading.enumerate()}
for threadId, stack in pycompat.items(sys._current_frames()):
thread_info = threads_info.get(threadId, {})
code.append("\n# Thread: %s (id:%s) (db:%s) (uid:%s)" %
code.append("\n# Thread: %s (id:%s) (db:%s) (uid:%s) (url:%s)" %
(thread_info.get('name', 'n/a'),
threadId,
thread_info.get('dbname', 'n/a'),
thread_info.get('uid', 'n/a')))
thread_info.get('uid', 'n/a'),
thread_info.get('url', 'n/a')))
for line in extract_stack(stack):
code.append(line)