[MERGE] forward port branch 10.0 up to 6466aa4

This commit is contained in:
Christophe Simonis
2016-10-19 18:16:14 +02:00
80 changed files with 956 additions and 408 deletions
+6 -6
View File
@@ -1,7 +1,7 @@
[![Build Status](http://runbot.odoo.com/runbot/badge/flat/1/master.svg)](http://runbot.odoo.com/runbot)
[![Tech Doc](http://img.shields.io/badge/master-docs-8f8f8f.svg?style=flat)](http://www.odoo.com/documentation/master)
[![Help](http://img.shields.io/badge/master-help-8f8f8f.svg?style=flat)](https://www.odoo.com/forum/help-1)
[![Nightly Builds](http://img.shields.io/badge/master-nightly-8f8f8f.svg?style=flat)](http://nightly.odoo.com/)
[![Tech Doc](http://img.shields.io/badge/master-docs-875A7B.svg?style=flat)](http://www.odoo.com/documentation/master)
[![Help](http://img.shields.io/badge/master-help-875A7B.svg?style=flat)](https://www.odoo.com/forum/help-1)
[![Nightly Builds](http://img.shields.io/badge/master-nightly-875A7B.svg?style=flat)](http://nightly.odoo.com/)
Odoo
----
@@ -27,14 +27,14 @@ a full-featured <a href="https://www.odoo.com">Open Source ERP</a> when you inst
Getting started with Odoo
-------------------------
For a standard installation please follow the <a href="https://www.odoo.com/documentation/9.0/setup/install.html">Setup instructions</a>
For a standard installation please follow the <a href="https://www.odoo.com/documentation/master/setup/install.html">Setup instructions</a>
from the documentation.
If you are a developer you may type the following command at your terminal:
wget -O- https://raw.githubusercontent.com/odoo/odoo/10.0/setup/setup_dev.py | python
wget -O- https://raw.githubusercontent.com/odoo/odoo/master/setup/setup_dev.py | python
Then follow <a href="https://www.odoo.com/documentation/9.0/tutorials.html">the developer tutorials</a>
Then follow <a href="https://www.odoo.com/documentation/master/tutorials.html">the developer tutorials</a>
For Odoo employees
+4
View File
@@ -731,6 +731,9 @@ class AccountTax(models.Model):
else:
total_included += tax_amount
# Keep base amount used for the current tax
tax_base = base
if tax.include_base_amount:
base += tax_amount
@@ -738,6 +741,7 @@ class AccountTax(models.Model):
'id': tax.id,
'name': tax.with_context(**{'lang': partner.lang} if partner else {}).name,
'amount': tax_amount,
'base': tax_base,
'sequence': tax.sequence,
'account_id': tax.account_id.id,
'refund_account_id': tax.refund_account_id.id,
@@ -250,13 +250,13 @@ class AccountBankStatement(models.Model):
context = dict(self._context or {})
context['journal_id'] = self.journal_id.id
return {
'name': _('Journal Items'),
'name': _('Journal Entries'),
'view_type': 'form',
'view_mode': 'tree,form',
'res_model': 'account.move.line',
'res_model': 'account.move',
'view_id': False,
'type': 'ir.actions.act_window',
'domain': [('statement_id', 'in', self.ids)],
'domain': [('id', 'in', self.mapped('move_line_ids').mapped('move_id').ids)],
'context': context,
}
@@ -366,6 +366,7 @@ class AccountBankStatementLine(models.Model):
journal_entry_ids = fields.One2many('account.move', 'statement_line_id', 'Journal Entries', copy=False, readonly=True)
amount_currency = fields.Monetary(help="The amount expressed in an optional other currency if it is a multi-currency entry.")
currency_id = fields.Many2one('res.currency', string='Currency', help="The optional other currency if it is a multi-currency entry.")
state = fields.Selection(related='statement_id.state' , string='Status', readonly=True)
move_name = fields.Char(string='Journal Entry Name', readonly=True,
default=False, copy=False,
help="Technical field holding the number given to the journal entry, automatically set when the statement line is reconciled then stored to set the same number again if the line is cancelled, set to draft and re-processed again.")
+36 -18
View File
@@ -12,6 +12,9 @@ from odoo.tools.misc import formatLang
from odoo.exceptions import UserError, RedirectWarning, ValidationError
import odoo.addons.decimal_precision as dp
import logging
_logger = logging.getLogger(__name__)
# mapping invoice type to journal type
TYPE2JOURNAL = {
@@ -592,6 +595,25 @@ class AccountInvoice(models.Model):
else:
return self.env.ref('account.invoice_form').id
def _prepare_tax_line_vals(self, line, tax):
""" Prepare values to create an account.invoice.tax line
The line parameter is an account.invoice.line, and the
tax parameter is the output of account.tax.compute_all().
"""
vals = {
'invoice_id': self.id,
'name': tax['name'],
'tax_id': tax['id'],
'amount': tax['amount'],
'base': tax['base'],
'manual': False,
'sequence': tax['sequence'],
'account_analytic_id': tax['analytic'] and line.account_analytic_id.id or False,
'account_id': self.type in ('out_invoice', 'in_invoice') and (tax['account_id'] or line.account_id.id) or (tax['refund_account_id'] or line.account_id.id),
}
return vals
@api.multi
def get_taxes_values(self):
tax_grouped = {}
@@ -599,16 +621,7 @@ class AccountInvoice(models.Model):
price_unit = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
taxes = line.invoice_line_tax_ids.compute_all(price_unit, self.currency_id, line.quantity, line.product_id, self.partner_id)['taxes']
for tax in taxes:
val = {
'invoice_id': self.id,
'name': tax['name'],
'tax_id': tax['id'],
'amount': tax['amount'],
'manual': False,
'sequence': tax['sequence'],
'account_analytic_id': tax['analytic'] and line.account_analytic_id.id or False,
'account_id': self.type in ('out_invoice', 'in_invoice') and (tax['account_id'] or line.account_id.id) or (tax['refund_account_id'] or line.account_id.id),
}
val = self._prepare_tax_line_vals(line, tax)
# If the taxes generate moves on the same financial account as the invoice line,
# propagate the analytic account from the invoice line to the tax line.
@@ -623,6 +636,7 @@ class AccountInvoice(models.Model):
tax_grouped[key] = val
else:
tax_grouped[key]['amount'] += val['amount']
tax_grouped[key]['base'] += val['base']
return tax_grouped
@api.multi
@@ -1336,15 +1350,19 @@ class AccountInvoiceTax(models.Model):
@api.depends('invoice_id.invoice_line_ids')
def _compute_base_amount(self):
tax_grouped = {}
for invoice in self.mapped('invoice_id'):
tax_grouped[invoice.id] = invoice.get_taxes_values()
for tax in self:
base = 0.0
for line in tax.invoice_id.invoice_line_ids:
if tax.tax_id in line.invoice_line_tax_ids:
price_unit = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
base += (line.invoice_line_tax_ids - tax.tax_id).compute_all(
price_unit, line.invoice_id.currency_id, line.quantity, line.product_id, line.invoice_id.partner_id
)['base']
tax.base = base
key = self.env['account.tax'].browse(tax.tax_id.id).get_grouping_key({
'tax_id': tax.tax_id.id,
'account_id': tax.account_id.id,
'account_analytic_id': tax.account_analytic_id.id,
})
if tax.invoice_id and key in tax_grouped[tax.invoice_id.id]:
tax.base = tax_grouped[tax.invoice_id.id][key]['base']
else:
_logger.warning('Tax Base Amount not computable probably due to a change in an underlying tax (%s).', tax.tax_id.name)
invoice_id = fields.Many2one('account.invoice', string='Invoice', ondelete='cascade', index=True)
name = fields.Char(string='Tax Description', required=True)
+7 -4
View File
@@ -1037,10 +1037,13 @@ class AccountMoveLine(models.Model):
# the provided values were not already multi-currency
if account.currency_id and 'amount_currency' not in vals and account.currency_id.id != account.company_id.currency_id.id:
vals['currency_id'] = account.currency_id.id
ctx = {}
if 'date' in vals:
ctx['date'] = vals['date']
vals['amount_currency'] = account.company_id.currency_id.with_context(ctx).compute(amount, account.currency_id)
if self._context.get('skip_full_reconcile_check') == 'amount_currency_excluded':
vals['amount_currency'] = 0.0
else:
ctx = {}
if 'date' in vals:
ctx['date'] = vals['date']
vals['amount_currency'] = account.company_id.currency_id.with_context(ctx).compute(amount, account.currency_id)
if not ok:
raise UserError(_('You cannot use this general account in this journal, check the tab \'Entry Controls\' on the related journal.'))
+4 -2
View File
@@ -39,7 +39,7 @@ def migrate_tags_on_taxes(cr, registry):
('description', '=', tax_template.description)
])
if len(tax_id.ids) == 1:
tax_id.sudo().write({'tag_ids': [(6, 0, [tax_template.tag_ids.ids])]})
tax_id.sudo().write({'tag_ids': [(6, 0, tax_template.tag_ids.ids)]})
# ---------------------------------------------------------------
# Account Templates: Account, Tax, Tax Code and chart. + Wizard
@@ -399,7 +399,9 @@ class AccountChartTemplate(models.Model):
:rtype: dict
"""
self.ensure_one()
account_reconcile_models = self.env['account.reconcile.model.template'].search([])
account_reconcile_models = self.env['account.reconcile.model.template'].search([
('account_id.chart_template_id', '=', self.id)
])
for account_reconcile_model in account_reconcile_models:
vals = {
'name': account_reconcile_model.name,
+1 -1
View File
@@ -445,5 +445,5 @@ class ResPartner(models.Model):
'''
action = self.env.ref('account.action_invoice_refund_out_tree')
result = action.read()[0]
result['domain'] = "[('id','in',[" + ','.join(map(str, self.ids)) + "])]"
result['domain'] = [('partner_id', 'in', self.ids)]
return result
@@ -41,3 +41,65 @@ class TestAccountSupplierInvoice(AccountingTestCase):
#I cancel the account move which is in posted state and verifies that it gives warning message
with self.assertRaises(Warning):
invoice.move_id.button_cancel()
def test_supplier_invoice2(self):
tax_fixed = self.env['account.tax'].create({
'sequence': 10,
'name': 'Tax 10.0 (Fixed)',
'amount': 10.0,
'amount_type': 'fixed',
'include_base_amount': True,
})
tax_percent_included_base_incl = self.env['account.tax'].create({
'sequence': 20,
'name': 'Tax 50.0% (Percentage of Price Tax Included)',
'amount': 50.0,
'amount_type': 'division',
'include_base_amount': True,
})
tax_percentage = self.env['account.tax'].create({
'sequence': 30,
'name': 'Tax 20.0% (Percentage of Price)',
'amount': 20.0,
'amount_type': 'percent',
'include_base_amount': False,
})
analytic_account = self.env['account.analytic.account'].create({
'name': 'test account',
})
# Should be changed by automatic on_change later
invoice_account = self.env['account.account'].search([('user_type_id', '=', self.env.ref('account.data_account_type_receivable').id)], limit=1).id
invoice_line_account = self.env['account.account'].search([('user_type_id', '=', self.env.ref('account.data_account_type_expenses').id)], limit=1).id
invoice = self.env['account.invoice'].create({'partner_id': self.env.ref('base.res_partner_2').id,
'account_id': invoice_account,
'type': 'in_invoice',
})
invoice_line = self.env['account.invoice.line'].create({'product_id': self.env.ref('product.product_product_4').id,
'quantity': 5.0,
'price_unit': 100.0,
'invoice_id': invoice.id,
'name': 'product that cost 100',
'account_id': invoice_line_account,
'invoice_line_tax_ids': [(6, 0, [tax_fixed.id, tax_percent_included_base_incl.id, tax_percentage.id])],
'account_analytic_id': analytic_account.id,
})
invoice.compute_taxes()
# check that Initially supplier bill state is "Draft"
self.assertTrue((invoice.state == 'draft'), "Initially vendor bill state is Draft")
#change the state of invoice to open by clicking Validate button
invoice.signal_workflow('invoice_open')
# Check if amount and corresponded base is correct for all tax scenarios given on a computational base
# Keep in mind that tax amount can be changed by the user at any time before validating (based on the invoice and tax laws applicable)
invoice_tax = invoice.tax_line_ids.sorted(key=lambda r: r.sequence)
self.assertEquals(invoice_tax.mapped('amount'), [50.0, 550.0, 220.0])
self.assertEquals(invoice_tax.mapped('base'), [500.0, 550.0, 1100.0])
#I cancel the account move which is in posted state and verifies that it gives warning message
with self.assertRaises(Warning):
invoice.move_id.button_cancel()
+2 -1
View File
@@ -556,7 +556,7 @@
<sheet>
<div class="oe_button_box" name="button_box">
<button class="oe_stat_button" name="button_journal_entries"
string="Journal Items" type="object"
string="Journal Entries" type="object"
attrs="{'invisible':[('move_line_ids','=',[])]}" icon="fa-bars"/>
<field name="move_line_ids" invisible="1"/>
</div>
@@ -884,6 +884,7 @@
<field name="type"/>
</group>
<group>
<field name="include_initial_balance"/>
</group>
</group>
<separator string="Description"/>
+1 -1
View File
@@ -102,7 +102,7 @@
icon="fa-pencil-square-o"
name="open_partner_history"
attrs="{'invisible': [('customer', '=', False)]}"
context="{'search_default_partner_id': active_id,'default_partner_id': active_id}">
context="{'default_partner_id': active_id}">
<div class="o_form_field o_stat_info">
<span class="o_stat_value"><field name="total_invoiced" widget='monetary' options="{'currency_field': 'currency_id'}"/></span>
<span class="o_stat_text">Invoiced</span>
+1 -1
View File
@@ -447,7 +447,7 @@
<span class="fa fa-copy fa-4x"></span>
<h3><strong>Contracts &amp; Subscriptions</strong></h3>
<p>Manage time &amp; material contracts or fixed-price recurring subscriptions.</p>
<a t-att-href="prepare_backend_url('base.open_module_tree', 'form', 'account_analytic_analysis')"
<a t-att-href="prepare_backend_url('base.open_module_tree', 'form', 'sale_contract')"
class="btn odoo_purple">
<span class="fa fa-arrow-circle-o-down"/> Install Now
</a>
+3 -3
View File
@@ -153,7 +153,7 @@ class AccountAssetAsset(models.Model):
else:
if self.method == 'linear':
amount = amount_to_depr / (undone_dotation_number - len(posted_depreciation_line_ids))
if self.prorata and self.category_id.type == 'purchase':
if self.prorata:
amount = amount_to_depr / self.method_number
if sequence == 1:
days = (self.company_id.compute_fiscalyear_dates(depreciation_date)['date_to'] - depreciation_date).days + 1
@@ -174,7 +174,7 @@ class AccountAssetAsset(models.Model):
while depreciation_date <= end_date:
depreciation_date = date(depreciation_date.year, depreciation_date.month, depreciation_date.day) + relativedelta(months=+self.method_period)
undone_dotation_number += 1
if self.prorata and self.category_id.type == 'purchase':
if self.prorata:
undone_dotation_number += 1
return undone_dotation_number
@@ -378,7 +378,7 @@ class AccountAssetAsset(models.Model):
@api.multi
def _compute_entries(self, date, group_entries=False):
depreciation_ids = self.env['account.asset.depreciation.line'].with_context(depreciation_date=date).search([
depreciation_ids = self.env['account.asset.depreciation.line'].search([
('asset_id', 'in', self.ids), ('depreciation_date', '<=', date),
('move_check', '=', False)])
if group_entries:
@@ -53,7 +53,8 @@
<attribute name="options">{'reload_on_button': true}</attribute>
</field>
<xpath expr="//field[@name='bank_account_id']" position="after">
<button name="button_cancel_reconciliation" attrs="{'invisible': [('journal_entry_ids', '=', [])]}" string="Cancel" type="object" icon="fa-undo text-warning"/>
<field name="state" invisible="1"/>
<button name="button_cancel_reconciliation" attrs="{'invisible': ['|',('journal_entry_ids', '=', []), ('state', '=', 'confirm')]}" string="Cancel" type="object" icon="fa fa-undo text-warning"/>
</xpath>
</field>
</record>
@@ -7,6 +7,7 @@ var common = require('web.form_common');
var BarcodeEvents = require('barcodes.BarcodeEvents');
var BarcodeHandlerMixin = require('barcodes.BarcodeHandlerMixin');
var KanbanRecord = require('web_kanban.Record');
var Dialog = require('web.Dialog');
var _t = core._t;
@@ -77,22 +78,38 @@ var FormViewBarcodeHandler = common.AbstractField.extend(BarcodeHandlerMixin, {
} else {
var field = this.form_view.fields[this.m2x_field];
var view = field.viewmanager.active_view;
var $content = $('<div>').append($('<input>', {type: 'text', class: 'o_set_qty_input'}));
if (this.last_scanned_barcode) {
var new_qty = window.prompt(_t('Set quantity'), character) || "0";
new_qty = new_qty.replace(',', '.');
var record = this._get_records(field).find(function(record) {
return record.get('product_barcode') === self.last_scanned_barcode;
this.dialog = new Dialog(this, {
title: _t('Set quantity'),
buttons: [{text: _t('Select'), classes: 'btn-primary', close: true, click: function () {
var new_qty = this.$content.find('.o_set_qty_input').val();
var record = _.find(self._get_records(field), function (record) {
return record.get('product_barcode') === self.last_scanned_barcode;
});
if (record) {
var values = {};
values[self.quantity_field] = parseFloat(new_qty);
field.data_update(record.get('id'), values).then(function () {
view.controller.reload_record(record);
});
} else {
self._display_no_last_scanned_warning();
}
}}, {text: _t('Discard'), close: true}],
$content: $content,
}).open();
// This line set the value of the key which triggered the _set_quantity in the input
this.dialog.$content.find('.o_set_qty_input').focus().val(character);
var $selectBtn = this.dialog.$footer.find('.btn-primary');
core.bus.on('keypress', this.dialog, function(event){
if (event.which === 13) {
event.preventDefault();
$selectBtn.click();
}
});
if (record) {
var values = {};
values[this.quantity_field] = parseFloat(new_qty);
field.data_update(record.get('id'), values).then(function () {
view.controller.reload_record(record);
});
} else {
this._display_no_last_scanned_warning();
}
} else {
this._display_no_last_scanned_warning();
}
@@ -159,8 +176,13 @@ var FormViewBarcodeHandler = common.AbstractField.extend(BarcodeHandlerMixin, {
},
_get_records: function(field) {
return field.viewmanager.active_view.controller.records || // tree view
field.viewmanager.active_view.controller.widgets; // kanban view
var active_view = field.viewmanager.active_view;
if (active_view.type === "kanban") {
return active_view.controller.widgets;
} else {
// tree view case
return active_view.controller.records.records;
}
},
});
@@ -198,7 +198,14 @@ class BaseActionRule(models.Model):
# subscribe followers
if self.act_followers and hasattr(records, 'message_subscribe'):
records.message_subscribe(self.act_followers.ids)
followers = self.env['mail.followers'].sudo().search(
[('res_model', '=', records._name),
('res_id', 'in', records.ids),
('partner_id', 'in', self.act_followers.ids),
]
)
if not len(followers) == len(self.act_followers):
records.message_subscribe(self.act_followers.ids)
# execute server actions
if self.server_action_ids:
+37 -23
View File
@@ -14,6 +14,7 @@ import re
from odoo import api, fields, models
from odoo.tools.translate import _
from odoo.tools.mimetypes import guess_mimetype
from odoo.tools.misc import ustr
from odoo.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT
try:
@@ -346,29 +347,37 @@ class Import(models.TransientModel):
if not date_patterns:
date_patterns = [pattern.replace('r', sep) for sep in separator for pattern in date_format]
date_patterns.extend([p.replace('Y', 'y') for p in date_patterns])
datetime_patterns = [options['datetime_format']] if options.get('datetime_format') else []
if not datetime_patterns:
datetime_patterns = [pattern + ' %H:%M:%S' for pattern in date_patterns]
current_date_pattern = False
for date_pattern in date_patterns:
date_ok = True
datetime_ok = False
for val in preview_values:
if val == '':
continue
try:
dt.strptime(val, date_pattern)
except ValueError:
date_ok = False
current_datetime_pattern = False
def check_patterns(patterns, preview_values):
for pattern in patterns:
match = True
for val in preview_values:
if not val:
continue
try:
dt.strptime(val, date_pattern+' %H:%M:%S')
datetime_ok = True
dt.strptime(val, pattern)
except ValueError:
datetime_ok = False
match = False
break
if date_ok or datetime_ok:
current_date_pattern = date_pattern
break
if match:
return pattern
return False
current_date_pattern = check_patterns(date_patterns, preview_values)
if current_date_pattern:
options['date_format'] = current_date_pattern
return ['date'] if date_ok else ['datetime']
return ['date']
current_datetime_pattern = check_patterns(datetime_patterns, preview_values)
if current_datetime_pattern:
options['datetime_format'] = current_datetime_pattern
return ['datetime']
return ['text', 'char', 'datetime', 'selection', 'many2one', 'one2many', 'many2many', 'html']
@@ -598,14 +607,19 @@ class Import(models.TransientModel):
# Parse date
index = import_fields.index(name)
dt = datetime.datetime
field_date_format = DEFAULT_SERVER_DATE_FORMAT if field['type'] == 'date' else DEFAULT_SERVER_DATETIME_FORMAT
if options.get('date_format', field_date_format) != field_date_format:
for line in data:
server_format = DEFAULT_SERVER_DATE_FORMAT if field['type'] == 'date' else DEFAULT_SERVER_DATETIME_FORMAT
if options.get('%s_format' % field['type'], server_format) != server_format:
user_format = ustr(options.get('%s_format' % field['type'])).encode('utf-8')
for num, line in enumerate(data):
if line[index]:
try:
line[index] = dt.strftime(dt.strptime(line[index], options['date_format']), field_date_format)
except ValueError:
raise ValueError(_("Column %s contains incorrect values (value: %s does not match date format" % (name, line[index])))
line[index] = dt.strftime(dt.strptime(ustr(line[index]).encode('utf-8'), user_format), server_format)
except ValueError, e:
raise ValueError(_("Column %s contains incorrect values. Error in line %d: %s") % (name, num + 1, ustr(e.message)))
except Exception, e:
raise ValueError(_("Error Parsing Date [%s:L%d]: %s") % (name, num + 1, ustr(e.message)))
elif field['type'] in ('float', 'monetary') and name in import_fields:
# Parse float, sometimes float values from file have currency symbol or () to denote a negative value
# We should be able to manage both case
@@ -119,11 +119,12 @@ var DataImport = Widget.extend(ControlPanelMixin, {
opts: [
{name: 'encoding', label: _lt("Encoding:"), value: 'utf-8'},
{name: 'separator', label: _lt("Separator:"), value: ','},
{name: 'quoting', label: _lt("Quoting:"), value: '"'}
{name: 'quoting', label: _lt("Text Delimiter:"), value: '"'}
],
parse_opts: [
{name: 'date_format', label: _lt("Date Format:"), value: ''},
{name: 'float_thousand_separator', label: _lt("Thousand Separator:"), value: ','},
{name: 'datetime_format', label: _lt("Datetime Format:"), value: ''},
{name: 'float_thousand_separator', label: _lt("Thousands Separator:"), value: ','},
{name: 'float_decimal_separator', label: _lt("Decimal Separator:"), value: '.'}
],
events: {
@@ -295,8 +296,8 @@ var DataImport = Widget.extend(ControlPanelMixin, {
self.$('input.oe_import_' + opt.name).val();
});
_(this.parse_opts).each(function (opt) {
if (opt.name === 'date_format') {
options[opt.name] = time.moment_to_strftime_format(self.$('input.oe_import_date_format').val());
if (opt.name === 'date_format' || opt.name === 'datetime_format') {
options[opt.name] = time.moment_to_strftime_format(self.$('input.oe_import_' + opt.name).val());
}
else {
options[opt.name] = self.$('input.oe_import_' + opt.name).val();
@@ -320,6 +321,7 @@ var DataImport = Widget.extend(ControlPanelMixin, {
.prop('disabled', true);
if (!this.$('input.oe_import_file').val()) { return this['settings_changed'](); }
this.$('.oe_import_date_format').val('');
this.$('.oe_import_datetime_format').val('');
this.$el.removeClass('oe_import_preview oe_import_error');
var import_toggle = false;
@@ -375,6 +377,7 @@ var DataImport = Widget.extend(ControlPanelMixin, {
}
this.$('.oe_import_date_format').val(time.strftime_to_moment_format(result.options.date_format));
this.$('.oe_import_datetime_format').val(time.strftime_to_moment_format(result.options.datetime_format));
this.$('.oe_import_float_thousand_separator').val(result.options.float_thousand_separator).change();
this.$('.oe_import_float_decimal_separator').val(result.options.float_decimal_separator).change();
if (result.debug === false){
@@ -379,6 +379,31 @@ class test_convert_import_data(TransactionCase):
['qux', '5', '6'],
])
def test_date_fields(self):
import_wizard = self.env['base_import.import'].create({
'res_model': 'res.partner',
'file': 'name,date,create_date\n'
'"foo","2013年07月18日","2016-10-12 06:06"\n',
'file_type': 'text/csv'
})
results = import_wizard.do(
['name', 'date', 'create_date'],
{
'date_format': '%Y年%m月%d日',
'datetime_format': '%Y-%m-%d %H:%M',
'quoting': '"',
'separator': ',',
'headers': True
}
)
# if results empty, no errors
self.assertItemsEqual(results, [])
def test_filtered(self):
""" If ``False`` is provided as field mapping for a column,
that column should be removed from importable data
+1 -1
View File
@@ -1372,7 +1372,7 @@ class Meeting(models.Model):
if not self._context.get('dont_notify'):
if len(meeting.alarm_ids) > 0 or values.get('alarm_ids'):
partners_to_notify = meeting.partner_ids.ids
event_attendees_changes = attendees_create and attendees_create[real_ids[0]]
event_attendees_changes = attendees_create and real_ids and attendees_create[real_ids[0]]
if event_attendees_changes:
partners_to_notify.append(event_attendees_changes['removed_partners'].ids)
self.env['calendar.alarm_manager'].notify_next_alarm(partners_to_notify)
+1 -1
View File
@@ -526,7 +526,7 @@
<field name="source_id" invisible="1"/>
<field name="stage_id"/>
<field name="planned_revenue" sum="Expected Revenues"/>
<field name="probability" />
<field name="probability" avg="Avg. of Probability"/>
<field name="team_id"/>
<field name="user_id"/>
<field name="referred" invisible="1"/>
+1 -1
View File
@@ -202,7 +202,7 @@
<label for="weight" string="Weight"/>
<div>
<field name="weight" class="oe_inline"/>
<field name="weight_uom_id" nolabel="1" class="oe_inline"/>
<field name="weight_uom_id" nolabel="1" class="oe_inline" style="margin-left:5px"/>
</div>
<field name="shipping_weight"/>
<field name="number_of_packages" attrs="{'readonly': [('state', '=', 'done')]}"/>
@@ -1,6 +1,8 @@
odoo.define('hr_attendance.greeting_message', function (require) {
"use strict";
var BarcodeHandlerMixin = require('barcodes.BarcodeHandlerMixin');
var core = require('web.core');
var Model = require('web.Model');
var Widget = require('web.Widget');
@@ -8,27 +10,39 @@ var Widget = require('web.Widget');
var _t = core._t;
var GreetingMessage = Widget.extend({
var GreetingMessage = Widget.extend(BarcodeHandlerMixin, {
template: 'HrAttendanceGreetingMessage',
events: {
"click .o_hr_attendance_button_dismiss": function () { this.do_action(this.next_action, {clear_breadcrumbs: true}); },
"click .o_hr_attendance_button_dismiss": function() { this.do_action(this.next_action, {clear_breadcrumbs: true}); },
},
init: function(parent, action) {
this._super.apply(this, arguments);
var self = this;
var default_action = { type: "ir.actions.client", name: 'Attendances', tag: 'hr_attendance_my_attendances', params: {} };
if(!action.attendance){
self.session.user_has_group('hr_attendance.group_hr_attendance_user').then(function(has_group){
if(has_group){
self.next_action = { type: "ir.actions.client", name: 'Attendances', tag: 'hr_attendance_kiosk_mode', target: 'fullscreen', params: {} };
this._super.apply(this, arguments);
BarcodeHandlerMixin.init.apply(this, arguments);
// if no correct action given (due to an erroneous back or refresh from the browser), we set the dismiss button to return
// to the (likely) appropriate menu, according to the user access rights
if(!action.attendance) {
this.stop_listening();
this.session.user_has_group('hr_attendance.group_hr_attendance_user').then(function(has_group) {
if(has_group) {
self.next_action = 'hr_attendance.hr_attendance_action_kiosk_mode';
} else {
self.next_action = 'hr_attendance.hr_attendance_action_my_attendances';
}
});
return;
}
this.next_action = action.next_action || default_action;
this.next_action = action.next_action || 'hr_attendance.hr_attendance_action_my_attendances';
// no listening to barcode scans if we aren't coming from the kiosk mode (and thus not going back to it with next_action)
if (this.next_action != 'hr_attendance.hr_attendance_action_kiosk_mode' && this.next_action.tag != 'hr_attendance_kiosk_mode') {
this.stop_listening();
}
this.attendance = action.attendance;
// check in/out times displayed in the greeting message template.
this.attendance.check_in_time = (new Date((new Date(this.attendance.check_in)).valueOf() - (new Date()).getTimezoneOffset()*60*1000)).toTimeString().slice(0,8);
this.attendance.check_out_time = this.attendance.check_out && (new Date((new Date(this.attendance.check_out)).valueOf() - (new Date()).getTimezoneOffset()*60*1000)).toTimeString().slice(0,8);
this.previous_attendance_change_date = action.previous_attendance_change_date;
@@ -36,48 +50,42 @@ var GreetingMessage = Widget.extend({
},
start: function() {
if (!this.attendance){
return;
}
if (this.attendance.check_out) {
this.farewell_message();
} else {
this.welcome_message();
if (this.attendance) {
this.attendance.check_out ? this.farewell_message() : this.welcome_message();
}
},
welcome_message: function() {
var self = this;
var now = new Date((new Date(this.attendance.check_in)).valueOf() - (new Date()).getTimezoneOffset()*60*1000);
self.return_to_main_menu = setTimeout( function() { self.do_action(self.next_action, {clear_breadcrumbs: true}); }, 5000);
this.return_to_main_menu = setTimeout( function() { self.do_action(self.next_action, {clear_breadcrumbs: true}); }, 5000);
if(now.getHours()<5){
self.$('.o_hr_attendance_message_message').append(_t("Good night"));
} else if(now.getHours() < 12){
if(now.getHours() < 8 && Math.random() < .3){
if(Math.random()<.75){
self.$('.o_hr_attendance_message_message').append(_t("The early bird catches the worm"));
if (now.getHours() < 5) {
this.$('.o_hr_attendance_message_message').append(_t("Good night"));
} else if (now.getHours() < 12) {
if (now.getHours() < 8 && Math.random() < 0.3) {
if (Math.random() < 0.75) {
this.$('.o_hr_attendance_message_message').append(_t("The early bird catches the worm"));
} else {
self.$('.o_hr_attendance_message_message').append(_t("First come, first served"));
this.$('.o_hr_attendance_message_message').append(_t("First come, first served"));
}
} else {
self.$('.o_hr_attendance_message_message').append(_t("Good morning"));
this.$('.o_hr_attendance_message_message').append(_t("Good morning"));
}
} else if(now.getHours()<17){
self.$('.o_hr_attendance_message_message').append(_t("Good afternoon"));
} else if(now.getHours()<23){
self.$('.o_hr_attendance_message_message').append(_t("Good evening"));
} else if (now.getHours() < 17){
this.$('.o_hr_attendance_message_message').append(_t("Good afternoon"));
} else if (now.getHours() < 23){
this.$('.o_hr_attendance_message_message').append(_t("Good evening"));
} else {
self.$('.o_hr_attendance_message_message').append(_t("Good night"));
this.$('.o_hr_attendance_message_message').append(_t("Good night"));
}
if(this.previous_attendance_change_date){
var last_check_out_date = new Date((new Date(this.previous_attendance_change_date)).valueOf() - (new Date()).getTimezoneOffset()*60*1000);
if(now.valueOf() - last_check_out_date.valueOf() > 1000*60*60*24*7){
self.$('.o_hr_attendance_random_message').html(_t("Glad to have you back, it's been a while!"));
this.$('.o_hr_attendance_random_message').html(_t("Glad to have you back, it's been a while!"));
} else {
if(Math.random() < .02){
self.$('.o_hr_attendance_random_message').html(_t("If a job is worth doing, it is worth doing well!"));
if(Math.random() < 0.02){
this.$('.o_hr_attendance_random_message').html(_t("If a job is worth doing, it is worth doing well!"));
}
}
}
@@ -86,38 +94,55 @@ var GreetingMessage = Widget.extend({
farewell_message: function() {
var self = this;
var now = new Date((new Date(this.attendance.check_out)).valueOf() - (new Date()).getTimezoneOffset()*60*1000);
self.return_to_main_menu = setTimeout( function() { self.do_action(self.next_action, {clear_breadcrumbs: true}); }, 5000);
this.return_to_main_menu = setTimeout( function() { self.do_action(self.next_action, {clear_breadcrumbs: true}); }, 5000);
if(this.previous_attendance_change_date){
var last_check_in_date = new Date((new Date(this.previous_attendance_change_date)).valueOf() - (new Date()).getTimezoneOffset()*60*1000);
if(now.valueOf() - last_check_in_date.valueOf() > 1000*60*60*12){
self.$('.o_hr_attendance_warning_message').append(_t("Warning! Last check in was over 12 hours ago.<br/>If this isn't right, please contact Human Resources."));
clearTimeout(self.return_to_main_menu);
this.$('.o_hr_attendance_warning_message').append(_t("Warning! Last check in was over 12 hours ago.<br/>If this isn't right, please contact Human Resources."));
clearTimeout(this.return_to_main_menu);
this.stop_listening();
} else if(now.valueOf() - last_check_in_date.valueOf() > 1000*60*60*8){
self.$('.o_hr_attendance_random_message').html(_t("Another good day's work! See you soon!"));
this.$('.o_hr_attendance_random_message').html(_t("Another good day's work! See you soon!"));
}
}
if(now.getHours()<12){
self.$('.o_hr_attendance_message_message').append(_t("Have a good day!"));
} else if(now.getHours()<14){
self.$('.o_hr_attendance_message_message').append(_t("Have a nice lunch!"));
if(Math.random() < 0.05){
self.$('.o_hr_attendance_random_message').html(_t("Eat breakfast as a king, lunch as a merchant and supper as a beggar"));
} else if(Math.random() < 0.06){
self.$('.o_hr_attendance_random_message').html(_t("An apple a day keeps the doctor away"));
if (now.getHours() < 12) {
this.$('.o_hr_attendance_message_message').append(_t("Have a good day!"));
} else if (now.getHours() < 14) {
this.$('.o_hr_attendance_message_message').append(_t("Have a nice lunch!"));
if (Math.random() < 0.05) {
this.$('.o_hr_attendance_random_message').html(_t("Eat breakfast as a king, lunch as a merchant and supper as a beggar"));
} else if (Math.random() < 0.06) {
this.$('.o_hr_attendance_random_message').html(_t("An apple a day keeps the doctor away"));
}
} else if(now.getHours()<17){
self.$('.o_hr_attendance_message_message').append(_t("Have a good afternoon"));
} else if (now.getHours() < 17) {
this.$('.o_hr_attendance_message_message').append(_t("Have a good afternoon"));
} else {
if(now.getHours()<18 && Math.random()<0.2){
self.$('.o_hr_attendance_message_message').append(_t("Early to bed and early to rise, makes a man healthy, wealthy and wise"));
if (now.getHours() < 18 && Math.random() < 0.2) {
this.$('.o_hr_attendance_message_message').append(_t("Early to bed and early to rise, makes a man healthy, wealthy and wise"));
} else {
self.$('.o_hr_attendance_message_message').append(_t("Have a good evening"));
this.$('.o_hr_attendance_message_message').append(_t("Have a good evening"));
}
}
},
on_barcode_scanned: function(barcode) {
var self = this;
if (this.return_to_main_menu) { // in case of multiple scans in the greeting message view, delete the timer, a new one will be created.
clearTimeout(this.return_to_main_menu);
}
var hr_employee = new Model('hr.employee');
hr_employee.call('attendance_scan', [barcode, ])
.then(function (result) {
if (result.action) {
self.do_action(result.action);
} else if (result.warning) {
self.do_warn(result.warning);
}
});
},
destroy: function () {
clearTimeout(this.return_to_main_menu);
this._super.apply(this, arguments);
@@ -55,7 +55,7 @@ var KioskConfirm = Widget.extend({
init: function (parent, action) {
this._super.apply(this, arguments);
this.next_action = { type: "ir.actions.client", name: "Attendances", tag: 'hr_attendance_kiosk_mode', target: "fullscreen", params: {} };
this.next_action = 'hr_attendance.hr_attendance_action_kiosk_mode';
this.employee_id = action.employee_id;
this.employee_name = action.employee_name;
this.employee_state = action.employee_state;
+24 -1
View File
@@ -136,6 +136,7 @@ class HrExpense(models.Model):
'product_id': line.get('product_id'),
'product_uom_id': line.get('uom_id'),
'analytic_account_id': line.get('analytic_account_id'),
'payment_id': line.get('payment_id'),
}
@api.multi
@@ -185,7 +186,10 @@ class HrExpense(models.Model):
'journal_id': journal.id,
'company_id': self.env.user.company_id.id,
'date': acc_date,
'ref': ref
'ref': ref,
# force the name to the default value, to avoid an eventual 'default_name' in the context
# to set it to '' which cause no number to be given to the account.move when posted.
'name': '/',
})
for expense in expense_list:
company_currency = expense.company_id.currency_id
@@ -194,11 +198,29 @@ class HrExpense(models.Model):
move_lines = expense._move_line_get()
#create one more move line, a counterline for the total on payable account
payment_id = False
total, total_currency, move_lines = expense._compute_expense_totals(company_currency, move_lines, acc_date)
if expense.payment_mode == 'company_account':
if not expense.sheet_id.bank_journal_id.default_credit_account_id:
raise UserError(_("No credit account found for the %s journal, please configure one.") % (expense.sheet_id.bank_journal_id.name))
emp_account = expense.sheet_id.bank_journal_id.default_credit_account_id.id
journal = expense.bank_journal_id
#create payment
payment_methods = (total < 0) and journal.outbound_payment_method_ids or journal.inbound_payment_method_ids
journal_currency = journal.currency_id or journal.company_id.currency_id
payment = self.env['account.payment'].create({
'payment_method_id': payment_methods and payment_methods[0].id or False,
'payment_type': total < 0 and 'outbound' or 'inbound',
'partner_id': expense.employee_id.address_home_id.commercial_partner_id.id,
'partner_type': 'supplier',
'journal_id': journal.id,
'payment_date': expense.date,
'state': 'reconciled',
'currency_id': diff_currency_p and expense.currency_id.id or journal_currency.id,
'amount': diff_currency_p and abs(total_currency) or abs(total),
'name': expense.name,
})
payment_id = payment.id
else:
if not expense.employee_id.address_home_id:
raise UserError(_("No Home Address found for the employee %s, please configure one.") % (expense.employee_id.name))
@@ -212,6 +234,7 @@ class HrExpense(models.Model):
'date_maturity': acc_date,
'amount_currency': diff_currency_p and total_currency or False,
'currency_id': diff_currency_p and expense.currency_id.id or False,
'payment_id': payment_id,
})
#convert eml into an osv-valid format
+1 -1
View File
@@ -213,5 +213,5 @@ class Employee(models.Model):
('date_from', '<=', today_end),
('date_to', '>=', today_start),
('type', '=', 'remove')
], ['employee_id'])
])
return [('id', 'in', holidays.mapped('employee_id').ids)]
+6 -1
View File
@@ -77,10 +77,15 @@ class EscposDriver(Thread):
printers = usb.core.find(find_all=True, idVendor=0x0519)
for printer in printers:
try:
description = usb.util.get_string(printer, 256, printer.iManufacturer) + " " + usb.util.get_string(printer, 256, printer.iProduct)
except Exception as e:
_logger.error("Can not get printer description: %s" % (e.message or repr(e)))
description = 'Unknown printer'
connected.append({
'vendor': printer.idVendor,
'product': printer.idProduct,
'name': usb.util.get_string(printer, 256, printer.iManufacturer) + " " + usb.util.get_string(printer, 256, printer.iProduct)
'name': description
})
return connected
@@ -462,7 +462,7 @@
<field name="account_id" ref="p11235"/>
<field name="refund_account_id" ref="p20011"/>
<field name="chart_template_id" ref="indian_chart_template_standard"/>
<field name="tag_ids" eval="[(6,0,[ref('st_tag_in_70'), ref('st_tag_in_21')])]"/>
<field name="tag_ids" eval="[(6, 0, [ref('st_tag_in_21'), ref('st_tag_in_66')])]"/>
</record>
<record id="input_tax_12" model="account.tax.template">
<field name="name">(standard)Input Sales Tax @ 12%</field>
@@ -474,7 +474,7 @@
<field name="account_id" ref="p11235"/>
<field name="refund_account_id" ref="p20011"/>
<field name="chart_template_id" ref="indian_chart_template_standard"/>
<field name="tag_ids" eval="[(6,0,[ref('st_tag_in_56')])]"/>
<field name="tag_ids" eval="[(6, 0, [ref('st_tag_in_18'), ref('st_tag_in_63')])]"/>
</record>
<record id="input_tax_4" model="account.tax.template">
<field name="name">(standard)Input Sales Tax @ 4%</field>
@@ -486,7 +486,7 @@
<field name="account_id" ref="p11235"/>
<field name="refund_account_id" ref="p20011"/>
<field name="chart_template_id" ref="indian_chart_template_standard"/>
<field name="tag_ids" eval="[(6,0,[ref('st_tag_in_15'), ref('st_tag_in_70')])]"/>
<field name="tag_ids" eval="[(6, 0, [ref('st_tag_in_15'), ref('st_tag_in_60')])]"/>
</record>
<record id="add_input_vat_1" model="account.tax.template">
<field name="name">(standard)Additional Input VAT @ 1%</field>
@@ -605,7 +605,7 @@
<field name="account_id" ref="p112341"/>
<field name="refund_account_id" ref="p20011"/>
<field name="chart_template_id" ref="indian_chart_template_standard"/>
<field name="tag_ids" eval="[(6,0,[ref('st_tag_in_25')])]"/>
<field name="tag_ids" eval="[(6, 0, [ref('st_tag_in_25'), ref('st_tag_in_63')])]"/>
</record>
<record id="sale_edu_cess_service_tax_2" model="account.tax.template">
<field name="name">(standard)Input Education Cess @ 2% On Service Tax</field>
+2 -16
View File
@@ -1109,37 +1109,23 @@ class MailThread(models.AbstractModel):
reply_match = False
if reply_match:
compat_mode = False
msg_references = tools.mail_header_msgid_re.findall(thread_references)
mail_messages = MailMessage.sudo().search([('message_id', 'in', msg_references)], limit=1)
# message is a reply to an existing thread (6.1 compatibility)
# do not match forwarded emails from another OpenERP system (thread_id collision!)
if not mail_messages and local_hostname == reply_hostname and reply_thread_id and reply_model in self.env:
mail_messages = MailMessage.search([
('message_id', '=', False),
('model', '=', reply_model),
('res_id', '=', reply_thread_id)])
compat_mode = True
if mail_messages:
model, thread_id = mail_messages.model, mail_messages.res_id
if not reply_private: # TDE note: not sure why private mode as no alias search, copying existing behavior
dest_aliases = Alias.search([('alias_name', 'in', rcpt_tos_localparts)], limit=1)
# TDE Note: compat mode = without context key, why ? because
route = self.message_route_verify(
message, message_dict,
(model, thread_id, custom_values, self._uid, dest_aliases),
update_author=True, assert_model=reply_private, create_fallback=True,
allow_private=reply_private, drop_alias=True)
if route:
# TDE Note: compat mode: parent is invalid for a compat-reply
# message_dict.pop('parent_id', None)
# TDE note: add compat mode for compat mode in debug
_logger.info(
'Routing mail from %s to %s with Message-Id %s%s: direct reply to msg: model: %s, thread_id: %s, custom_values: %s, uid: %s',
email_from, email_to, message_id, compat_mode and ' (compat mode)' or '', model, thread_id, custom_values, self._uid)
'Routing mail from %s to %s with Message-Id %s: direct reply to msg: model: %s, thread_id: %s, custom_values: %s, uid: %s',
email_from, email_to, message_id, model, thread_id, custom_values, self._uid)
return [route]
elif route is False:
return []
+8 -4
View File
@@ -580,25 +580,29 @@ class TestMailgateway(TestMail):
msg_id='<1198923581.41972151344608186802.JavaMail.diff1@agrolait.com>')
# when 6.1 messages are present, compat mode is available
# Odoo 10 update: compat mode has been removed and should not work anymore
self.fake_email.write({'message_id': False})
# Do: compat mode accepts partial-matching emails
frog_groups = self.format_and_process(
self.assertRaises(
ValueError,
self.format_and_process,
MAIL_TEMPLATE, email_from='other5@gmail.com',
msg_id='<1.2.JavaMail.new@agrolait.com>',
to='noone@example.com>', subject='spam',
extra='In-Reply-To: <12321321-openerp-%d-mail.channel@%s>' % (self.group_public.id, socket.gethostname()))
# 3''. 6.1 compat mode should not work if hostname does not match!
# Odoo 10 update: compat mode has been removed and should not work anymore and does not depend from hostname
self.assertRaises(ValueError,
self.format_and_process,
MAIL_TEMPLATE, email_from='other5@gmail.com',
msg_id='<1.3.JavaMail.new@agrolait.com>',
to='noone@example.com>', subject='spam',
extra='In-Reply-To: <12321321-openerp-%d-mail.channel@neighbor.com>' % frog_groups.id)
extra='In-Reply-To: <12321321-openerp-%d-mail.channel@neighbor.com>' % self.group_public.id)
# Test created messages
self.assertEqual(len(self.group_public.message_ids), 2, 'message_process: group should contain 6 messages')
self.assertEqual(len(self.group_public.message_ids[0].child_ids), 0, 'message_process: msg1 should not have children')
self.assertEqual(len(self.group_public.message_ids), 1)
self.assertEqual(len(self.group_public.message_ids[0].child_ids), 0)
@mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models')
def test_message_process_duplicate(self):
+4
View File
@@ -410,6 +410,10 @@ var PosDB = core.Class.extend({
}
}
// Only necessary when we store a new, validated order. Orders
// that where already stored should already have been removed.
this.remove_unpaid_order(order);
orders.push({id: order_id, data: order});
this.save('orders',orders);
return order_id;
@@ -1895,7 +1895,7 @@ var PaymentScreenWidget = ScreenWidget.extend({
}
// if the change is too large, it's probably an input error, make the user confirm.
if (!force_validation && (order.get_total_with_tax() * 1000 < order.get_total_paid())) {
if (!force_validation && order.get_total_with_tax() > 0 && (order.get_total_with_tax() * 1000 < order.get_total_paid())) {
this.gui.show_popup('confirm',{
title: _t('Please Confirm Large Amount'),
body: _t('Are you sure that the customer wants to pay') +
+4 -3
View File
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import base64
from ast import literal_eval
from odoo import models, fields, api
@@ -29,7 +29,7 @@ class pos_cache(models.Model):
prod_ctx = prod_ctx.sudo(self.compute_user_id.id)
res = prod_ctx.read(self.get_product_fields())
datas = {
'cache': cPickle.dumps(res),
'cache': base64.encodestring(cPickle.dumps(res)),
}
self.write(datas)
@@ -49,7 +49,8 @@ class pos_cache(models.Model):
self.product_fields = str(fields)
self.refresh_cache()
return cPickle.loads(self.cache)
cache = base64.decodestring(self.cache)
return cPickle.loads(cache)
class pos_config(models.Model):
+30 -25
View File
@@ -30,6 +30,35 @@ class AccountInvoice(models.Model):
]}
return result
def _prepare_invoice_line_from_po_line(self, line):
if line.product_id.purchase_method == 'purchase':
qty = line.product_qty - line.qty_invoiced
else:
qty = line.qty_received - line.qty_invoiced
if float_compare(qty, 0.0, precision_rounding=line.product_uom.rounding) <= 0:
qty = 0.0
taxes = line.taxes_id
invoice_line_tax_ids = self.purchase_id.fiscal_position_id.map_tax(taxes)
invoice_line = self.env['account.invoice.line']
data = {
'purchase_line_id': line.id,
'name': self.purchase_id.name+': '+line.name,
'origin': self.purchase_id.origin,
'uom_id': line.product_uom.id,
'product_id': line.product_id.id,
'account_id': invoice_line.with_context({'journal_id': self.journal_id.id, 'type': 'in_invoice'})._default_account(),
'price_unit': line.order_id.currency_id.compute(line.price_unit, self.currency_id, round=False),
'quantity': qty,
'discount': 0.0,
'account_analytic_id': line.account_analytic_id.id,
'analytic_tag_ids': line.analytic_tag_ids.ids,
'invoice_line_tax_ids': invoice_line_tax_ids.ids
}
account = invoice_line.get_invoice_line_account('in_invoice', line.product_id, self.purchase_id.fiscal_position_id, self.env.user.company_id)
if account:
data['account_id'] = account.id
return data
# Load all unsold PO lines
@api.onchange('purchase_id')
def purchase_order_change(self):
@@ -43,31 +72,7 @@ class AccountInvoice(models.Model):
# Load a PO line only once
if line in self.invoice_line_ids.mapped('purchase_line_id'):
continue
if line.product_id.purchase_method == 'purchase':
qty = line.product_qty - line.qty_invoiced
else:
qty = line.qty_received - line.qty_invoiced
if float_compare(qty, 0.0, precision_rounding=line.product_uom.rounding) <= 0:
qty = 0.0
taxes = line.taxes_id
invoice_line_tax_ids = self.purchase_id.fiscal_position_id.map_tax(taxes)
data = {
'purchase_line_id': line.id,
'name': self.purchase_id.name+': '+line.name,
'origin': self.purchase_id.origin,
'uom_id': line.product_uom.id,
'product_id': line.product_id.id,
'account_id': self.env['account.invoice.line'].with_context({'journal_id': self.journal_id.id, 'type': 'in_invoice'})._default_account(),
'price_unit': line.order_id.currency_id.compute(line.price_unit, self.currency_id, round=False),
'quantity': qty,
'discount': 0.0,
'account_analytic_id': line.account_analytic_id.id,
'analytic_tag_ids': line.analytic_tag_ids.ids,
'invoice_line_tax_ids': invoice_line_tax_ids.ids
}
account = new_lines.get_invoice_line_account('in_invoice', line.product_id, self.purchase_id.fiscal_position_id, self.env.user.company_id)
if account:
data['account_id'] = account.id
data = self._prepare_invoice_line_from_po_line(line)
new_line = new_lines.new(data)
new_line._set_additional_fields(self)
new_lines += new_line
@@ -194,16 +194,16 @@ class PurchaseOrder(models.Model):
taxes_ids = line.product_id.supplier_taxes_id.filtered(lambda tax: tax.company_id == requisition.company_id).ids
# Compute quantity and price_unit
if requisition.type_id.quantity_copy != 'copy':
product_qty = 0
price_unit = line.price_unit
elif line.product_uom_id != line.product_id.uom_po_id:
if line.product_uom_id != line.product_id.uom_po_id:
product_qty = line.product_uom_id._compute_quantity(line.product_qty, line.product_id.uom_po_id)
price_unit = line.product_uom_id._compute_price(line.price_unit, line.product_id.uom_po_id)
else:
product_qty = line.product_qty
price_unit = line.price_unit
if requisition.type_id.quantity_copy != 'copy':
product_qty = 0
# Compute price_unit in appropriate currency
if requisition.company_id.currency_id != currency:
price_unit = requisition.company_id.currency_id.compute(price_unit, currency)
@@ -258,6 +258,24 @@ class PurchaseOrder(models.Model):
return result
class PurchaseOrderLine(models.Model):
_inherit = "purchase.order.line"
@api.onchange('product_qty', 'product_uom')
def _onchange_quantity(self):
res = super(PurchaseOrderLine, self)._onchange_quantity()
if self.order_id.requisition_id:
for line in self.order_id.requisition_id.line_ids:
if line.product_id == self.product_id:
if line.product_uom_id != self.product_uom:
self.price_unit = self.env['product.uom']._compute_price(
line.product_uom_id.id, line.price_unit, to_uom_id=self.product_uom.id)
else:
self.price_unit = line.price_unit
break
return res
class ProductTemplate(models.Model):
_inherit = 'product.template'
+4 -1
View File
@@ -10,10 +10,13 @@ class ProjectTaskType(models.Model):
_inherit = 'project.task.type'
def _default_domain_rating_template_id(self):
return [('model', '=', 'project.task')]
rating_template_id = fields.Many2one(
'mail.template',
string='Rating Email Template',
domain=[('model', '=', 'project.task')],
domain=lambda self: self._default_domain_rating_template_id(),
help="Select an email template. An email will be sent to the customer when the task reach this step.")
auto_validation_kanban_state = fields.Boolean('Automatic kanban status', default=False,
help="Automatically modify the kanban state when the customer reply to the feedback for this stage.\n"
@@ -28,6 +28,16 @@ class ProjectIssue(models.Model):
return super(ProjectIssue, self).rating_apply(rate, token=token, feedback=feedback, subtype="rating_project_issue.mt_issue_rating")
class Stage(models.Model):
_inherit = ['project.task.type']
def _default_domain_rating_template_id(self):
domain = super(Stage, self)._default_domain_rating_template_id()
return ['|'] + domain + [('model', '=', 'project.issue')]
class Project(models.Model):
_inherit = "project.project"
@@ -83,6 +83,10 @@ class SaleAdvancePaymentInv(models.TransientModel):
else:
amount = self.amount
name = _('Down Payment')
if order.fiscal_position_id and self.product_id.taxes_id:
tax_ids = order.fiscal_position_id.map_tax(self.product_id.taxes_id).ids
else:
tax_ids = self.product_id.taxes_id.ids
invoice = inv_obj.create({
'name': order.client_order_ref or order.name,
@@ -102,7 +106,7 @@ class SaleAdvancePaymentInv(models.TransientModel):
'uom_id': self.product_id.uom_id.id,
'product_id': self.product_id.id,
'sale_line_ids': [(6, 0, [so_line.id])],
'invoice_line_tax_ids': [(6, 0, [x.id for x in self.product_id.taxes_id])],
'invoice_line_tax_ids': [(6, 0, tax_ids)],
'account_analytic_id': order.project_id.id or False,
})],
'currency_id': order.pricelist_id.currency_id.id,
@@ -142,6 +146,10 @@ class SaleAdvancePaymentInv(models.TransientModel):
raise UserError(_('The product used to invoice a down payment should have an invoice policy set to "Ordered quantities". Please update your deposit product to be able to create a deposit invoice.'))
if self.product_id.type != 'service':
raise UserError(_("The product used to invoice a down payment should be of type 'Service'. Please use another product or update this product."))
if order.fiscal_position_id and self.product_id.taxes_id:
tax_ids = order.fiscal_position_id.map_tax(self.product_id.taxes_id).ids
else:
tax_ids = self.product_id.taxes_id.ids
so_line = sale_line_obj.create({
'name': _('Advance: %s') % (time.strftime('%m %Y'),),
'price_unit': amount,
@@ -150,7 +158,7 @@ class SaleAdvancePaymentInv(models.TransientModel):
'discount': 0.0,
'product_uom': self.product_id.uom_id.id,
'product_id': self.product_id.id,
'tax_id': [(6, 0, self.product_id.taxes_id.ids)],
'tax_id': [(6, 0, tax_ids)],
'is_downpayment': True,
})
self._create_invoice(order, so_line, amount)
+21 -7
View File
@@ -393,16 +393,16 @@ class ProductTemplate(models.Model):
('none', 'No Tracking')], string="Tracking", default='none', required=True)
description_picking = fields.Text('Description on Picking', translate=True)
qty_available = fields.Float(
'Quantity On Hand', compute='_compute_quantities', search='_search_quantities',
'Quantity On Hand', compute='_compute_quantities', search='_search_qty_available',
digits=dp.get_precision('Product Unit of Measure'))
virtual_available = fields.Float(
'Forecasted Quantity', compute='_compute_quantities', search='_search_quantities',
'Forecasted Quantity', compute='_compute_quantities', search='_search_virtual_available',
digits=dp.get_precision('Product Unit of Measure'))
incoming_qty = fields.Float(
'Incoming', compute='_compute_quantities', search='_search_quantities',
'Incoming', compute='_compute_quantities', search='_search_incoming_qty',
digits=dp.get_precision('Product Unit of Measure'))
outgoing_qty = fields.Float(
'Outgoing', compute='_compute_quantities', search='_search_quantities',
'Outgoing', compute='_compute_quantities', search='_search_outgoing_qty',
digits=dp.get_precision('Product Unit of Measure'))
location_id = fields.Many2one('stock.location', 'Location')
warehouse_id = fields.Many2one('stock.warehouse', 'Warehouse')
@@ -452,9 +452,23 @@ class ProductTemplate(models.Model):
}
return prod_available
def _search_quantities(self, operator, value):
# TDE FIXME: does this work anyway ?
domain = [] # TDE ADDED
def _search_qty_available(self, operator, value):
domain = [('qty_available', operator, value)]
product_variant_ids = self.env['product.product'].search(domain)
return [('product_variant_ids', 'in', product_variant_ids.ids)]
def _search_virtual_available(self, operator, value):
domain = [('virtual_available', operator, value)]
product_variant_ids = self.env['product.product'].search(domain)
return [('product_variant_ids', 'in', product_variant_ids.ids)]
def _search_incoming_qty(self, operator, value):
domain = [('incoming_qty', operator, value)]
product_variant_ids = self.env['product.product'].search(domain)
return [('product_variant_ids', 'in', product_variant_ids.ids)]
def _search_outgoing_qty(self, operator, value):
domain = [('outgoing_qty', operator, value)]
product_variant_ids = self.env['product.product'].search(domain)
return [('product_variant_ids', 'in', product_variant_ids.ids)]
+4 -6
View File
@@ -91,14 +91,12 @@ class Inventory(models.Model):
('category', _('One product category')),
('product', _('One product only')),
('partial', _('Select products manually'))]
stock_settings = self.env['stock.config.settings'].search([], limit=1, order='id DESC')
if not stock_settings:
return res_filter
if stock_settings.group_stock_tracking_owner:
if self.user_has_groups('stock.group_tracking_owner'):
res_filter += [('owner', _('One owner only')), ('product_owner', _('One product for a specific owner'))]
if stock_settings.group_stock_production_lot:
if self.user_has_groups('stock.group_production_lot'):
res_filter.append(('lot', _('One Lot/Serial Number')))
if stock_settings.group_stock_tracking_lot:
if self.user_has_groups('stock.group_tracking_lot'):
res_filter.append(('pack', _('A Pack')))
return res_filter
+2 -5
View File
@@ -194,7 +194,6 @@ class StockMove(models.Model):
@api.multi
def _compute_string_qty_information(self):
StockConfig = self.env['stock.config.settings']
precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
void_moves = self.filtered(lambda move: move.state in ('draft', 'done', 'cancel') or move.location_id.usage != 'internal')
other_moves = self - void_moves
@@ -205,9 +204,7 @@ class StockMove(models.Model):
total_available = move.product_id.uom_id._compute_quantity(total_available, move.product_uom, round=False)
total_available = float_round(total_available, precision_digits=precision)
info = str(total_available)
# look in the settings if we need to display the UoM name or not
config = StockConfig.search([], limit=1, order='id DESC')
if config and config.group_uom:
if self.user_has_groups('product.group_uom'):
info += ' ' + move.product_uom.name
if move.reserved_availability:
if move.reserved_availability != total_available:
@@ -380,7 +377,7 @@ class StockMove(models.Model):
# if no specialized push rule has been found yet, we try to find a general one (without route)
rules = Push.search(domain + [('route_id', '=', False)], order='sequence')
# Make sure it is not returning the return
if rules and (not move.origin_returned_move_id or move.origin_returned_move_id.location_id.id != rules.location_dest_id.id):
if rules and (not move.origin_returned_move_id or move.origin_returned_move_id.location_dest_id.id != rules.location_dest_id.id):
rules._apply(move)
return True
@@ -77,7 +77,7 @@ class AccountInvoiceLine(models.Model):
def _get_price(self, company_currency, price_unit):
if self.invoice_id.currency_id.id != company_currency:
price = company_currency.with_context(date=self.invoice_id.date_invoice).compute(price_unit * self.quantity, self.invoice_id.currency_id.id)
price = company_currency.with_context(date=self.invoice_id.date_invoice).compute(price_unit * self.quantity, self.invoice_id.currency_id)
else:
price = price_unit * self.quantity
return round(price, self.invoice_id.currency_id.decimal_places)
+2 -1
View File
@@ -3,6 +3,7 @@
from odoo import api, fields, models, tools, _
from odoo.exceptions import UserError
from odoo.tools import float_is_zero
class ProductTemplate(models.Model):
@@ -105,7 +106,7 @@ class ProductProduct(models.Model):
for location in locations:
for product in self.with_context(location=location.id, compute_child=False):
diff = product.standard_price - new_price
if diff:
if float_is_zero(diff, precision_rounding=product.currency_id.rounding):
raise UserError(_("No difference between standard price and new price!"))
if not product_accounts[product.id].get('stock_valuation', False):
raise UserError(_('You don\'t have any stock valuation account defined on your product category. You must define one before processing this operation.'))
+5
View File
@@ -115,6 +115,11 @@ class StockQuant(models.Model):
else:
self.with_context(force_company=company_from.id)._create_account_move_line(move, acc_valuation, acc_dest, journal_id)
if move.company_id.anglo_saxon_accounting and move.location_id.usage == 'supplier' and move.location_dest_id.usage == 'customer':
# Creates an account entry from stock_input to stock_output on a dropship move. https://github.com/odoo/odoo/issues/12687
journal_id, acc_src, acc_dest, acc_valuation = move._get_accounting_data_for_valuation()
self.with_context(force_company=move.company_id.id)._create_account_move_line(move, acc_src, acc_dest, journal_id)
def _create_account_move_line(self, move, credit_account_id, debit_account_id, journal_id):
# group quants by cost
quant_cost_qty = defaultdict(lambda: 0.0)
+3
View File
@@ -459,6 +459,9 @@ var ViewManager = Widget.extend(ControlPanelMixin, {
}
},
do_load_state: function(state, warm) {
if (state.view_type && state.view_type !== this.active_view.type) {
this.switch_mode(state.view_type, true);
}
this.active_view.controller.do_load_state(state, warm);
},
});
@@ -430,6 +430,7 @@ var FormWidget = Widget.extend(InvisibilityChangerMixin, {
this.process_modifiers();
this._super();
this.$el.addClass(this.node.attrs["class"] || "");
this.$el.attr('style', this.node.attrs.style);
},
destroy: function() {
$.fn.tooltip('destroy');
@@ -1275,6 +1275,9 @@ var FieldOne2Many = FieldX2Many.extend({
return self.mutex.def;
});
},
is_false: function() {
return false;
},
});
var Many2ManyListView = X2ManyListView.extend({
+1 -1
View File
@@ -1733,7 +1733,7 @@ var Column = Class.extend({
}
var aggregation_func = (this.sum && 'sum') || (this.avg && 'avg') ||
(this.max && 'max') || (this.min && 'min') || this.group_operator;
(this.max && 'max') || (this.min && 'min');
if (!aggregation_func) {
return {};
+30 -18
View File
@@ -113,25 +113,37 @@ return AbstractWebClient.extend({
}
},
on_hashchange: function(event) {
var self = this;
var stringstate = event.getState(false);
if (!_.isEqual(this._current_state, stringstate)) {
var state = event.getState(true);
if(!state.action && state.menu_id) {
self.menu.is_bound.done(function() {
self.menu.menu_click(state.menu_id);
});
} else {
state._push_me = false; // no need to push state back...
this.action_manager.do_load_state(state, !!this._current_state).then(function () {
var action = self.action_manager.get_inner_action();
if (action) {
self.menu.open_action(action.action_descr.id);
}
});
}
if (this._ignore_hashchange) {
this._ignore_hashchange = false;
return;
}
this._current_state = stringstate;
var self = this;
this.clear_uncommitted_changes().then(function () {
var stringstate = event.getState(false);
if (!_.isEqual(self._current_state, stringstate)) {
var state = event.getState(true);
if(!state.action && state.menu_id) {
self.menu.is_bound.done(function() {
self.menu.menu_click(state.menu_id);
});
} else {
state._push_me = false; // no need to push state back...
self.action_manager.do_load_state(state, !!self._current_state).then(function () {
var action = self.action_manager.get_inner_action();
if (action) {
self.menu.open_action(action.action_descr.id);
}
});
}
}
self._current_state = stringstate;
}, function () {
if (event) {
self._ignore_hashchange = true;
window.location = event.originalEvent.oldURL;
}
});
},
on_menu_action: function(options) {
var self = this;
@@ -460,6 +460,15 @@
}
}
}
// Notebooks
.o_notebook {
.nav-tabs {
> .tab-pane {
min-height: 100px;
}
}
}
}
// Overriden style when form view in modal
+1 -1
View File
@@ -226,7 +226,7 @@ class Web_Editor(http.Controller):
@http.route('/web_editor/customize_template_get', type='json', auth='user', website=True)
def customize_template_get(self, key, full=False, bundles=False):
""" Get inherit view's informations of the template ``key``.
returns templates (which can be active or not)
returns templates info (which can be active or not)
``bundles=True`` returns also the asset bundles
"""
return request.env["ir.ui.view"].customize_template_get(key, full=full, bundles=bundles)
+14 -35
View File
@@ -175,42 +175,21 @@ class IrUiView(models.Model):
return views_to_return
@api.model
def customize_template_get(self, key, full=False, bundles=False):
""" Get inherit view's informations of the template ``key``.
returns templates (which can be active or not)
def _customize_template_get_views(self, key, full=False, bundles=False):
""" Get inherit views of the template ``key``.
returns views (which can be active or not)
``bundles=True`` returns also the asset bundles
"""
user = self.env.user
user_groups = set(user.groups_id)
user_groups = set(self.env.user.groups_id)
views = self.with_context(active_test=False)._views_get(key, bundles=bundles)
views = views.filtered(lambda v: user_groups.issuperset(v.groups_id))
return views
view_theme_id = self.env['ir.model.data'].xmlid_to_res_id('website.theme')
done = set()
result = []
for view in views:
if full or (view.customize_show and view.inherit_id.id != view_theme_id):
if not user_groups.issuperset(view.groups_id):
continue
if view.inherit_id not in done:
result.append({
'name': view.inherit_id.name,
'id': view.id,
'key': view.key,
'xml_id': view.xml_id,
'arch': view.arch,
'inherit_id': view.inherit_id.id,
'header': True,
'active': False
})
done.add(view.inherit_id)
result.append({
'name': view.name,
'id': view.id,
'key': view.key,
'xml_id': view.xml_id,
'arch': view.arch,
'inherit_id': view.inherit_id.id,
'header': False,
'active': view.active,
})
return result
@api.model
def customize_template_get(self, key, full=False, bundles=False):
""" Get inherit view's informations of the template ``key``.
returns templates info (which can be active or not)
``bundles=True`` returns also the asset bundles
"""
views = self._customize_template_get_views(key, full=full, bundles=bundles)
return views.read(['name', 'id', 'key', 'xml_id', 'arch', 'active', 'inherit_id'])
+1 -1
View File
@@ -149,7 +149,7 @@ var ViewEditor = Widget.extend({
view.children = [];
});
_.each(index, function (view) {
var parentId = view.inherit_id;
var parentId = view.inherit_id[0];
if (parentId && index[parentId]) {
index[parentId].children.push(view);
} else {
+1 -1
View File
@@ -1145,7 +1145,7 @@ var LinkDialog = Dialog.extend({
$preview.attr("target", new_window ? '_blank' : "")
.attr("href", url && url.length ? url : "#")
.html((label && label.length ? label : url))
.attr("class", classes.replace(/pull-\w+/, ''));
.attr("class", classes.replace(/pull-\w+/, '') + " o_btn_preview");
});
}
});
@@ -189,7 +189,7 @@ body .modal {
ul.pager {
margin: 0;
> li > a {
border-radius: 3px;
border-radius: 0px;
margin-left: 2px;
padding: 6px 14px;
}
@@ -206,20 +206,34 @@ body .modal {
}
}
}
.dropdown-toggle {
line-height: 0;
height: 34px;
.well {
border-top-width: 0;
label {
margin: auto 10px;
}
.btn-group {
margin-right: 10px;
}
}
.o_existing_attachment_cell {
@odoo-border-image: 1px;
.o_existing_attachment_remove {
.o-position-absolute(@odoo-border-image, 15px + @odoo-border-image);
padding: 3px 3px 12px 12px;
background-color: white;
border-bottom: 1px solid black;
border-left: 1px solid black;
border-radius: 0 0 0 100%;
opacity: 0.8;
padding: 3px 5px 5px 5px;
background-color: fade(white, 80%);
text-shadow: 0 0 1px white;
border-radius: 0 0 0 2px;
opacity: 0;
cursor: pointer;
&:hover {
background-color: white;
color: @o-we-color-danger;
}
}
.o_image {
border: @odoo-border-image solid transparent;
@@ -235,15 +249,29 @@ body .modal {
border-color: @odoo-brand-primary;
box-shadow: 0px 0px 2px 2px @odoo-brand-secondary;
}
&:hover {
.o_existing_attachment_remove {
opacity: 1;
}
}
}
.font-icons-icons > span {
font-size: 30px;
margin: 5px;
padding: 3px;
cursor: pointer;
.font-icons-icons {
padding: 20px 0;
&.o_selected {
box-shadow: 0px 0px 2px 2px @odoo-brand-secondary;
> span {
text-align: center;
font-size: 22px;
margin: 5px;
width: 50px;
height: 50px;
padding: 15px;
cursor: pointer;
&.o_selected {
cursor: auto;
background-color: @o-we-color-paper;
box-shadow: 0px 0px 0px 1px @o-we-color-success;
}
}
}
}
@@ -9,6 +9,9 @@
@o-we-color-text-normal: #999999;
@o-we-color-text-light: #d4d5d7;
@o-we-color-danger: #e6586c;
@o-we-color-warning: #f0ad4e;
@o-we-color-success: #40ad67;
@o-we-color-info: #6999a8;
@o-we-sidebar-width: 210px;
@@ -76,3 +79,119 @@
from { background: @odoo-brand-optional; }
to { width: 0; background: transparent; }
}
// ============ Preservetion mixins ===============
// Used to preserve web_editor and website UI from themes customizations
.o-w-preserve-base() {
font-family: @o-we-font-family;
line-height: 1.42857143;
color: @o-we-color-divider;
.text-muted {
color: @o-we-color-text-normal;
}
}
.o-w-preserve-headings() {
h1, h2, h3, h4, h5, h6, .h1, .h2, .h3, .h4, .h5, .h6 {
font-family: @o-we-font-family;
line-height: 1.42857143;
color: @o-we-color-dark;
font-weight: bold;
}
}
.o-w-preserve-links() {
a:not(.o_btn_preview) {
color: @odoo-brand-optional;
&:focus, &:active, &:focus:active {
outline: none!important;
}
}
}
.o-w-preserve-forms() {
.form-control {
height: 34px;
padding: 6px 12px;
font-size: 14px;
line-height: 1.42857143;
border: 1px solid @o-we-color-text-light;
color: #555;
background-color: #fff;
border-radius: 0;
}
}
.o-w-preserve-modals() {
background: 0;
.modal-content {
border-radius: 0;
background-color: @o-we-color-paper;
.modal-body {
background-color: white;
}
.modal-footer {
text-align: left;
}
}
}
.o-w-preserve-btn() {
.btn:not(.o_btn_preview) {
border-width: 1px;
border-radius: 0;
font-weight: normal;
.button-size(6px; 12px; 14px; 1.42857143; 0);
&.btn-primary {
.button-variant(white, @odoo-brand-optional, darken(@odoo-brand-optional,5%));
text-transform: uppercase;
}
&.btn-default {
.button-variant(@odoo-brand-optional, white, @o-we-color-text-light);
text-transform: uppercase;
}
&.btn-link {
.button-variant(@odoo-brand-optional, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0));
}
&.btn-success {
.button-variant(white; @o-we-color-success; darken(@o-we-color-success, 5%));
}
&.btn-info {
.button-variant(white; @o-we-color-info; darken(@o-we-color-info, 5%));
}
&.btn-warning {
.button-variant(white; @o-we-color-warning; darken(@o-we-color-warning, 5%));
}
&.btn-danger {
.button-variant(white; @o-we-color-danger; darken(@o-we-color-danger, 5%));
}
&.btn-lg {
.button-size(10px; 16px; 18px; 1.3333333; 0);
}
&.btn-sm {
.button-size(5px; 10px; 12px; 1.5; 0);
}
&.btn-xs {
.button-size(1px; 5px; 12px; 1.5; 0);
}
}
}
.o-w-preserve-wells() {
.well {
padding: 19px;
margin-bottom: 20px;
background-color: @o-we-color-paper;
border: 1px solid darken(@o-we-color-paper,5%);
border-radius: 0;
.box-shadow(none);
}
}
+11 -11
View File
@@ -251,31 +251,31 @@
<span class="caret"/></a>
<ul aria-labelledby="dLabel" class="dropdown-menu" role="menu">
<li>
<label class="btn btn-link">Link
<label class="btn btn-link o_btn_preview">Link
<input checked="checked" class="link-style" name="link-style-type" type="radio" value=""/></label>
</li>
<li>
<label class="btn btn-default">Basic
<label class="btn btn-default o_btn_preview">Basic
<input class="link-style" name="link-style-type" type="radio" value="btn-default"/></label>
</li>
<li>
<label class="btn btn-success">Success
<label class="btn btn-success o_btn_preview">Success
<input class="link-style" name="link-style-type" type="radio" value="btn-success"/></label>
</li>
<li>
<label class="btn btn-primary">Primary
<label class="btn btn-primary o_btn_preview">Primary
<input class="link-style" name="link-style-type" type="radio" value="btn-primary"/></label>
</li>
<li>
<label class="btn btn-info">Info
<label class="btn btn-info o_btn_preview">Info
<input class="link-style" name="link-style-type" type="radio" value="btn-info"/></label>
</li>
<li>
<label class="btn btn-warning">Warning
<label class="btn btn-warning o_btn_preview">Warning
<input class="link-style" name="link-style-type" type="radio" value="btn-warning"/></label>
</li>
<li>
<label class="btn btn-danger">Danger
<label class="btn btn-danger o_btn_preview">Danger
<input class="link-style" name="link-style-type" type="radio" value="btn-danger"/></label>
</li>
</ul>
@@ -287,19 +287,19 @@
<span class="caret"/></a>
<ul aria-labelledby="dLabel" class="dropdown-menu" role="menu">
<li>
<label class="btn btn-default btn-xs">Extra Small
<label class="btn btn-default btn-xs o_btn_preview">Extra Small
<input class="link-style" name="link-style-size" type="radio" value="btn-xs"/></label>
</li>
<li>
<label class="btn btn-default btn-sm">Small
<label class="btn btn-default btn-sm o_btn_preview">Small
<input class="link-style" name="link-style-size" type="radio" value="btn-sm"/></label>
</li>
<li>
<label class="btn btn-default">Default
<label class="btn btn-default o_btn_preview">Default
<input checked="checked" class="link-style" name="link-style-size" type="radio" value=""/></label>
</li>
<li>
<label class="btn btn-default btn-lg">Large
<label class="btn btn-default btn-lg o_btn_preview">Large
<input class="link-style" name="link-style-size" type="radio" value="btn-lg"/></label>
</li>
</ul>
@@ -62,7 +62,7 @@ var KanbanView = View.extend({
this.qweb = new QWeb(session.debug, {_s: session.origin});
this.limit = this.options.limit || 40;
this.limit = this.options.limit || parseInt(this.fields_view.arch.attrs.limit, 10) || 40;
this.fields = {};
this.fields_keys = _.keys(this.fields_view.fields);
this.grouped = undefined;
+5 -4
View File
@@ -262,10 +262,10 @@ class Website(Home):
@http.route('/website/customize_template_get', type='json', auth='user', website=True)
def customize_template_get(self, key, full=False, bundles=False):
""" Get inherit view's informations of the template ``key``. By default, only
:returns ``customize_show`` templates (which can be active or not), if
``full=True`` returns inherit view's informations of the template ``key``.
``bundles=True`` returns also the asset bundles
""" Get inherit view's informations of the template ``key``.
returns templates info (which can be active or not)
``full=False`` returns only the customize_show template
``bundles=True`` returns also the asset bundles
"""
return request.env["ir.ui.view"].customize_template_get(key, full=full, bundles=bundles)
@@ -370,6 +370,7 @@ class Website(Home):
], type='http', auth="public", website=True)
def actions_server(self, path_or_xml_id_or_id, **post):
ServerActions = request.env['ir.actions.server']
action = action_id = None
# find the action_id: either an xml_id, the path, or an ID
if isinstance(path_or_xml_id_or_id, basestring) and '.' in path_or_xml_id_or_id:
+9 -21
View File
@@ -136,25 +136,13 @@ class View(models.Model):
return qcontext
@api.model
def customize_template_get(self, key, full=False, bundles=False):
""" Get inherit view's informations of the template ``key``. By default, only
:returns ``customize_show`` templates (which can be active or not), if
``full=True`` returns inherit view's informations of the template ``key``.
``bundles=True`` returns also the asset bundles
def _customize_template_get_views(self, key, full=False, bundles=False):
""" Get inherit view's informations of the template ``key``.
returns views (which can be active or not)
``full=False`` returns only the customize_show template
``bundles=True`` returns also the asset bundles
"""
result = super(View, self).customize_template_get(key, full=full, bundles=bundles)
imd = self.env['ir.model.data']
view_theme_id = imd.xmlid_to_res_id('website.theme')
result_filtered = []
for x in result:
if not full:
view = self.browse(x['id'])
if not view.customize_show:
continue
if view_theme_id and x['inherit_id'] == view_theme_id:
continue
result_filtered.append(x)
return result_filtered
views = super(View, self)._customize_template_get_views(key, full=full, bundles=bundles)
if full:
return views
return views.filtered(lambda v: v.customize_show)
@@ -74,16 +74,17 @@ website.TopBarCustomize = Widget.extend({
key: this.view_name,
full: false
}).then(function (result) {
var current_group = "";
_.each(result, function (item) {
if (item.header) {
$menu.append('<li class="dropdown-header">' + item.name + '</li>');
} else {
var $li = $('<li/>', {role: 'presentation'})
.append($('<a/>', {href: '#', 'data-view-id': item.id, role: 'menuitem'})
.append(qweb.render('web_editor.components.switch', {id: 'switch-' + item.id, label: item.name})));
$li.find('input').prop('checked', !!item.active);
$menu.append($li);
if (current_group !== item.inherit_id[1]) {
current_group = item.inherit_id[1];
$menu.append("<li class=\"dropdown-header\">" + current_group + "</li>");
}
var $li = $('<li/>', {role: 'presentation'})
.append($('<a/>', {href: '#', 'data-view-id': item.id, role: 'menuitem'})
.append(qweb.render('web_editor.components.switch', {id: 'switch-' + item.id, label: item.name})));
$li.find('input').prop('checked', !!item.active);
$menu.append($li);
});
});
},
@@ -122,7 +122,7 @@ animation.registry.share = animation.Class.extend({
animation.registry.media_video = animation.Class.extend({
selector: ".media_iframe_video",
start: function () {
if (!this.$target.has('.media_iframe_video_size').length) {
if (!this.$target.has('> iframe').length) {
var editor = '<div class="css_editable_mode_display">&nbsp;</div>';
var size = '<div class="media_iframe_video_size">&nbsp;</div>';
this.$target.html(editor+size+'<iframe src="'+_.escape(this.$target.data("src"))+'" frameborder="0" allowfullscreen="allowfullscreen"></iframe>');
@@ -311,8 +311,10 @@ options.registry.parallax = options.Class.extend({
this.$target.data("snippet-view", new animation.registry.parallax(this.$target));
}
this.scroll();
this.buildingBlock.$el.on("snippet-dropped snippet-activated", this._refresh.bind(this));
this.$target.on('snippet-option-change snippet-option-preview', this._refresh.bind(this));
this._refresh_callback = this._refresh.bind(this);
this.buildingBlock.$el.on("snippet-dropped snippet-activated", this._refresh_callback);
this.$target.on('snippet-option-change snippet-option-preview', this._refresh_callback);
},
scroll: function (type, value) {
this.$target.attr("data-scroll-background-ratio", value);
@@ -330,6 +332,11 @@ options.registry.parallax = options.Class.extend({
this._super.apply(this, arguments);
this._refresh();
},
on_remove: function () {
this._super.apply(this, arguments);
this.$target.off("snippet-option-change snippet-option-preview", this._refresh_callback);
this.buildingBlock.$el.off("snippet-dropped snippet-activated", this._refresh_callback);
},
});
options.registry.ul = options.Class.extend({
@@ -1,52 +1,25 @@
// MODALS
body .modal {
a {
&:focus, &:active, &:focus:active {
outline: none!important;
}
}
&.o_website_modal {
font-family: @o-we-font-family;
h1, h2, h3, h4, h5, h6, .h1, .h2, .h3, .h4, .h5, .h6 {
font-family: @o-we-font-family;
}
.o-w-preserve-base();
.o-w-preserve-headings();
.o-w-preserve-forms();
.o-w-preserve-links();
.o-w-preserve-btn();
.o-w-preserve-wells();
.o-w-preserve-modals();
.modal-content {
border-radius: 0;
color: initial!important;
background-color: @o-we-color-paper!important;
.modal-header .o_subtitle {
margin-left: 10px;
}
.modal-body {
color: inherit!important;
background-color: transparent!important;
.o_modal_header {
.o-webclient-padding(@top: 10px, @bottom: 10px);
.clearfix();
}
}
.modal-footer {
text-align: left;
.btn {
border-radius: 0;
&.btn-primary {
.button-variant(white, @odoo-brand-optional, @odoo-brand-optional);
text-transform: uppercase;
}
&.btn-default {
.button-variant(@odoo-brand-optional, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0));
text-transform: uppercase;
}
&.btn-link {
.button-variant(@odoo-brand-optional, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0));
}
}
}
}
@media (min-width: @screen-sm-min) {
@@ -89,7 +62,6 @@ body .modal {
.modal-content {
background-color: black!important;
color: lightgray!important;
border: 3px outset gray;
border-radius: 20px;
@@ -99,6 +71,7 @@ body .modal {
font-family: @o-we-font-family;
h4 {
color: lightgray;
font-family:inherit;
font-weight: normal;
@@ -109,6 +82,8 @@ body .modal {
}
.modal-body {
background-color: inherit!important;
border-radius: 20px;
padding: 15px;
@mobile-preview-width: 320px;
@@ -324,6 +299,10 @@ body .modal {
// ACE EDITOR
.o_ace_view_editor {
.o-w-preserve-base();
.o-w-preserve-btn();
.o-w-preserve-forms();
.o-position-absolute(@odoo-navbar-height, 0, 0);
position: fixed;
z-index: 1001;
+21
View File
@@ -139,6 +139,24 @@ class TestViewSaving(common.TransactionCase):
Company = self.env['res.company']
View = self.env['ir.ui.view']
# create a view with an xmlid, like the file import would
with self.env.norecompute():
self.view_id = View.create(self.cr, self.uid, {
'name': "Test View",
'type': 'qweb',
'arch': ET.tostring(self.arch, encoding='utf-8').decode('utf-8')
})
imd = self.env['ir.model.data'].create({
'module': 'website',
'name': 'test_view',
'model': 'ir.ui.view',
'res_id': self.view_id,
})
# the xml_id of the view should not be flagged as 'noupdate'
self.assertEqual(View.browse(self.cr, self.uid, self.view_id).model_data_id, imd)
self.assertFalse(imd.noupdate)
replacement = ET.tostring(h.DIV(
h.H3("Column 2"),
h.UL(
@@ -149,6 +167,9 @@ class TestViewSaving(common.TransactionCase):
), encoding='utf-8')
self.view_id.save(value=replacement, xpath='/div/div[2]')
# the xml_id of the view should be flagged as 'noupdate'
self.assertTrue(imd.noupdate)
company = Company.browse(1)
self.assertEqual(company.name, "Acme Corporation")
self.assertEqual(company.phone, "+12 3456789")
+20 -2
View File
@@ -31,6 +31,20 @@ class Blog(models.Model):
post_ids.write({'active': vals['active']})
return res
@api.multi
def message_post(self, parent_id=False, subtype=None, **kwargs):
""" Temporary workaround to avoid spam. If someone replies on a channel
through the 'Presentation Published' email, it should be considered as a
note as we don't want all channel followers to be notified of this answer. """
self.ensure_one()
if parent_id:
parent_message = self.env['mail.message'].sudo().browse(parent_id)
if parent_message.subtype_id and parent_message.subtype_id == self.env.ref('website_blog.mt_blog_blog_published'):
if kwargs.get('subtype_id'):
kwargs['subtype_id'] = False
subtype = 'mail.mt_note'
return super(Blog, self).message_post(parent_id=parent_id, subtype=subtype, **kwargs)
@api.multi
def all_tags(self, min_limit=1):
req = """
@@ -87,11 +101,13 @@ class BlogPost(models.Model):
blog_post.website_url = "/blog/%s/post/%s" % (slug(blog_post.blog_id), slug(blog_post))
@api.multi
@api.depends('post_date', 'visits')
def _compute_ranking(self):
res = {}
for blog_post in self:
age = datetime.now() - fields.Datetime.from_string(blog_post.post_date)
res[blog_post.id] = blog_post.visits * (0.5 + random.random()) / max(3, age.days)
if blog_post.id: # avoid to rank one post not yet saved and so withtout post_date in case of an onchange.
age = datetime.now() - fields.Datetime.from_string(blog_post.post_date)
res[blog_post.id] = blog_post.visits * (0.5 + random.random()) / max(3, age.days)
return res
def _default_content(self):
@@ -167,6 +183,8 @@ class BlogPost(models.Model):
def _set_post_date(self):
for blog_post in self:
blog_post.published_date = blog_post.post_date
if not blog_post.published_date:
blog_post._write(dict(post_date=blog_post.create_date)) # dont trigger inverse function
def _check_for_publication(self, vals):
if vals.get('website_published'):
@@ -298,7 +298,7 @@
<div class="modal-body" id="sign-dialog">
<p>
<span>I agree that by signing this proposal, I
accept it on the behalf of </span><b t-field="quotation.company_id"/><span>, for an amount of </span>
accept it on the behalf of </span><b t-field="quotation.partner_id.commercial_partner_id"/><span>, for an amount of </span>
<b data-id="total_amount" t-field="quotation.amount_total"
t-options='{"widget": "monetary", "display_currency": quotation.pricelist_id.currency_id}'/>
<span>with payment terms: </span><b t-field="quotation.payment_term_id"/>.
@@ -325,7 +325,7 @@
<div class="modal-body" id="sign-dialog">
<p>
<span>I agree that by paying this proposal, I
accept it on the behalf of </span><b t-field="quotation.company_id"/><span>, for an amount of </span>
accept it on the behalf of </span><b t-field="quotation.partner_id.commercial_partner_id"/><span>, for an amount of </span>
<b data-id="total_amount" t-field="quotation.amount_total"
t-options='{"widget": "monetary", "display_currency": quotation.pricelist_id.currency_id}'/>
<span>with payment terms: </span><b t-field="quotation.payment_term_id"/>.
-1
View File
@@ -207,7 +207,6 @@ class Product(models.Model):
self.ensure_one()
return self.product_tmpl_id.website_publish_button()
class ProductAttribute(models.Model):
_inherit = "product.attribute"
@@ -55,7 +55,7 @@ $(document).ready(function () {
}
$form.off('submit');
ajax.jsonRpc('/shop/payment/transaction/' + acquirer_id, 'call', params).then(function (data) {
$(data).submit();
$(data).appendTo('body').submit();
});
return false;
});
+14
View File
@@ -164,6 +164,20 @@ class Channel(models.Model):
self.with_context(active_test=False).mapped('slide_ids').write({'active': vals['active']})
return res
@api.multi
def message_post(self, parent_id=False, subtype=None, **kwargs):
""" Temporary workaround to avoid spam. If someone replies on a channel
through the 'Presentation Published' email, it should be considered as a
note as we don't want all channel followers to be notified of this answer. """
self.ensure_one()
if parent_id:
parent_message = self.env['mail.message'].sudo().browse(parent_id)
if parent_message.subtype_id and parent_message.subtype_id == self.env.ref('website_slides.mt_channel_slide_published'):
if kwargs.get('subtype_id'):
kwargs['subtype_id'] = False
subtype = 'mail.mt_note'
return super(Channel, self).message_post(parent_id=parent_id, subtype=subtype, **kwargs)
class Category(models.Model):
""" Channel contain various categories to manage its slides """
+9 -5
View File
@@ -1208,6 +1208,7 @@ class IrModelData(models.Model):
datas = self.search([('module', 'in', modules_to_remove)])
wkf_todo = []
to_unlink = tools.OrderedSet()
undeletable = self.browse([])
for data in datas.sorted(key='id', reverse=True):
model = data.model
@@ -1232,6 +1233,7 @@ class IrModelData(models.Model):
_logger.info('Unable to force processing of workflow for item %s@%s in order to leave activity to be deleted', res_id, model, exc_info=True)
def unlink_if_refcount(to_unlink):
undeletable = self.browse()
for model, res_id in to_unlink:
external_ids = self.search([('model', '=', model), ('res_id', '=', res_id)])
if external_ids - datas:
@@ -1255,28 +1257,30 @@ class IrModelData(models.Model):
self.env[model].browse(res_id).unlink()
except Exception:
_logger.info('Unable to delete %s@%s', res_id, model, exc_info=True)
undeletable += external_ids
self._cr.execute('ROLLBACK TO SAVEPOINT record_unlink_save')
else:
self._cr.execute('RELEASE SAVEPOINT record_unlink_save')
return undeletable
# Remove non-model records first, then model fields, and finish with models
unlink_if_refcount(item for item in to_unlink if item[0] not in ('ir.model', 'ir.model.fields', 'ir.model.constraint'))
unlink_if_refcount(item for item in to_unlink if item[0] == 'ir.model.constraint')
undeletable += unlink_if_refcount(item for item in to_unlink if item[0] not in ('ir.model', 'ir.model.fields', 'ir.model.constraint'))
undeletable += unlink_if_refcount(item for item in to_unlink if item[0] == 'ir.model.constraint')
modules = self.env['ir.module.module'].search([('name', 'in', modules_to_remove)])
constraints = self.env['ir.model.constraint'].search([('module', 'in', modules.ids)])
constraints._module_data_uninstall()
unlink_if_refcount(item for item in to_unlink if item[0] == 'ir.model.fields')
undeletable += unlink_if_refcount(item for item in to_unlink if item[0] == 'ir.model.fields')
relations = self.env['ir.model.relation'].search([('module', 'in', modules.ids)])
relations._module_data_uninstall()
unlink_if_refcount(item for item in to_unlink if item[0] == 'ir.model')
undeletable += unlink_if_refcount(item for item in to_unlink if item[0] == 'ir.model')
self._cr.commit()
datas.unlink()
(datas - undeletable).unlink()
@api.model
def _process_end(self, modules):
+13 -4
View File
@@ -229,10 +229,11 @@ actual arch.
for view, view_wo_lang in zip(self, self.with_context(lang=None)):
view_wo_lang.arch = view.arch_base
@api.depends('write_date')
def _compute_model_data_id(self):
# get the last ir_model_data record corresponding to self
# get the first ir_model_data record corresponding to self
domain = [('model', '=', 'ir.ui.view'), ('res_id', 'in', self.ids)]
for data in self.env['ir.model.data'].search_read(domain, ['res_id']):
for data in self.env['ir.model.data'].search_read(domain, ['res_id'], order='id desc'):
view = self.browse(data['res_id'])
view.model_data_id = data['id']
@@ -343,7 +344,15 @@ actual arch.
values['name'] = "%s %s" % (values.get('model'), values['type'])
self.clear_caches()
return super(View, self).create(self._compute_defaults(values))
if 'install_mode_data' in self._context:
# the view is created from a data file by installing a module; delay
# the recomputation of field 'model_data_id' until its xmlid record
# is created
with self.env.norecompute():
return super(View, self).create(self._compute_defaults(values))
else:
return super(View, self).create(self._compute_defaults(values))
@api.multi
def write(self, vals):
@@ -1168,4 +1177,4 @@ actual arch.
try:
self.browse(vid)._check_xml()
except Exception as e:
self.raise_view_error("Can't validate view: %s" % e.message, vid)
self.raise_view_error("Can't validate view:\n%s" % (e.message or repr(e)), vid)
+8
View File
@@ -12,6 +12,14 @@ from odoo.tests import common
from odoo.tools import mute_logger
class ViewXMLID(common.TransactionCase):
def test_model_data_id(self):
""" Check whether views know their xmlid record. """
view = self.env.ref('base.view_company_form')
self.assertTrue(view)
self.assertTrue(view.model_data_id)
self.assertEqual(view.model_data_id.complete_name, 'base.view_company_form')
class ViewCase(common.TransactionCase):
def setUp(self):
super(ViewCase, self).setUp()
+2 -2
View File
@@ -23,7 +23,7 @@ class CreatorCase(common.TransactionCase):
def export(self, value, fields=('value',), context=None):
record = self.make(value)
record.invalidate_cache()
return record._BaseModel__export_rows([f.split('/') for f in fields])
return record._export_rows([f.split('/') for f in fields])
class test_boolean_field(CreatorCase):
@@ -449,7 +449,7 @@ class test_o2m_multiple(CreatorCase):
def export(self, value=None, fields=('child1', 'child2',), context=None, **values):
record = self.make(value, **values)
return record._BaseModel__export_rows([f.split('/') for f in fields])
return record._export_rows([f.split('/') for f in fields])
def test_empty(self):
self.assertEqual(
@@ -3,5 +3,6 @@
from . import test_new_fields
from . import test_onchange
from . import test_attributes
from . import test_one2many
from . import test_ui
from . import test_domain
@@ -176,7 +176,7 @@ class TestOnChange(common.TransactionCase):
self.assertEqual(values, {
'name': partner.name,
'partner': partner.id,
'lines': [(5,), (4, line.id)],
'lines': [(6, 0, [line.id])],
})
# modify 'partner'
@@ -0,0 +1,65 @@
# -*- coding: utf-8 -*-
from openerp.tests.common import TransactionCase
class One2manyCase(TransactionCase):
def setUp(self):
super(One2manyCase, self).setUp()
self.Line = self.env["test_new_api.multi.line"]
self.multi = self.env["test_new_api.multi"].create({
"name": "What is up?"
})
def operations(self):
"""Run operations on o2m fields to check all works fine."""
# Check the lines first
self.assertItemsEqual(
self.multi.lines.mapped('name'),
map(str, range(10)))
# Modify the first line and drop the last one
self.multi.lines[0].name = "hello"
self.multi.lines = self.multi.lines[:-1]
self.assertEqual(len(self.multi.lines), 9)
self.assertIn("hello", self.multi.lines.mapped('name'))
# Invalidate the cache and check again; this crashes if the value
# of self.multi.lines in cache contains new records
self.multi.invalidate_cache()
self.assertEqual(len(self.multi.lines), 9)
self.assertIn("hello", self.multi.lines.mapped('name'))
def test_new_one_by_one(self):
"""Check lines created with ``new()`` and appended one by one."""
for name in range(10):
self.multi.lines |= self.Line.new({"name": str(name)})
self.operations()
def test_new_single(self):
"""Check lines created with ``new()`` and added in one step."""
self.multi.lines = self.Line.browse(
[self.Line.new({"name": str(name)}).id for name in range(10)]
)
self.operations()
def test_create_one_by_one(self):
"""Check lines created with ``create()`` and appended one by one."""
for name in range(10):
self.multi.lines |= self.Line.create({"name": str(name)})
self.operations()
def test_create_single(self):
"""Check lines created with ``create()`` and added in one step."""
self.multi.lines = self.Line.browse(
[self.Line.create({"name": str(name)}).id for name in range(10)]
)
self.operations()
def test_rpcstyle_one_by_one(self):
"""Check lines created with RPC style and appended one by one."""
for name in range(10):
self.multi.lines = [(0, 0, {"name": str(name)})]
self.operations()
def test_rpcstyle_single(self):
"""Check lines created with RPC style and added in one step"""
self.multi.lines = [(0, 0, {'name': str(name)}) for name in range(10)]
self.operations()
@@ -22,6 +22,7 @@ _logger = logging.getLogger(__name__)
class TestPyLint(TransactionCase):
ENABLED_CODES = [
'E0601', # using variable before assignment
'W0123', # eval used
'W0101', # unreachable code
]
+11 -5
View File
@@ -894,10 +894,12 @@ class Field(object):
env.invalidate(spec)
else:
# simply write to the database, and update cache
# Write to database
write_value = self.convert_to_write(self.convert_to_record(value, record), record)
record.write({self.name: write_value})
record._cache[self] = value
# Update the cache unless value contains a new record
if not (self.relational and not all(value)):
record._cache[self] = value
############################################################################
#
@@ -1987,7 +1989,7 @@ class _RelationalMulti(_Relational):
def convert_to_write(self, value, record):
# make result with new and existing records
result = [(5,)]
result = [(6, 0, [])]
for record in value:
if not record.id:
values = {name: record[name] for name in record._cache}
@@ -1998,7 +2000,7 @@ class _RelationalMulti(_Relational):
values = record._convert_to_write(values)
result.append((1, record.id, values))
else:
result.append((4, record.id))
result[0][2].append(record.id)
return result
def convert_to_onchange(self, value, record, fnames=()):
@@ -2171,7 +2173,11 @@ class One2many(_RelationalMulti):
query = "SELECT id FROM %s WHERE %s=%%s AND id <> ALL(%%s)" % (comodel._table, inverse)
comodel._cr.execute(query, (record.id, act[2] or [0]))
lines = comodel.browse([row[0] for row in comodel._cr.fetchall()])
lines.write({inverse: False})
inverse_field = comodel._fields[inverse]
if inverse_field.ondelete == 'cascade':
lines.unlink()
else:
lines.write({inverse: False})
class Many2many(_RelationalMulti):
+7 -5
View File
@@ -738,7 +738,7 @@ class BaseModel(object):
return '__export__.' + name
@api.multi
def __export_rows(self, fields):
def _export_rows(self, fields):
""" Export fields of the records in ``self``.
:param fields: list of lists of fields to traverse
@@ -785,7 +785,7 @@ class BaseModel(object):
# recursively export the fields that follow name
fields2 = [(p[1:] if p and p[0] == name else []) for p in fields]
lines2 = value.__export_rows(fields2)
lines2 = value._export_rows(fields2)
if lines2:
# merge first line with record's main line
for j, val in enumerate(lines2[0]):
@@ -804,6 +804,9 @@ class BaseModel(object):
return lines
# backward compatibility
__export_rows = _export_rows
@api.multi
def export_data(self, fields_to_export, raw_data=False):
""" Export fields for selected objects
@@ -817,7 +820,7 @@ class BaseModel(object):
fields_to_export = map(fix_import_export_id_paths, fields_to_export)
if raw_data:
self = self.with_context(export_raw_data=True)
return {'datas': self.__export_rows(fields_to_export)}
return {'datas': self._export_rows(fields_to_export)}
@api.model
def load(self, fields, data):
@@ -3505,8 +3508,7 @@ class BaseModel(object):
``(6, _, ids)``
replaces all existing records in the set by the ``ids`` list,
equivalent to using the command ``5`` followed by a command
``4`` for each ``id`` in ``ids``. Can not be used on
:class:`~odoo.fields.One2many`.
``4`` for each ``id`` in ``ids``.
.. note:: Values marked as ``_`` in the list above are ignored and
can be anything, generally ``0`` or ``False``.
+1 -1
View File
@@ -363,7 +363,7 @@ class configmanager(object):
rcfilepath = old_rcfilepath
self.rcfile = os.path.abspath(
self.config_file or opt.config or os.environ.get('OPENERP_SERVER') or rcfilepath)
self.config_file or opt.config or os.environ.get('ODOO_RC') or os.environ.get('OPENERP_SERVER') or rcfilepath)
self.load()
# Verify that we want to log or not, if not the output will go to stdout