[MERGE] forward port branch 11.0 up to 3b80a059ff

This commit is contained in:
Christophe Simonis
2018-01-12 19:03:00 +01:00
62 changed files with 463 additions and 180 deletions
+9 -11
View File
@@ -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 &amp;&amp; !(state.reconciliation_proposition[0] || {}).partial_reconcile">
<tr t-if="state.balance.amount_currency &amp;&amp; !(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 &lt; 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 &lt; 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 &gt; 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 &lt; 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 &lt; 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 &gt; 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"/>
+1 -1
View File
@@ -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 -->
+9 -9
View File
@@ -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}),
});
}
+1 -1
View File
@@ -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>
+1 -1
View File
@@ -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;
+3 -1
View File
@@ -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
+1 -1
View File
@@ -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>
+1
View File
@@ -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>
+1 -1
View File
@@ -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',
-1
View File
@@ -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
+4
View File
@@ -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"
+11 -5
View File
@@ -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):
+17 -1
View File
@@ -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.
-3
View File
@@ -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);
+32 -3
View File
@@ -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);
+94 -1
View File
@@ -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() {
+5 -5
View File
@@ -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()
+1 -1
View File
@@ -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()
+11 -3
View File
@@ -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
+2 -2
View File
@@ -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'
}">
+3 -1
View File
@@ -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
+1 -1
View File
@@ -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
],
+8 -2
View File
@@ -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:
+1 -1
View File
@@ -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
+11
View File
@@ -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
+4
View File
@@ -75,6 +75,10 @@ in each method::
# other tests...
.. note::
Test methods must start with ``test_``
.. autoclass:: odoo.tests.common.Form
:members:
+8 -1
View File
@@ -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
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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
View File
@@ -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``. """
+3
View File
@@ -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
View File
@@ -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: