[MERGE] forward port branch 11.0 up to 3b80a059ff
This commit is contained in:
@@ -9,17 +9,19 @@ from odoo.tools import consteq
|
||||
|
||||
|
||||
class PortalAccount(CustomerPortal):
|
||||
|
||||
def _prepare_portal_layout_values(self):
|
||||
values = super(PortalAccount, self)._prepare_portal_layout_values()
|
||||
|
||||
def _get_account_invoice_domain(self):
|
||||
partner = request.env.user.partner_id
|
||||
|
||||
invoice_count = request.env['account.invoice'].search_count([
|
||||
domain = [
|
||||
('type', 'in', ['out_invoice', 'out_refund']),
|
||||
('message_partner_ids', 'child_of', [partner.commercial_partner_id.id]),
|
||||
('state', 'in', ['open', 'paid', 'cancel'])
|
||||
])
|
||||
]
|
||||
return domain
|
||||
|
||||
def _prepare_portal_layout_values(self):
|
||||
values = super(PortalAccount, self)._prepare_portal_layout_values()
|
||||
invoice_count = request.env['account.invoice'].search_count(self._get_account_invoice_domain())
|
||||
values['invoice_count'] = invoice_count
|
||||
return values
|
||||
|
||||
@@ -62,11 +64,7 @@ class PortalAccount(CustomerPortal):
|
||||
partner = request.env.user.partner_id
|
||||
AccountInvoice = request.env['account.invoice']
|
||||
|
||||
domain = [
|
||||
('type', 'in', ['out_invoice', 'out_refund']),
|
||||
('message_partner_ids', 'child_of', [partner.commercial_partner_id.id]),
|
||||
('state', 'in', ['open', 'paid', 'cancelled'])
|
||||
]
|
||||
domain = self._get_account_invoice_domain()
|
||||
|
||||
searchbar_sortings = {
|
||||
'date': {'label': _('Invoice Date'), 'order': 'date_invoice desc'},
|
||||
|
||||
@@ -359,7 +359,11 @@ var ManualAction = StatementAction.extend({
|
||||
title: self.title,
|
||||
time: Date.now()-self.time,
|
||||
});
|
||||
self._openFirstLine();
|
||||
if(!_.any(result.updated, function (handle) {
|
||||
return self.model.getLine(handle).mode !== 'inactive';
|
||||
})) {
|
||||
self._openFirstLine();
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -501,7 +501,7 @@ var StatementModel = BasicModel.extend({
|
||||
var amount = line.balance.amount;
|
||||
var amount_str = _.str.sprintf('%.2f', Math.abs(amount));
|
||||
amount_str = (amount > '0' ? '-' : '+') + amount_str;
|
||||
if (line.balance.amount_currency) {
|
||||
if (line.balance.currency_id && line.balance.amount_currency) {
|
||||
var amount_currency = line.balance.amount_currency;
|
||||
var amount_currency_str = _.str.sprintf('%.2f', Math.abs(amount_currency));
|
||||
amount_str += '|' + (amount_currency > '0' ? '-' : '+') + amount_currency_str;
|
||||
@@ -821,14 +821,13 @@ var StatementModel = BasicModel.extend({
|
||||
amount: total,
|
||||
amount_str: field_utils.format.monetary(Math.abs(total), {}, formatOptions),
|
||||
currency_id: isOtherCurrencyId,
|
||||
amount_currency: isOtherCurrencyId ? amount_currency : false,
|
||||
amount_currency: isOtherCurrencyId ? amount_currency : total,
|
||||
amount_currency_str: isOtherCurrencyId ? field_utils.format.monetary(Math.abs(amount_currency), {}, {
|
||||
currency_id: isOtherCurrencyId
|
||||
}) : false,
|
||||
account_code: self.accounts[line.st_line.open_balance_account_id],
|
||||
};
|
||||
line.balance.type = (isOtherCurrencyId && amount_currency || line.balance.amount) ?
|
||||
(line.balance.amount > 0 && line.st_line.partner_id ? 0 : -1) : 1;
|
||||
line.balance.type = line.balance.amount_currency ? (line.balance.amount_currency > 0 && line.st_line.partner_id ? 0 : -1) : 1;
|
||||
});
|
||||
},
|
||||
/**
|
||||
@@ -1295,7 +1294,7 @@ var ManualModel = StatementModel.extend({
|
||||
return this._super(line).then(function () {
|
||||
var props = _.reject(line.reconciliation_proposition, 'invalid');
|
||||
line.balance.type = -1;
|
||||
if (!line.balance.amount && props.length) {
|
||||
if (!line.balance.amount_currency && props.length) {
|
||||
line.balance.type = 1;
|
||||
} else if(_.any(props, function (prop) {return prop.amount > 0;}) &&
|
||||
_.any(props, function (prop) {return prop.amount < 0;})) {
|
||||
|
||||
@@ -363,7 +363,7 @@ var LineRenderer = Widget.extend(FieldManagerMixin, {
|
||||
.attr("data-content", qweb.render('reconciliation.line.mv_line.details', {'line': line}));
|
||||
}
|
||||
|
||||
if ((state.balance.amount !== 0 || line.partial_reconcile) && props.length === 1 &&
|
||||
if ((state.balance.amount_currency !== 0 || line.partial_reconcile) && props.length === 1 &&
|
||||
line.already_paid === false &&
|
||||
(
|
||||
(state.st_line.amount > 0 && state.st_line.amount < props[0].amount) ||
|
||||
@@ -425,7 +425,7 @@ var LineRenderer = Widget.extend(FieldManagerMixin, {
|
||||
}
|
||||
});
|
||||
}
|
||||
this.$('.create .add_line').toggle(!!state.balance.amount);
|
||||
this.$('.create .add_line').toggle(!!state.balance.amount_currency);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
@@ -124,13 +124,13 @@
|
||||
</t>
|
||||
|
||||
<t t-name="reconciliation.line.balance">
|
||||
<tr t-if="state.balance.amount && !(state.reconciliation_proposition[0] || {}).partial_reconcile">
|
||||
<tr t-if="state.balance.amount_currency && !(state.reconciliation_proposition[0] || {}).partial_reconcile">
|
||||
<td class="cell_action"><span class="toggle_create fa fa-play"></span></td>
|
||||
<td class="cell_account_code"><t t-esc="state.balance.account_code"/></td>
|
||||
<td class="cell_due_date"></td>
|
||||
<td class="cell_label"><t t-if="state.balance.amount < 0">Create Write-off</t><t t-elif="state.st_line.partner_id">Open balance</t><t t-else="">Choose counterpart</t></td>
|
||||
<td class="cell_left"><t t-if="state.balance.amount < 0"><span t-if="state.balance.amount_currency_str" t-attf-class="o_multi_currency o_multi_currency_color_#{state.balance.currency_id%8} line_info_button fa fa-money" t-att-data-content="state.balance.amount_currency_str"/><t t-raw="state.balance.amount_str"/></t></td>
|
||||
<td class="cell_right"><t t-if="state.balance.amount > 0"><span t-if="state.balance.amount_currency_str" t-attf-class="o_multi_currency o_multi_currency_color_#{state.balance.currency_id%8} line_info_button fa fa-money" t-att-data-content="state.balance.amount_currency_str"/><t t-raw="state.balance.amount_str"/></t></td>
|
||||
<td class="cell_label"><t t-if="state.balance.amount_currency < 0">Create Write-off</t><t t-elif="state.st_line.partner_id">Open balance</t><t t-else="">Choose counterpart</t></td>
|
||||
<td class="cell_left"><t t-if="state.balance.amount_currency < 0"><span t-if="state.balance.amount_currency_str" t-attf-class="o_multi_currency o_multi_currency_color_#{state.balance.currency_id%8} line_info_button fa fa-money" t-att-data-content="state.balance.amount_currency_str"/><t t-raw="state.balance.amount_str"/></t></td>
|
||||
<td class="cell_right"><t t-if="state.balance.amount_currency > 0"><span t-if="state.balance.amount_currency_str" t-attf-class="o_multi_currency o_multi_currency_color_#{state.balance.currency_id%8} line_info_button fa fa-money" t-att-data-content="state.balance.amount_currency_str"/><t t-raw="state.balance.amount_str"/></t></td>
|
||||
<td class="cell_info_popover"></td>
|
||||
</tr>
|
||||
</t>
|
||||
|
||||
@@ -448,7 +448,10 @@ var move_lines_for_manual_reconciliation = {
|
||||
'[7,12,"",0,6]': [
|
||||
{'account_type': "receivable", 'account_id': [287, "101200 Account Receivable"], 'amount_currency_str': "", 'currency_id': false, 'date_maturity': "2017-02-08", 'date': "2017-02-08", 'total_amount_str': "$ 11,000.00", 'partner_id': 12, 'account_name': "101200 Account Receivable", 'name': "INV/2017/0004: Customer Payment", 'partner_name': "Camptocamp", 'total_amount_currency_str': "", 'id': 17, 'credit': 11000.0, 'journal_id': [1, "Customer Invoices"], 'amount_str': "$ 11,000.00", 'debit': 0.0, 'account_code': "101200", 'ref': "", 'already_paid': false},
|
||||
{'account_type': "receivable", 'account_id': [7, "101200 Account Receivable"], 'amount_currency_str': "", 'currency_id': false, 'date_maturity': "2017-02-09", 'date': "2017-02-09", 'total_amount_str': "$ 1,000.00", 'partner_id': 12, 'account_name': "101200 Account Receivable", 'name': "INV/2017/0005: Customer Payment", 'partner_name': "Camptocamp", 'total_amount_currency_str': "", 'id': 19, 'credit': 1000.0, 'journal_id': [1, "Customer Invoices"], 'amount_str': "$ 1,000.00", 'debit': 0.0, 'account_code': "101200", 'ref': "", 'already_paid': false},
|
||||
{'account_type': "receivable", 'account_id': [287, "101200 Account Receivable"], 'amount_currency_str': "", 'currency_id': false, 'date_maturity': "2017-02-09", 'date': "2017-02-09", 'total_amount_str': "$ 180.00", 'partner_id': 12, 'account_name': "101200 Account Receivable", 'name': "BILL/2017/0003: Customer Payment", 'partner_name': "Camptocamp", 'total_amount_currency_str': "", 'id': 21, 'credit': 180.0, 'journal_id': [2, "Vendor Bills"], 'amount_str': "$ 180.00", 'debit': 0.0, 'account_code': "101200", 'ref': "fddfgfdgfdgsdfg", 'already_paid': false},
|
||||
{'account_type': "receivable", 'account_id': [287, "101200 Account Receivable"], 'amount_currency': 100, 'amount_currency_str': "100.00 €", 'currency_id': 1, 'date_maturity': "2017-02-09", 'date': "2017-02-09", 'total_amount_str': "$ 170.00", 'partner_id': 12, 'account_name': "101200 Account Receivable", 'name': "INV/2017/0003: Customer Payment", 'partner_name': "Camptocamp", 'total_amount_currency_str': "", 'id': 21, 'credit': 0.0, 'journal_id': [1, "Customer Invoices"], 'amount_str': "$ 170.00", 'debit': 170.0, 'account_code': "101200", 'ref': "INV fddfgfdgfdgsdfg", 'already_paid': false},
|
||||
{'account_type': "receivable", 'account_id': [287, "101200 Account Receivable"], 'amount_currency': 100, 'amount_currency_str': "100.00 €", 'currency_id': 1, 'date_maturity': "2017-02-10", 'date': "2017-02-10", 'total_amount_str': "$ 180.00", 'partner_id': 12, 'account_name': "101200 Account Receivable", 'name': "BILL/2017/0003: Customer Payment", 'partner_name': "Camptocamp", 'total_amount_currency_str': "", 'id': 22, 'credit': 180.0, 'journal_id': [2, "Vendor Bills"], 'amount_str': "$ 180.00", 'debit': 0.0, 'account_code': "101200", 'ref': "fddfgfdgfdgsdfg", 'already_paid': false},
|
||||
{'account_type': "receivable", 'account_id': [287, "101200 Account Receivable"], 'amount_currency': 170, 'amount_currency_str': "170.00 €", 'currency_id': 1, 'date_maturity': "2017-02-09", 'date': "2017-02-09", 'total_amount_str': "$ 100.00", 'partner_id': 12, 'account_name': "101200 Account Receivable", 'name': "INV/2017/0003: Customer Payment", 'partner_name': "Camptocamp", 'total_amount_currency_str': "", 'id': 23, 'credit': 0.0, 'journal_id': [1, "Customer Invoices"], 'amount_str': "$ 100.00", 'debit': 100.0, 'account_code': "101200", 'ref': "INV fddfgfdgfdgsdfg", 'already_paid': false},
|
||||
{'account_type': "receivable", 'account_id': [287, "101200 Account Receivable"], 'amount_currency': 180, 'amount_currency_str': "180.00 €", 'currency_id': 1, 'date_maturity': "2017-02-10", 'date': "2017-02-10", 'total_amount_str': "$ 100.00", 'partner_id': 12, 'account_name': "101200 Account Receivable", 'name': "BILL/2017/0003: Customer Payment", 'partner_name': "Camptocamp", 'total_amount_currency_str': "", 'id': 24, 'credit': 100.0, 'journal_id': [2, "Vendor Bills"], 'amount_str': "$ 100.00", 'debit': 0.0, 'account_code': "101200", 'ref': "fddfgfdgfdgsdfg", 'already_paid': false},
|
||||
],
|
||||
'[284,8,"",0,6]': [
|
||||
{'account_type': "receivable", 'account_id': [284, "111100 Account Payable"], 'amount_currency_str': "", 'currency_id': false, 'date_maturity': "2017-02-08", 'date': "2017-02-08", 'total_amount_str': "$ 11,000.00", 'partner_id': 8, 'account_name': "101200 Account Receivable", 'name': "INV/2017/0004: Customer Payment", 'partner_name': "Agrolait", 'total_amount_currency_str': "", 'id': 17, 'credit': 11000.0, 'journal_id': [1, "Customer Invoices"], 'amount_str': "$ 11,000.00", 'debit': 0.0, 'account_code': "111100", 'ref': "", 'already_paid': false},
|
||||
@@ -1117,7 +1120,7 @@ QUnit.module('account', {
|
||||
testUtils.addMockEnvironment(clientAction, {
|
||||
'data': this.params.data,
|
||||
});
|
||||
clientAction.appendTo($('body'));
|
||||
clientAction.appendTo($('#qunit-fixture'));
|
||||
|
||||
clientAction.$('button.o_automatic_reconciliation').trigger('click');
|
||||
|
||||
@@ -1141,7 +1144,7 @@ QUnit.module('account', {
|
||||
});
|
||||
|
||||
QUnit.test('Reconciliation manual', function (assert) {
|
||||
assert.expect(8);
|
||||
assert.expect(13);
|
||||
|
||||
var clientAction = new ReconciliationClientAction.ManualAction(null, this.params.options);
|
||||
|
||||
@@ -1171,6 +1174,23 @@ QUnit.module('account', {
|
||||
|
||||
assert.strictEqual(clientAction.$('.accounting_view:first .o_no_valid:visible').length, 1, "should display the skip button");
|
||||
|
||||
clientAction.$('.accounting_view:eq(1) thead td:first').trigger('click');
|
||||
clientAction.$('.o_reconciliation_line:eq(1) [data-line-id="21"] .cell_label').trigger('click');
|
||||
clientAction.$('.o_reconciliation_line:eq(1) [data-line-id="22"] .cell_label').trigger('click');
|
||||
|
||||
assert.strictEqual(clientAction.$('.o_reconciliation_line:eq(1) tfoot tr').length, 0, "should not display the 'Write-off' line because the balance is null in Euro");
|
||||
assert.strictEqual(clientAction.$('.o_reconciliation_line:eq(1) .o_reconcile:visible').length, 1, "should display 'Reconcile' button in green");
|
||||
|
||||
clientAction.$('.o_reconciliation_line:eq(1) .o_reconcile:visible').trigger('click');
|
||||
|
||||
assert.strictEqual(clientAction.$('.o_reconciliation_line[data-mode!="inactive"]').length, 1, "should have only one line open");
|
||||
|
||||
clientAction.$('.o_reconciliation_line:eq(1) [data-line-id="23"] .cell_label').trigger('click');
|
||||
clientAction.$('.o_reconciliation_line:eq(1) [data-line-id="24"] .cell_label').trigger('click');
|
||||
|
||||
assert.strictEqual(clientAction.$('.o_reconciliation_line:eq(1) tfoot tr').length, 1, "should display the 'Write-off' line because the balance is not null in Euro");
|
||||
assert.strictEqual(clientAction.$('.o_reconciliation_line:eq(1) .o_validate:visible').length, 1, "should display 'Reconcile' button");
|
||||
|
||||
clientAction.destroy();
|
||||
});
|
||||
|
||||
|
||||
@@ -252,7 +252,7 @@
|
||||
<group>
|
||||
<field name="origin" attrs="{'invisible': [('origin', '=', False)]}"/>
|
||||
<field name="date_invoice" string="Bill Date"/>
|
||||
<field name="date_due" attrs="{'readonly': ['|',('payment_term_id','!=',False), ('state','=','paid')]}"/>
|
||||
<field name="date_due" attrs="{'readonly': ['|',('payment_term_id','!=',False), ('state','=','paid')]}" force_save="1"/>
|
||||
<field name="move_name" invisible="1"/>
|
||||
<field name="currency_id" options="{'no_create': True, 'no_open': True}" groups="base.group_multi_currency"/>
|
||||
<field name="company_currency_id" invisible="1"/>
|
||||
|
||||
@@ -297,7 +297,7 @@
|
||||
<field name="domain">[('id','in',active_ids)]</field>
|
||||
<field name="view_type">tree</field>
|
||||
<field name="view_id" ref="view_partner_tree2"/>
|
||||
<field name="binding_model_id" ref="base.model_res_partner"/>
|
||||
<field name="binding_model_id" ref="hr.model_hr_employee"/>
|
||||
</record>
|
||||
|
||||
<!-- Employee Category -->
|
||||
|
||||
@@ -43,8 +43,8 @@ class AccountFrFec(models.TransientModel):
|
||||
'-' AS PieceRef,
|
||||
%s AS PieceDate,
|
||||
'/' AS EcritureLib,
|
||||
replace(CASE WHEN COALESCE(sum(aml.balance), 0) <= 0 THEN '0,00' ELSE to_char(SUM(aml.balance), '999999999999999D99') END, '.', ',') AS Debit,
|
||||
replace(CASE WHEN COALESCE(sum(aml.balance), 0) >= 0 THEN '0,00' ELSE to_char(-SUM(aml.balance), '999999999999999D99') END, '.', ',') AS Credit,
|
||||
replace(CASE WHEN COALESCE(sum(aml.balance), 0) <= 0 THEN '0,00' ELSE to_char(SUM(aml.balance), '000000000000000D99') END, '.', ',') AS Debit,
|
||||
replace(CASE WHEN COALESCE(sum(aml.balance), 0) >= 0 THEN '0,00' ELSE to_char(-SUM(aml.balance), '000000000000000D99') END, '.', ',') AS Credit,
|
||||
'' AS EcritureLet,
|
||||
'' AS DateLet,
|
||||
%s AS ValidDate,
|
||||
@@ -142,8 +142,8 @@ class AccountFrFec(models.TransientModel):
|
||||
'-' AS PieceRef,
|
||||
%s AS PieceDate,
|
||||
'/' AS EcritureLib,
|
||||
replace(CASE WHEN sum(aml.balance) <= 0 THEN '0,00' ELSE to_char(SUM(aml.balance), '999999999999999D99') END, '.', ',') AS Debit,
|
||||
replace(CASE WHEN sum(aml.balance) >= 0 THEN '0,00' ELSE to_char(-SUM(aml.balance), '999999999999999D99') END, '.', ',') AS Credit,
|
||||
replace(CASE WHEN sum(aml.balance) <= 0 THEN '0,00' ELSE to_char(SUM(aml.balance), '000000000000000D99') END, '.', ',') AS Debit,
|
||||
replace(CASE WHEN sum(aml.balance) >= 0 THEN '0,00' ELSE to_char(-SUM(aml.balance), '000000000000000D99') END, '.', ',') AS Credit,
|
||||
'' AS EcritureLet,
|
||||
'' AS DateLet,
|
||||
%s AS ValidDate,
|
||||
@@ -227,8 +227,8 @@ class AccountFrFec(models.TransientModel):
|
||||
'-' AS PieceRef,
|
||||
%s AS PieceDate,
|
||||
'/' AS EcritureLib,
|
||||
replace(CASE WHEN sum(aml.balance) <= 0 THEN '0,00' ELSE to_char(SUM(aml.balance), '999999999999999D99') END, '.', ',') AS Debit,
|
||||
replace(CASE WHEN sum(aml.balance) >= 0 THEN '0,00' ELSE to_char(-SUM(aml.balance), '999999999999999D99') END, '.', ',') AS Credit,
|
||||
replace(CASE WHEN sum(aml.balance) <= 0 THEN '0,00' ELSE to_char(SUM(aml.balance), '000000000000000D99') END, '.', ',') AS Debit,
|
||||
replace(CASE WHEN sum(aml.balance) >= 0 THEN '0,00' ELSE to_char(-SUM(aml.balance), '000000000000000D99') END, '.', ',') AS Credit,
|
||||
'' AS EcritureLet,
|
||||
'' AS DateLet,
|
||||
%s AS ValidDate,
|
||||
@@ -289,14 +289,14 @@ class AccountFrFec(models.TransientModel):
|
||||
AS PieceRef,
|
||||
TO_CHAR(am.date, 'YYYYMMDD') AS PieceDate,
|
||||
CASE WHEN aml.name IS NULL THEN '/' ELSE replace(replace(aml.name, '|', '/'), '\t', '') END AS EcritureLib,
|
||||
replace(CASE WHEN aml.debit = 0 THEN '0,00' ELSE to_char(aml.debit, '999999999999999D99') END, '.', ',') AS Debit,
|
||||
replace(CASE WHEN aml.credit = 0 THEN '0,00' ELSE to_char(aml.credit, '999999999999999D99') END, '.', ',') AS Credit,
|
||||
replace(CASE WHEN aml.debit = 0 THEN '0,00' ELSE to_char(aml.debit, '000000000000000D99') END, '.', ',') AS Debit,
|
||||
replace(CASE WHEN aml.credit = 0 THEN '0,00' ELSE to_char(aml.credit, '000000000000000D99') END, '.', ',') AS Credit,
|
||||
CASE WHEN rec.name IS NULL THEN '' ELSE rec.name END AS EcritureLet,
|
||||
CASE WHEN aml.full_reconcile_id IS NULL THEN '' ELSE TO_CHAR(rec.create_date, 'YYYYMMDD') END AS DateLet,
|
||||
TO_CHAR(am.date, 'YYYYMMDD') AS ValidDate,
|
||||
CASE
|
||||
WHEN aml.amount_currency IS NULL OR aml.amount_currency = 0 THEN ''
|
||||
ELSE replace(to_char(aml.amount_currency, '999999999999999D99'), '.', ',')
|
||||
ELSE replace(to_char(aml.amount_currency, '000000000000000D99'), '.', ',')
|
||||
END AS Montantdevise,
|
||||
CASE WHEN aml.currency_id IS NULL THEN '' ELSE rc.name END AS Idevise
|
||||
FROM
|
||||
|
||||
@@ -48,7 +48,7 @@ BasicModel.include({
|
||||
def = self._rpc({
|
||||
model: list.model,
|
||||
method: 'name_get',
|
||||
args: list.res_ids,
|
||||
args: [list.res_ids],
|
||||
context: record.getContext({fieldName: fieldName}),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<div class="o_thread_message" style="margin-bottom: 10px">
|
||||
<div class="o_thread_message_sidebar">
|
||||
<div class="o_avatar_stack">
|
||||
<img t-attf-src="/web/image#{activity.user_id[0] >= 0 ? ('/res.users/' + activity.user_id[0] + '/image_small') : ''}" class="o_thread_message_avatar img-circle mb8" t-att-title="activity.create_uid[1]"/>
|
||||
<img t-attf-src="/web/image#{activity.user_id[0] >= 0 ? ('/res.users/' + activity.user_id[0] + '/image_small') : ''}" class="o_thread_message_avatar img-circle mb8" t-att-title="activity.user_id[1]"/>
|
||||
<i t-att-class="'o_avatar_icon fa ' + activity.icon + ' bg-' + (activity.state == 'planned'? 'success' : (activity.state == 'today'? 'warning' : 'danger')) + '-full'"
|
||||
t-att-title="activity.activity_type_id[1]"/>
|
||||
</div>
|
||||
|
||||
@@ -1273,7 +1273,7 @@ QUnit.test('fieldmany2many tags email', function (assert) {
|
||||
},
|
||||
mockRPC: function (route, args) {
|
||||
if (route === "/web/dataset/call_kw/partner_type/name_get") {
|
||||
assert.deepEqual(args.args, nameGottenIds.shift(),
|
||||
assert.deepEqual(args.args[0], nameGottenIds.shift(),
|
||||
"partner with email should be name_get'ed");
|
||||
}
|
||||
else if (args.method ==='read' && args.model === 'partner_type') {
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
// ===== Layout =====
|
||||
.o_layout {
|
||||
overflow: hidden;
|
||||
box-sizing: content-box;
|
||||
width: 94%;
|
||||
min-height: 100%;
|
||||
background-color: @o_mm_def_color_alpha;
|
||||
|
||||
@@ -13,7 +13,9 @@ class StockMoveLine(models.Model):
|
||||
workorder_id = fields.Many2one('mrp.workorder', 'Work Order')
|
||||
production_id = fields.Many2one('mrp.production', 'Production Order')
|
||||
lot_produced_id = fields.Many2one('stock.production.lot', 'Finished Lot')
|
||||
lot_produced_qty = fields.Float('Quantity Finished Product', help="Informative, not used in matching")
|
||||
lot_produced_qty = fields.Float(
|
||||
'Quantity Finished Product', digits=dp.get_precision('Product Unit of Measure'),
|
||||
help="Informative, not used in matching")
|
||||
done_wo = fields.Boolean('Done for Work Order', default=True, help="Technical Field which is False when temporarily filled in in work order") # TDE FIXME: naming
|
||||
done_move = fields.Boolean('Move Done', related='move_id.is_done', store=True) # TDE FIXME: naming
|
||||
|
||||
|
||||
@@ -85,7 +85,7 @@
|
||||
<field name="name"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="location_id" groups="stock.group_adv_location"/>
|
||||
<field name="location_id" groups="stock.group_adv_location" domain="[('usage', '!=', 'view')]"/>
|
||||
<field name="company_id" options="{'no_create': True}" groups="base.group_multi_company"/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
@@ -16,6 +16,7 @@ formatting.
|
||||
|
||||
This module applies this feature to Leads and Contacts.""",
|
||||
'data': [
|
||||
'views/res_company_view.xml'
|
||||
],
|
||||
'depends': ['base'],
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ class ResCompany(models.Model):
|
||||
phone_international_format = fields.Selection([
|
||||
('no_prefix', 'No prefix'),
|
||||
('prefix', 'Add international prefix'),
|
||||
],string="Local Numbers", default="no_prefix",
|
||||
], string="Local Numbers", default="prefix",
|
||||
help="Always encode phone numbers using international format. Otherwise "
|
||||
"numbers coming from the company's country are nationaly formatted. "
|
||||
"International numbers are always using international format."
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
The purpose of this view is to move the VAT field at the top of the
|
||||
company form. That way, the user will fill it first
|
||||
-->
|
||||
<odoo>
|
||||
<record id="res_company_view_form_phone_validation" model="ir.ui.view">
|
||||
<field name="name">res.company.view.form.phone_validation</field>
|
||||
<field name="model">res.company</field>
|
||||
<field name="inherit_id" ref="base.view_company_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='phone']" position="after">
|
||||
<field name="phone_international_format" groups="base.group_no_one"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -89,7 +89,7 @@
|
||||
</div>
|
||||
<div name="tags"/>
|
||||
<ul>
|
||||
<li>Price: <field name="lst_price" widget="monetary"></field></li>
|
||||
<li>Price: <field name="lst_price" widget="monetary" options="{'currency_field': 'currency_id', 'field_digits': True}"></field></li>
|
||||
</ul>
|
||||
<div name="tags"/>
|
||||
</div>
|
||||
|
||||
@@ -25,7 +25,7 @@ You can choose flexible invoicing methods:
|
||||
* *Before Delivery*: A Draft invoice is created and must be paid before delivery
|
||||
""",
|
||||
'website': 'https://www.odoo.com/page/warehouse',
|
||||
'depends': ['sale', 'stock_account'],
|
||||
'depends': ['sale_management', 'stock_account'],
|
||||
'data': [
|
||||
'security/sale_stock_security.xml',
|
||||
'security/ir.model.access.csv',
|
||||
|
||||
@@ -78,7 +78,6 @@ class ProcurementRule(models.Model):
|
||||
data = self._get_stock_move_values(product_id, product_qty, product_uom, location_id, name, origin, values, group_id)
|
||||
# Since action_confirm launch following procurement_group we should activate it.
|
||||
move = self.env['stock.move'].sudo().create(data)
|
||||
move._assign_picking()
|
||||
move._action_confirm()
|
||||
return True
|
||||
|
||||
|
||||
@@ -183,6 +183,10 @@ class Route(models.Model):
|
||||
'domain': [('route_ids', 'in', self.ids)],
|
||||
}
|
||||
|
||||
@api.onchange('warehouse_selectable')
|
||||
def _onchange_warehouse_selectable(self):
|
||||
if not self.warehouse_selectable:
|
||||
self.warehouse_ids = [(5, 0, 0)]
|
||||
|
||||
class PushedFlow(models.Model):
|
||||
_name = "stock.location.path"
|
||||
|
||||
@@ -17,6 +17,7 @@ import operator
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
import zlib
|
||||
|
||||
@@ -24,6 +25,7 @@ import werkzeug
|
||||
import werkzeug.utils
|
||||
import werkzeug.wrappers
|
||||
import werkzeug.wsgi
|
||||
from collections import OrderedDict
|
||||
from werkzeug.urls import url_decode, iri_to_uri
|
||||
from xml.etree import ElementTree
|
||||
|
||||
@@ -41,6 +43,7 @@ from odoo.http import content_disposition, dispatch_rpc, request, \
|
||||
serialize_exception as _serialize_exception, Response
|
||||
from odoo.exceptions import AccessError, UserError
|
||||
from odoo.models import check_method_name
|
||||
from odoo.service import db
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -155,10 +158,10 @@ def module_installed(environment):
|
||||
# TODO The following code should move to ir.module.module.list_installed_modules()
|
||||
Modules = environment['ir.module.module']
|
||||
domain = [('state','=','installed'), ('name','in', loadable)]
|
||||
modules = {
|
||||
module.name: module.dependencies_id.mapped('name')
|
||||
modules = OrderedDict(
|
||||
(module.name, module.dependencies_id.mapped('name'))
|
||||
for module in Modules.search(domain)
|
||||
}
|
||||
)
|
||||
|
||||
sorted_modules = topological_sort(modules)
|
||||
return sorted_modules
|
||||
@@ -740,12 +743,15 @@ class Database(http.Controller):
|
||||
@http.route('/web/database/restore', type='http', auth="none", methods=['POST'], csrf=False)
|
||||
def restore(self, master_pwd, backup_file, name, copy=False):
|
||||
try:
|
||||
data = base64.b64encode(backup_file.read())
|
||||
dispatch_rpc('db', 'restore', [master_pwd, name, data, str2bool(copy)])
|
||||
with tempfile.NamedTemporaryFile(delete=False) as data_file:
|
||||
backup_file.save(data_file)
|
||||
db.restore_db(name, data_file.name, str2bool(copy))
|
||||
return http.local_redirect('/web/database/manager')
|
||||
except Exception as e:
|
||||
error = "Database restore error: %s" % (str(e) or repr(e))
|
||||
return self._render_template(error=error)
|
||||
finally:
|
||||
os.unlink(data_file.name)
|
||||
|
||||
@http.route('/web/database/change_password', type='http', auth="none", methods=['POST'], csrf=False)
|
||||
def change_password(self, master_pwd, master_pwd_new):
|
||||
|
||||
@@ -23,6 +23,22 @@ var utils = {
|
||||
throw new Error("AssertionError");
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Check if the value is a bin_size or not.
|
||||
* If not, compute an approximate size out of the base64 encoded string.
|
||||
*
|
||||
* @param {string} value original format
|
||||
* @return {string} bin_size (human-readable)
|
||||
*/
|
||||
binaryToBinsize: function (value) {
|
||||
if (!this.is_bin_size(value)) {
|
||||
// Computing approximate size out of base64 encoded string
|
||||
// http://en.wikipedia.org/wiki/Base64#MIME
|
||||
return this.human_size(value.length / 1.37);
|
||||
}
|
||||
// already bin_size
|
||||
return value;
|
||||
},
|
||||
/**
|
||||
* Confines a value inside an interval
|
||||
*
|
||||
@@ -132,7 +148,7 @@ var utils = {
|
||||
size /= 1024;
|
||||
++i;
|
||||
}
|
||||
return size.toFixed(2) + ' ' + units[i];
|
||||
return size.toFixed(2) + ' ' + units[i].trim();
|
||||
},
|
||||
/**
|
||||
* Insert "thousands" separators in the provided number (which is actually
|
||||
|
||||
@@ -44,6 +44,11 @@ var AbstractField = Widget.extend({
|
||||
navigation_move: '_onNavigationMove',
|
||||
},
|
||||
|
||||
/**
|
||||
* fields can extend the context, e.g. binary fields add {bin_size: true}
|
||||
*/
|
||||
context: null,
|
||||
|
||||
/**
|
||||
* An object representing fields to be fetched by the model eventhough not present in the view
|
||||
* This object contains "field name" as key and an object as value.
|
||||
|
||||
@@ -1094,6 +1094,7 @@ var AbstractFieldBinary = AbstractField.extend({
|
||||
},
|
||||
'click .o_clear_file_button': 'on_clear',
|
||||
}),
|
||||
context: { bin_size: true },
|
||||
init: function (parent, name, record) {
|
||||
this._super.apply(this, arguments);
|
||||
this.fields = record.fields;
|
||||
@@ -1312,25 +1313,6 @@ var FieldBinaryFile = AbstractFieldBinary.extend({
|
||||
},
|
||||
});
|
||||
|
||||
var FieldBinaryDownloadLink = AbstractFieldBinary.extend({
|
||||
template: 'FieldBinaryDownloadLink',
|
||||
events: _.extend({}, AbstractFieldBinary.prototype.events, {
|
||||
'click': function (ev) {
|
||||
ev.stopPropagation();
|
||||
},
|
||||
}),
|
||||
supportedFieldTypes: ['binary'],
|
||||
init: function () {
|
||||
this._super.apply(this, arguments);
|
||||
this.filename = _t("Binary File");
|
||||
if (this.nodeOptions.filename && this.recordData[this.nodeOptions.filename]) {
|
||||
this.filename = this.recordData[this.nodeOptions.filename];
|
||||
}
|
||||
this.url = "data:application/octet-stream;base64," + this.value;
|
||||
this.text = _t("Download");
|
||||
},
|
||||
});
|
||||
|
||||
var PriorityWidget = AbstractField.extend({
|
||||
// the current implementation of this widget makes it
|
||||
// only usable for fields of type selection
|
||||
@@ -2561,7 +2543,6 @@ return {
|
||||
TranslatableFieldMixin: TranslatableFieldMixin,
|
||||
DebouncedField: DebouncedField,
|
||||
FieldEmail: FieldEmail,
|
||||
FieldBinaryDownloadLink: FieldBinaryDownloadLink,
|
||||
FieldBinaryFile: FieldBinaryFile,
|
||||
FieldBinaryImage: FieldBinaryImage,
|
||||
FieldBoolean: FieldBoolean,
|
||||
|
||||
@@ -33,7 +33,6 @@ registry
|
||||
.add('email', basic_fields.FieldEmail)
|
||||
.add('phone', basic_fields.FieldPhone)
|
||||
.add('url', basic_fields.UrlWidget)
|
||||
.add('download_link', basic_fields.FieldBinaryDownloadLink)
|
||||
.add('image', basic_fields.FieldBinaryImage)
|
||||
.add('binary', basic_fields.FieldBinaryFile)
|
||||
.add('monetary', basic_fields.FieldMonetary)
|
||||
|
||||
@@ -28,17 +28,17 @@ var _t = core._t;
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Convert binary to bin_size
|
||||
*
|
||||
* @param {string} value base64 representation of the binary
|
||||
* @param {string} value base64 representation of the binary (might be already a bin_size!)
|
||||
* @param {Object} [field]
|
||||
* a description of the field (note: this parameter is ignored)
|
||||
* @param {Object} [options] additional options (note: this parameter is ignored)
|
||||
*
|
||||
* @returns {string} bin_size (which is human-readable)
|
||||
*/
|
||||
function formatBinary(value, field, options) {
|
||||
if (!value) {
|
||||
return '';
|
||||
}
|
||||
return utils.human_size(value.length / 1.37); // base64 estimated size: http://en.wikipedia.org/wiki/Base64#MIME
|
||||
return utils.binaryToBinsize(value);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -387,6 +387,10 @@ return core.Class.extend({
|
||||
attrs.fieldDependencies = attrs.Widget.prototype.fieldDependencies;
|
||||
}
|
||||
|
||||
if (attrs.Widget.prototype.context) {
|
||||
attrs.context = attrs.Widget.prototype.context;
|
||||
}
|
||||
|
||||
return attrs;
|
||||
},
|
||||
/**
|
||||
|
||||
@@ -33,7 +33,7 @@ odoo.define('web.BasicModel', function (require) {
|
||||
* isOpen: {boolean},
|
||||
* loadMoreOffset: {integer},
|
||||
* limit: {integer},
|
||||
* model: {string,
|
||||
* model: {string},
|
||||
* offset: {integer},
|
||||
* openGroupByDefault: {boolean},
|
||||
* orderedBy: {Object[]},
|
||||
@@ -1854,7 +1854,7 @@ var BasicModel = AbstractModel.extend({
|
||||
model: record.model,
|
||||
method: 'read',
|
||||
args: [[record.res_id], fieldNames],
|
||||
context: _.extend({}, record.getContext(), {bin_size: true}),
|
||||
context: record.getContext(),
|
||||
})
|
||||
.then(function (result) {
|
||||
if (result.length === 0) {
|
||||
@@ -2374,7 +2374,7 @@ var BasicModel = AbstractModel.extend({
|
||||
});
|
||||
record.data[fieldName] = list.id;
|
||||
if (!fieldInfo.__no_fetch) {
|
||||
var def = self._readUngroupedList(list).then(function () {
|
||||
var def = self._readUngroupedList(list).then(function (list) {
|
||||
return $.when(
|
||||
self._fetchX2ManysBatched(list),
|
||||
self._fetchReferencesBatched(list)
|
||||
@@ -3040,10 +3040,33 @@ var BasicModel = AbstractModel.extend({
|
||||
* the resource in the localData object.
|
||||
*
|
||||
* @param {Object} params
|
||||
* @param {Object} [params.fieldsInfo={}] contains the fieldInfo of each field
|
||||
* @param {Array} [params.fieldNames] the name of fields to load, the list
|
||||
* of all fields by default
|
||||
* @param {Object} [params.aggregateValues={}]
|
||||
* @param {Object} [params.context={}] context of the action
|
||||
* @param {integer} [params.count=0] number of record being manipulated
|
||||
* @param {Object|Object[]} [params.data={}|[]] data of the record
|
||||
* @param {*[]} [params.domain=[]]
|
||||
* @param {Object} params.fields contains the description of each field
|
||||
* @param {Object} [params.fieldsInfo={}] contains the fieldInfo of each field
|
||||
* @param {Object[]} [params.fieldNames] the name of fields to load, the list
|
||||
* of all fields by default
|
||||
* @param {string[]} [params.groupedBy=[]]
|
||||
* @param {boolean} [params.isOpen]
|
||||
* @param {integer} params.limit max number of records shown on screen (pager size)
|
||||
* @param {string} params.modelName
|
||||
* @param {integer} [params.offset]
|
||||
* @param {boolean} [params.openGroupByDefault]
|
||||
* @param {Object[]} [params.orderedBy=[]]
|
||||
* @param {integer[]} [params.orderedResIDs]
|
||||
* @param {string} [params.parentID] model name ID of the parent model
|
||||
* @param {Object} [params.rawContext]
|
||||
* @param {[type]} [params.ref]
|
||||
* @param {string} [params.relationField]
|
||||
* @param {integer|null} [params.res_id] actual id of record in the server
|
||||
* @param {integer[]} [params.res_ids] context in which the data point is used, from a list of res_id
|
||||
* @param {boolean} [params.static=false]
|
||||
* @param {string} [params.type='record'|'list']
|
||||
* @param {[type]} [params.value]
|
||||
* @param {string} [params.viewType] the type of the view, e.g. 'list' or 'form'
|
||||
* @returns {Object} the resource created
|
||||
*/
|
||||
_makeDataPoint: function (params) {
|
||||
@@ -3069,13 +3092,23 @@ var BasicModel = AbstractModel.extend({
|
||||
id: {type: 'integer'},
|
||||
}, params.fields);
|
||||
|
||||
// datapoint context is extended by fields widgets
|
||||
var context = _.extend({}, params.context);
|
||||
if (params.fieldsInfo && params.viewType) {
|
||||
_.each(params.fieldsInfo[params.viewType], function (field) {
|
||||
if (field.context) {
|
||||
_.extend(context, field.context);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var dataPoint = {
|
||||
_cache: type === 'list' ? {} : undefined,
|
||||
_changes: null,
|
||||
_domains: {},
|
||||
_rawChanges: {},
|
||||
aggregateValues: params.aggregateValues || {},
|
||||
context: params.context || {},
|
||||
context: context,
|
||||
count: params.count || res_ids.length,
|
||||
data: data,
|
||||
domain: params.domain || [],
|
||||
|
||||
@@ -693,6 +693,7 @@ var FormRenderer = BasicRenderer.extend({
|
||||
var self = this;
|
||||
var $headers = $('<ul class="nav nav-tabs">');
|
||||
var $pages = $('<div class="tab-content nav nav-tabs">');
|
||||
var autofocusTab = -1;
|
||||
// renderedTabs is used to aggregate the generated $headers and $pages
|
||||
// alongside their node, so that their modifiers can be registered once
|
||||
// all tabs have been rendered, to ensure that the first visible tab
|
||||
@@ -701,9 +702,8 @@ var FormRenderer = BasicRenderer.extend({
|
||||
var pageID = _.uniqueId('notebook_page_');
|
||||
var $header = self._renderTabHeader(child, pageID);
|
||||
var $page = self._renderTabPage(child, pageID);
|
||||
if (index === 0) {
|
||||
$header.addClass('active');
|
||||
$page.addClass('active');
|
||||
if (autofocusTab === -1 && child.attrs.autofocus === 'autofocus') {
|
||||
autofocusTab = index;
|
||||
}
|
||||
self._handleAttributes($header, child);
|
||||
$headers.append($header);
|
||||
@@ -714,6 +714,11 @@ var FormRenderer = BasicRenderer.extend({
|
||||
node: child,
|
||||
};
|
||||
});
|
||||
if (renderedTabs.length) {
|
||||
var tabToFocus = renderedTabs[Math.max(0, autofocusTab)];
|
||||
tabToFocus.$header.addClass('active');
|
||||
tabToFocus.$page.addClass('active');
|
||||
}
|
||||
// register the modifiers for each tab
|
||||
_.each(renderedTabs, function (tab) {
|
||||
self._registerModifiers(tab.node, self.state, tab.$header, {
|
||||
|
||||
@@ -239,15 +239,16 @@ var KanbanController = BasicController.extend({
|
||||
* @private
|
||||
*/
|
||||
_onButtonNew: function () {
|
||||
var data = this.model.get(this.handle, {raw: true});
|
||||
if (data.groupedBy.length > 0 && data.count > 0 && this.on_create === 'quick_create') {
|
||||
var state = this.model.get(this.handle, {raw: true});
|
||||
var hasColumns = state.groupedBy.length > 0 && state.data.length > 0;
|
||||
if (hasColumns && this.on_create === 'quick_create') {
|
||||
// Activate the quick create in the first column
|
||||
this.renderer.addQuickCreate();
|
||||
} else if (this.on_create && this.on_create !== 'quick_create') {
|
||||
// Execute the given action
|
||||
this.do_action(this.on_create, {
|
||||
on_close: this.reload.bind(this),
|
||||
additional_context: data.context,
|
||||
additional_context: state.context,
|
||||
});
|
||||
} else {
|
||||
// Open the form view
|
||||
@@ -355,11 +356,13 @@ var KanbanController = BasicController.extend({
|
||||
self._updateEnv();
|
||||
|
||||
var columnState = self.model.getColumn(db_id);
|
||||
return self.renderer.updateColumn(columnState.id, columnState).then(function () {
|
||||
if (event.data.openRecord) {
|
||||
self.trigger_up('open_record', {id: db_id, mode: 'edit'});
|
||||
}
|
||||
});
|
||||
return self.renderer
|
||||
.updateColumn(columnState.id, columnState, {openQuickCreate: true})
|
||||
.then(function () {
|
||||
if (event.data.openRecord) {
|
||||
self.trigger_up('open_record', {id: db_id, mode: 'edit'});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@@ -141,15 +141,23 @@ var KanbanRenderer = BasicRenderer.extend({
|
||||
*
|
||||
* @param {string} localID the column id
|
||||
* @param {Object} columnState
|
||||
* @param {Object} [options]
|
||||
* @param {boolean} [options.openQuickCreate] if true, directly opens the
|
||||
* QuickCreate widget in the updated column
|
||||
*
|
||||
* @returns {Deferred}
|
||||
*/
|
||||
updateColumn: function (localID, columnState) {
|
||||
updateColumn: function (localID, columnState, options) {
|
||||
var newColumn = new KanbanColumn(this, columnState, this.columnOptions, this.recordOptions);
|
||||
var index = _.findIndex(this.widgets, {db_id: localID});
|
||||
var column = this.widgets[index];
|
||||
this.widgets[index] = newColumn;
|
||||
return newColumn.insertAfter(column.$el).then(column.destroy.bind(column));
|
||||
return newColumn.insertAfter(column.$el).then(function () {
|
||||
if (options && options.openQuickCreate) {
|
||||
newColumn.addQuickCreate();
|
||||
}
|
||||
column.destroy();
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Updates a given record with its new state.
|
||||
|
||||
@@ -906,9 +906,6 @@
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
<t t-name="FieldBinaryDownloadLink">
|
||||
<a t-att-download="widget.filename" t-att-href="widget.url"><t t-esc="widget.text"/></a>
|
||||
</t>
|
||||
<t t-name="HiddenInputFile">
|
||||
<div t-attf-class="o_hidden_input_file #{fileupload_class or ''}" t-att-style="fileupload_style">
|
||||
<form class="o_form_binary_form" t-att-target="fileupload_id"
|
||||
|
||||
@@ -1518,40 +1518,6 @@ QUnit.module('basic_fields', {
|
||||
session.get_file = oldGetFile;
|
||||
});
|
||||
|
||||
QUnit.test('download_link widget in list view', function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
this.data.partner.fields.filename = { string: "Filename", type: "char" };
|
||||
this.data.partner.records[0].filename = "document.txt";
|
||||
|
||||
var form = createView({
|
||||
View: ListView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<tree string="Partners">' +
|
||||
'<field name="document" widget="download_link" options="{\'filename\': \'filename\'}"/>' +
|
||||
'<field name="filename"/>' +
|
||||
'</tree>',
|
||||
res_id: 1,
|
||||
});
|
||||
|
||||
assert.ok(form.$('.o_data_cell').first().hasClass("o_download_link_cell"),
|
||||
"this should be a download link cell");
|
||||
assert.ok(form.$('.o_data_cell').first().find('a').eq(0),
|
||||
"there should be a link in the cell");
|
||||
assert.strictEqual(form.$('.o_data_cell').first().find('a').eq(0).attr('download'),
|
||||
"document.txt",
|
||||
"it should download a file named 'document.txt'");
|
||||
assert.strictEqual(form.$('.o_data_cell').first().find('a').eq(0).attr('href').trim(),
|
||||
"data:application/octet-stream;base64,coucou==",
|
||||
"it should have the appropriate link of a binary");
|
||||
assert.strictEqual(form.$('.o_data_cell').first().find('a').eq(0).text(),
|
||||
"Download",
|
||||
"the text of the link should be 'Download'");
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('text field rendering in list view', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
|
||||
@@ -6916,7 +6916,6 @@ QUnit.module('relational_fields', {
|
||||
if (args.method === 'read' && args.model === 'partner') {
|
||||
assert.deepEqual(args.kwargs.context, {
|
||||
active_field: 2,
|
||||
bin_size: true,
|
||||
someKey: 'some value',
|
||||
}, "sent context should be correct");
|
||||
}
|
||||
@@ -7600,6 +7599,46 @@ QUnit.module('relational_fields', {
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('one2many with multiple pages and sequence field', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
this.data.partner.records[0].turtles = [3, 2, 1];
|
||||
this.data.partner.onchanges.turtles = function () {};
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch:'<form string="Partners">' +
|
||||
'<field name="turtles">' +
|
||||
'<tree limit="2">' +
|
||||
'<field name="turtle_int" widget="handle"/>' +
|
||||
'<field name="turtle_foo"/>' +
|
||||
'<field name="partner_ids" invisible="1"/>' +
|
||||
'</tree>' +
|
||||
'</field>' +
|
||||
'</form>',
|
||||
res_id: 1,
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === 'onchange') {
|
||||
return $.when({value: { turtles: [
|
||||
[5],
|
||||
[1, 1, {turtle_foo: "from onchange", partner_ids: [[5]]}],
|
||||
]}});
|
||||
}
|
||||
return this._super(route, args);
|
||||
},
|
||||
viewOptions: {
|
||||
mode: 'edit',
|
||||
},
|
||||
});
|
||||
form.$('span[name="delete"]').first().click();
|
||||
assert.strictEqual(form.$('.o_data_row').text(), 'from onchange',
|
||||
'onchange has been properly applied');
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.module('FieldMany2Many');
|
||||
|
||||
QUnit.test('many2many kanban: edition', function (assert) {
|
||||
|
||||
@@ -74,7 +74,6 @@ QUnit.module('Views', {
|
||||
mockRPC: function (route, args) {
|
||||
assert.deepEqual(args.kwargs.context, {
|
||||
active_field: 2,
|
||||
bin_size: true,
|
||||
someKey: 'some value',
|
||||
}, "should have sent the correct context");
|
||||
return this._super.apply(this, arguments);
|
||||
|
||||
@@ -199,7 +199,7 @@ QUnit.module('Views', {
|
||||
});
|
||||
|
||||
QUnit.test('only necessary fields are fetched with correct context', function (assert) {
|
||||
assert.expect(2);
|
||||
assert.expect(1);
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
@@ -214,8 +214,6 @@ QUnit.module('Views', {
|
||||
// field, not sure why. Maybe this test should be modified.
|
||||
assert.deepEqual(args.args[1], ["foo", "display_name"],
|
||||
"should only fetch requested fields");
|
||||
assert.deepEqual(args.kwargs.context, {bin_size: true},
|
||||
"bin_size should always be in the context");
|
||||
return this._super(route, args);
|
||||
}
|
||||
});
|
||||
@@ -483,6 +481,37 @@ QUnit.module('Views', {
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('autofocus on second notebook page', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<form string="Partners">' +
|
||||
'<sheet>' +
|
||||
'<field name="product_id"/>' +
|
||||
'<notebook>' +
|
||||
'<page string="Choucroute">' +
|
||||
'<field name="foo"/>' +
|
||||
'</page>' +
|
||||
'<page string="Cassoulet" autofocus="autofocus">' +
|
||||
'<field name="bar"/>' +
|
||||
'</page>' +
|
||||
'</notebook>' +
|
||||
'</sheet>' +
|
||||
'</form>',
|
||||
res_id: 1,
|
||||
});
|
||||
|
||||
assert.notOk(form.$('.o_notebook .nav li:first()').hasClass('active'),
|
||||
'first tab should not active');
|
||||
assert.ok(form.$('.o_notebook .nav li:nth(1)').hasClass('active'),
|
||||
'second tab should be active');
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('invisible attrs on group are re-evaluated on field change', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
|
||||
@@ -493,6 +493,58 @@ QUnit.module('Views', {
|
||||
kanban.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('quick create several records in a row', function (assert) {
|
||||
assert.expect(6);
|
||||
|
||||
var kanban = createView({
|
||||
View: KanbanView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<kanban class="o_kanban_test" on_create="quick_create">' +
|
||||
'<field name="bar"/>' +
|
||||
'<templates><t t-name="kanban-box">' +
|
||||
'<div><field name="foo"/></div>' +
|
||||
'</t></templates></kanban>',
|
||||
groupBy: ['bar'],
|
||||
});
|
||||
|
||||
assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record').length, 1,
|
||||
"first column should contain one record");
|
||||
|
||||
// click to add an element, fill the input and press ENTER
|
||||
kanban.$('.o_kanban_header .o_kanban_quick_add i').first().click();
|
||||
|
||||
assert.strictEqual(kanban.$('.o_kanban_quick_create').length, 1,
|
||||
"the quick create should be open");
|
||||
|
||||
kanban.$('.o_kanban_quick_create input')
|
||||
.val('new partner 1')
|
||||
.trigger($.Event('keypress', {
|
||||
which: $.ui.keyCode.ENTER,
|
||||
keyCode: $.ui.keyCode.ENTER,
|
||||
}));
|
||||
|
||||
assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record').length, 2,
|
||||
"first column should now contain two records");
|
||||
assert.strictEqual(kanban.$('.o_kanban_quick_create').length, 1,
|
||||
"the quick create should still be open");
|
||||
|
||||
// create a second element in a row
|
||||
kanban.$('.o_kanban_quick_create input')
|
||||
.val('new partner 2')
|
||||
.trigger($.Event('keypress', {
|
||||
which: $.ui.keyCode.ENTER,
|
||||
keyCode: $.ui.keyCode.ENTER,
|
||||
}));
|
||||
|
||||
assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record').length, 3,
|
||||
"first column should now contain three records");
|
||||
assert.strictEqual(kanban.$('.o_kanban_quick_create').length, 1,
|
||||
"the quick create should still be open");
|
||||
|
||||
kanban.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('quick create fail in grouped', function (assert) {
|
||||
assert.expect(7);
|
||||
|
||||
@@ -524,7 +576,6 @@ QUnit.module('Views', {
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
kanban.renderButtons();
|
||||
|
||||
assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record').length, 2,
|
||||
"there should be 2 records in first column");
|
||||
@@ -556,6 +607,48 @@ QUnit.module('Views', {
|
||||
kanban.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('quick create record in empty grouped kanban', function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
var kanban = createView({
|
||||
View: KanbanView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<kanban on_create="quick_create">' +
|
||||
'<field name="product_id"/>' +
|
||||
'<templates><t t-name="kanban-box">' +
|
||||
'<div><field name="foo"/></div>' +
|
||||
'</t></templates>' +
|
||||
'</kanban>',
|
||||
groupBy: ['product_id'],
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === 'read_group') {
|
||||
// override read_group to return empty groups, as this is
|
||||
// the case for several models (e.g. project.task grouped
|
||||
// by stage_id)
|
||||
var result = [
|
||||
{__domain: [['product_id', '=', 3]], product_id_count: 0},
|
||||
{__domain: [['product_id', '=', 5]], product_id_count: 0},
|
||||
];
|
||||
return $.when(result);
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
|
||||
assert.strictEqual(kanban.$('.o_kanban_group').length, 2,
|
||||
"there should be 2 columns");
|
||||
assert.strictEqual(kanban.$('.o_kanban_record').length, 0,
|
||||
"both columns should be empty");
|
||||
|
||||
kanban.$buttons.find('.o-kanban-button-new').click();
|
||||
|
||||
assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_quick_create').length, 1,
|
||||
"should have opened the quick create in the first column");
|
||||
|
||||
kanban.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('many2many_tags in kanban views', function (assert) {
|
||||
assert.expect(12);
|
||||
|
||||
|
||||
@@ -388,9 +388,12 @@ eventHandler.modules.imageDialog.showImageDialog = function ($editable) {
|
||||
if (r.sc.tagName && r.sc.childNodes.length) {
|
||||
r.sc = r.sc.childNodes[r.so];
|
||||
}
|
||||
var media = $(r.sc).parents().addBack().filter(function (i, el) {
|
||||
return dom.isImg(el);
|
||||
})[0];
|
||||
core.bus.trigger('media_dialog_demand', {
|
||||
$editable: $editable,
|
||||
media: dom.isImg(r.sc) ? r.sc : null,
|
||||
media: media,
|
||||
});
|
||||
return new $.Deferred().reject();
|
||||
};
|
||||
|
||||
@@ -172,18 +172,18 @@ return core.Class.extend(mixins.EventDispatcherMixin, ServicesMixin, {
|
||||
/**
|
||||
* Registers a tour described by the following arguments *in order*
|
||||
*
|
||||
* @param [string] name - tour's name
|
||||
* @param [Object] - options (optional), available options are:
|
||||
* @param [boolean] - true if this is only for tests
|
||||
* @param [boolean]
|
||||
* @param {string} name - tour's name
|
||||
* @param {Object} [options] - options (optional), available options are:
|
||||
* @param {boolean} [options.test=false] - true if this is only for tests
|
||||
* @param {boolean} [options.skip_enabled=false]
|
||||
* true to add a link in its tips to consume the whole tour
|
||||
* @param [string]
|
||||
* @param {string} [options.url]
|
||||
* the url to load when manually running the tour
|
||||
* @param [boolean]
|
||||
* @param {boolean} [options.rainbowMan=true]
|
||||
* whether or not the rainbowman must be shown at the end of the tour
|
||||
* @param [Deferred]
|
||||
* @param {Deferred} [options.wait_for]
|
||||
* indicates when the tour can be started
|
||||
* @param [Object] steps - steps' descriptions, each step being an object
|
||||
* @param {Object[]} steps - steps' descriptions, each step being an object
|
||||
* containing a tip description
|
||||
*/
|
||||
register: function() {
|
||||
|
||||
@@ -66,17 +66,17 @@ class Website(Home):
|
||||
@http.route('/', type='http', auth="public", website=True)
|
||||
def index(self, **kw):
|
||||
homepage = request.website.homepage_id
|
||||
if homepage and homepage.url != '/':
|
||||
if homepage and (homepage.sudo().is_visible or request.env.user.has_group('base.group_user')) and homepage.url != '/':
|
||||
return request.env['ir.http'].reroute(homepage.url)
|
||||
|
||||
website_page = request.env['ir.http']._serve_page()
|
||||
if website_page:
|
||||
return website_page
|
||||
else:
|
||||
top_menu = request.website.sudo().menu_id
|
||||
first_menu = top_menu and top_menu.child_id and top_menu.child_id[0]
|
||||
if first_menu and first_menu.url != '/' and (not (first_menu.url.startswith(('/?', '/#')))):
|
||||
return request.redirect(first_menu.url)
|
||||
top_menu = request.website.menu_id
|
||||
first_menu = top_menu and top_menu.child_id and top_menu.child_id.filtered(lambda menu: menu.is_visible)
|
||||
if first_menu and first_menu[0].url != '/' and (not (first_menu[0].url.startswith(('/?', '/#')))):
|
||||
return request.redirect(first_menu[0].url)
|
||||
|
||||
raise request.not_found()
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ class Http(models.AbstractModel):
|
||||
if not request.session.uid:
|
||||
env = api.Environment(request.cr, SUPERUSER_ID, request.context)
|
||||
website = env['website'].get_current_website()
|
||||
if website:
|
||||
if website and website.user_id:
|
||||
request.uid = website.user_id.id
|
||||
if not request.uid:
|
||||
super(Http, cls)._auth_method_public()
|
||||
|
||||
@@ -63,7 +63,7 @@ class Website(models.Model):
|
||||
google_management_client_id = fields.Char('Google Client ID')
|
||||
google_management_client_secret = fields.Char('Google Client Secret')
|
||||
|
||||
user_id = fields.Many2one('res.users', string='Public User', default=lambda self: self.env.ref('base.public_user').id)
|
||||
user_id = fields.Many2one('res.users', string='Public User', required=True, default=lambda self: self.env.ref('base.public_user').id)
|
||||
cdn_activated = fields.Boolean('Activate CDN for assets')
|
||||
cdn_url = fields.Char('CDN Base URL', default='')
|
||||
cdn_filters = fields.Text('CDN Filters', default=lambda s: '\n'.join(DEFAULT_CDN_FILTERS), help="URL matching those filters will be rewritten using the CDN Base URL")
|
||||
@@ -791,7 +791,7 @@ class Menu(models.Model):
|
||||
|
||||
_parent_store = True
|
||||
_parent_order = 'sequence'
|
||||
_order = "sequence"
|
||||
_order = "sequence, id"
|
||||
|
||||
def _default_sequence(self):
|
||||
menu = self.search([], limit=1, order="sequence DESC")
|
||||
@@ -807,6 +807,14 @@ class Menu(models.Model):
|
||||
child_id = fields.One2many('website.menu', 'parent_id', string='Child Menus')
|
||||
parent_left = fields.Integer('Parent Left', index=True)
|
||||
parent_right = fields.Integer('Parent Rigth', index=True)
|
||||
is_visible = fields.Boolean(compute='_compute_visible', string='Is Visible')
|
||||
|
||||
@api.one
|
||||
def _compute_visible(self):
|
||||
visible = True
|
||||
if self.page_id and not self.page_id.sudo().is_visible and not self.user_has_groups('base.group_user'):
|
||||
visible = False
|
||||
self.is_visible = visible
|
||||
|
||||
@api.model
|
||||
def clean_url(self):
|
||||
@@ -815,7 +823,7 @@ class Menu(models.Model):
|
||||
url = self.page_id.sudo().url
|
||||
else:
|
||||
url = self.url
|
||||
if not self.url.startswith('/'):
|
||||
if url and not self.url.startswith('/'):
|
||||
if '@' in self.url:
|
||||
if not self.url.startswith('mailto'):
|
||||
url = 'mailto:%s' % self.url
|
||||
|
||||
@@ -67,14 +67,14 @@
|
||||
|
||||
<!-- Layout -->
|
||||
<template id="submenu" name="Submenu">
|
||||
<li t-if="not submenu.child_id" t-att-class="
|
||||
<li t-if="submenu.is_visible and not submenu.child_id.filtered(lambda menu: menu.is_visible)" t-att-class="
|
||||
'active' if submenu.clean_url() and unslug_url(request.httprequest.path) == unslug_url(submenu.clean_url()) else None
|
||||
">
|
||||
<a t-att-href="submenu.clean_url()" t-ignore="true" t-att-target="'_blank' if submenu.new_window else None">
|
||||
<span t-field="submenu.name"/>
|
||||
</a>
|
||||
</li>
|
||||
<li t-if="submenu.child_id" t-attf-class="dropdown #{
|
||||
<li t-if="submenu.is_visible and submenu.child_id.filtered(lambda menu: menu.is_visible)" t-attf-class="dropdown #{
|
||||
(submenu.clean_url() and submenu.clean_url() != '/' and any([request.httprequest.path == child.url for child in submenu.child_id if child.url]) or
|
||||
(submenu.clean_url() and request.httprequest.path == submenu.clean_url())) and 'active'
|
||||
}">
|
||||
|
||||
@@ -173,7 +173,9 @@ class WebsiteBlog(http.Controller):
|
||||
v = {}
|
||||
v['blog'] = blog
|
||||
v['base_url'] = request.env['ir.config_parameter'].sudo().get_param('web.base.url')
|
||||
v['posts'] = request.env['blog.post'].search([('blog_id','=', blog.id)], limit=min(int(limit), 50))
|
||||
v['posts'] = request.env['blog.post'].search([('blog_id','=', blog.id)],
|
||||
limit=min(int(limit), 50),
|
||||
order="post_date DESC")
|
||||
v['html2plaintext'] = html2plaintext
|
||||
r = request.render("website_blog.blog_feed", v, headers=[('Content-Type', 'application/atom+xml')])
|
||||
return r
|
||||
|
||||
@@ -151,7 +151,7 @@ class BlogPost(models.Model):
|
||||
write_date = fields.Datetime('Last Modified on', index=True, readonly=True)
|
||||
write_uid = fields.Many2one('res.users', 'Last Contributor', index=True, readonly=True)
|
||||
author_avatar = fields.Binary(related='author_id.image_small', string="Avatar")
|
||||
visits = fields.Integer('No of Views')
|
||||
visits = fields.Integer('No of Views', copy=False)
|
||||
ranking = fields.Float(compute='_compute_ranking', string='Ranking')
|
||||
|
||||
@api.multi
|
||||
|
||||
@@ -548,14 +548,14 @@
|
||||
<title t-esc="blog.name"/>
|
||||
<link t-att-href="'%s/blog/%s' % (base_url ,blog.id)"/>
|
||||
<id t-esc="'%s/blog/%s' % (base_url, blog.id)"/>
|
||||
<updated t-esc="(posts[0] if posts else blog).write_date.replace(' ', 'T') + 'Z'"/>
|
||||
<updated t-esc="(posts[0] if posts else blog).post_date.replace(' ', 'T') + 'Z'"/>
|
||||
<entry t-foreach="posts" t-as="post">
|
||||
<title t-esc="post.name"/>
|
||||
<link t-att-href="'%s%s' % (base_url, post.website_url)"/>
|
||||
<id t-esc="'%s%s' % (base_url, post.website_url)"/>
|
||||
<author><name t-esc="post.sudo().author_id.name"/></author>
|
||||
<summary t-esc="html2plaintext(post.teaser)"/>
|
||||
<updated t-esc="post.write_date.replace(' ', 'T') + 'Z'"/>
|
||||
<updated t-esc="post.post_date.replace(' ', 'T') + 'Z'"/>
|
||||
</entry>
|
||||
</feed>
|
||||
</template>
|
||||
|
||||
@@ -67,11 +67,13 @@ class WebsiteEventTrackController(http.Controller):
|
||||
|
||||
@http.route(['''/event/<model("event.event"):event>/agenda'''], type='http', auth="public", website=True, sitemap=False)
|
||||
def event_agenda(self, event, tag=None, **post):
|
||||
local_tz = pytz.timezone(event.date_tz or 'UTC')
|
||||
days_tracks = collections.defaultdict(lambda: [])
|
||||
for track in event.track_ids.sorted(lambda track: (track.date or '', bool(track.location_id))):
|
||||
if not track.date:
|
||||
continue
|
||||
days_tracks[track.date[:10]].append(track)
|
||||
date = fields.Datetime.from_string(track.date).replace(tzinfo=pytz.utc).astimezone(local_tz)
|
||||
days_tracks[str(date)[:10]].append(track)
|
||||
|
||||
days = {}
|
||||
tracks_by_days = {}
|
||||
|
||||
@@ -215,7 +215,7 @@ odoo.define('website_sale.website_sale', function (require) {
|
||||
return false;
|
||||
});
|
||||
|
||||
$('.oe_website_sale .a-submit, #comment .a-submit').off('click').on('click', function (event) {
|
||||
$('.oe_website_sale, #comment').off('click', '.a-submit').on('click', '.a-submit', function (event) {
|
||||
if (!event.isDefaultPrevented() && !$(this).is(".disabled")) {
|
||||
event.preventDefault();
|
||||
$(this).closest('form').submit();
|
||||
|
||||
@@ -35,7 +35,7 @@ $('.oe_website_sale #add_to_cart, .oe_website_sale #products_grid .a-submit')
|
||||
$(this).remove();
|
||||
});
|
||||
|
||||
$modal.on('click', '.a-submit', function () {
|
||||
$modal.on('click', '.a-submit', function (ev) {
|
||||
var $a = $(this);
|
||||
$form.ajaxSubmit({
|
||||
url: '/shop/cart/update_option',
|
||||
@@ -50,6 +50,7 @@ $('.oe_website_sale #add_to_cart, .oe_website_sale #products_grid .a-submit')
|
||||
}
|
||||
});
|
||||
$modal.modal('hide');
|
||||
ev.preventDefault();
|
||||
});
|
||||
|
||||
$modal.on('click', '.css_attribute_color input', function (event) {
|
||||
|
||||
@@ -565,6 +565,7 @@ def extract_subtypes(parent_name, doc):
|
||||
'name': typename,
|
||||
'doc': param.doc,
|
||||
'_members': [
|
||||
# TODO: add default value
|
||||
(sub.name, jsdoc.PropertyDoc(dict(sub.to_dict(), sourcemodule=doc['sourcemodule'])))
|
||||
for sub in subs
|
||||
],
|
||||
|
||||
@@ -122,8 +122,14 @@ class FunctionDoc(CommentDoc):
|
||||
@property
|
||||
def params(self):
|
||||
tag_texts = self.get_as_list('param')
|
||||
if self.get('guessed_params') is None:
|
||||
return [ParamDoc(text) for text in tag_texts]
|
||||
# turns out guessed_params is *almost* (?) always set to a list,
|
||||
# if empty list of guessed params fall back to @params
|
||||
if not self['guessed_params']:
|
||||
# only get "primary" params (no "." in name)
|
||||
return [
|
||||
p for p in map(ParamDoc, tag_texts)
|
||||
if '.' not in p.name
|
||||
]
|
||||
else:
|
||||
param_dict = {}
|
||||
for text in tag_texts:
|
||||
|
||||
@@ -12,4 +12,4 @@ Robin Lucbenet robin@meta-it.fr https://github.com/n1b0r
|
||||
|
||||
List of contributors:
|
||||
|
||||
Guillaume Masson guillaume.masson@meta-it.fr
|
||||
Guillaume Masson guillaume.masson@meta-it.fr https://github.com/metaminux
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
Costa Rica, 2018-01-11
|
||||
|
||||
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,
|
||||
|
||||
Randall Castro randall_castro@me.com https://github.com/rcastro-tyc
|
||||
@@ -75,6 +75,10 @@ in each method::
|
||||
|
||||
# other tests...
|
||||
|
||||
.. note::
|
||||
|
||||
Test methods must start with ``test_``
|
||||
|
||||
.. autoclass:: odoo.tests.common.Form
|
||||
:members:
|
||||
|
||||
|
||||
@@ -442,7 +442,12 @@ class IrActionsServer(models.Model):
|
||||
for exp in action.fields_lines:
|
||||
res[exp.col1.name] = exp.eval_value(eval_context=eval_context)[exp.id]
|
||||
|
||||
self.env[action.model_id.model].browse(self._context.get('active_id')).write(res)
|
||||
if self._context.get('onchange_self'):
|
||||
record_cached = self._context['onchange_self']
|
||||
for field, new_value in res.items():
|
||||
record_cached[field] = new_value
|
||||
else:
|
||||
self.env[action.model_id.model].browse(self._context.get('active_id')).write(res)
|
||||
|
||||
@api.model
|
||||
def run_action_object_create(self, action, eval_context=None):
|
||||
@@ -537,6 +542,8 @@ class IrActionsServer(models.Model):
|
||||
|
||||
elif hasattr(self, 'run_action_%s' % action.state):
|
||||
active_id = self._context.get('active_id')
|
||||
if not active_id and self._context.get('onchange_self'):
|
||||
active_id = self._context['onchange_self']._origin.id
|
||||
active_ids = self._context.get('active_ids', [active_id] if active_id else [])
|
||||
for active_id in active_ids:
|
||||
# run context dedicated to a particular active_id
|
||||
|
||||
@@ -215,7 +215,7 @@ class ir_cron(models.Model):
|
||||
_logger.debug("Job `%s` already executed by another process/thread. skipping it", job['cron_name'])
|
||||
continue
|
||||
# Got the lock on the job row, run its code
|
||||
_logger.debug('Starting job `%s`.', job['cron_name'])
|
||||
_logger.info('Starting job `%s`.', job['cron_name'])
|
||||
job_cr = db.cursor()
|
||||
try:
|
||||
registry = odoo.registry(db_name)
|
||||
|
||||
@@ -247,7 +247,7 @@ actual arch.
|
||||
if fullpath:
|
||||
arch_fs = get_view_arch_from_file(fullpath, view.xml_id)
|
||||
# replace %(xml_id)s, %(xml_id)d, %%(xml_id)s, %%(xml_id)d by the res_id
|
||||
arch_fs = arch_fs and resolve_external_ids(arch_fs, view.xml_id)
|
||||
arch_fs = arch_fs and resolve_external_ids(arch_fs, view.xml_id).replace('%%', '%')
|
||||
else:
|
||||
_logger.warning("View %s: Full path [%s] cannot be found.", view.xml_id, view.arch_fs)
|
||||
arch_fs = False
|
||||
|
||||
+8
-5
@@ -1004,11 +1004,14 @@ class Cache(object):
|
||||
|
||||
def get_records(self, model, field):
|
||||
""" Return the records of ``model`` that have a value for ``field``. """
|
||||
browse = model.browse
|
||||
ids = [record_id
|
||||
for record_id, field_record_cache in self._data[field].items()
|
||||
if field.cache_key(browse(record_id)) in field_record_cache]
|
||||
return browse(ids)
|
||||
key = field.cache_key(model)
|
||||
# optimization: do not field.cache_key(record) for each record in cache
|
||||
ids = [
|
||||
record_id
|
||||
for record_id, field_record_cache in self._data[field].items()
|
||||
if key in field_record_cache
|
||||
]
|
||||
return model.browse(ids)
|
||||
|
||||
def invalidate(self, spec=None):
|
||||
""" Invalidate the cache, partially or totally depending on ``spec``. """
|
||||
|
||||
@@ -763,6 +763,9 @@ class Field(MetaField('DummyField', (object,), {})):
|
||||
cache, the full cache key being ``(self, record.id, key)``.
|
||||
"""
|
||||
env = record.env
|
||||
# IMPORTANT: odoo.api.Cache.get_records() depends on the fact that the
|
||||
# result does not depend on record.id. If you ever make the following
|
||||
# dependent on record.id, don't forget to fix the other method!
|
||||
return env if self.context_dependent else (env.cr, env.uid)
|
||||
|
||||
def null(self, record):
|
||||
|
||||
+5
-1
@@ -231,9 +231,13 @@ def dump_db(db_name, stream, backup_format='zip'):
|
||||
|
||||
@check_db_management_enabled
|
||||
def exp_restore(db_name, data, copy=False):
|
||||
def chunks(d, n=8192):
|
||||
for i in range(0, len(d), n):
|
||||
yield d[i:i+n]
|
||||
data_file = tempfile.NamedTemporaryFile(delete=False)
|
||||
try:
|
||||
data_file.write(base64.b64decode(data))
|
||||
for chunk in chunks(data):
|
||||
data_file.write(base64.b64decode(chunk))
|
||||
data_file.close()
|
||||
restore_db(db_name, data_file.name, copy=copy)
|
||||
finally:
|
||||
|
||||
Reference in New Issue
Block a user