[MERGE] forward port branch saas-16 up to 278e478d55
This commit is contained in:
@@ -272,6 +272,11 @@ file_filter = addons/hr_expense/i18n/<lang>.po
|
||||
source_file = addons/hr_expense/i18n/hr_expense.pot
|
||||
source_lang = en
|
||||
|
||||
[odoo-master.hr_expense_check]
|
||||
file_filter = addons/hr_expense_check/i18n/<lang>.po
|
||||
source_file = addons/hr_expense_check/i18n/hr_expense_check.pot
|
||||
source_lang = en
|
||||
|
||||
[odoo-master.hr_gamification]
|
||||
file_filter = addons/hr_gamification/i18n/<lang>.po
|
||||
source_file = addons/hr_gamification/i18n/hr_gamification.pot
|
||||
|
||||
@@ -897,6 +897,11 @@ class AccountMoveLine(models.Model):
|
||||
|
||||
@api.multi
|
||||
def reconcile(self, writeoff_acc_id=False, writeoff_journal_id=False):
|
||||
# Empty self can happen if the user tries to reconcile entries which are already reconciled.
|
||||
# The calling method might have filtered out reconciled lines.
|
||||
if not self:
|
||||
return True
|
||||
|
||||
#Perform all checks on lines
|
||||
company_ids = set()
|
||||
all_accounts = []
|
||||
@@ -1425,7 +1430,7 @@ class AccountPartialReconcile(models.Model):
|
||||
help='Utility field to express amount currency')
|
||||
company_id = fields.Many2one('res.company', related='debit_move_id.company_id', store=True, string='Currency')
|
||||
full_reconcile_id = fields.Many2one('account.full.reconcile', string="Full Reconcile", copy=False)
|
||||
max_date = fields.Datetime(string='Max Date of Matched Lines', compute='_compute_max_date',
|
||||
max_date = fields.Date(string='Max Date of Matched Lines', compute='_compute_max_date',
|
||||
readonly=True, copy=False, stored=True,
|
||||
help='Technical field used to determine at which date this reconciliation needs to be shown on the aged receivable/payable reports.')
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import time
|
||||
from odoo import api, models, _
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tools import float_is_zero
|
||||
from datetime import datetime
|
||||
from dateutil.relativedelta import relativedelta
|
||||
@@ -196,6 +197,9 @@ class ReportAgedPartnerBalance(models.AbstractModel):
|
||||
|
||||
@api.model
|
||||
def get_report_values(self, docids, data=None):
|
||||
if not data.get('form') or not self.env.context.get('active_model') or not self.env.context.get('active_id'):
|
||||
raise UserError(_("Some data are missing, this report cannot be printed."))
|
||||
|
||||
total = []
|
||||
model = self.env.context.get('active_model')
|
||||
docs = self.env[model].browse(self.env.context.get('active_id'))
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import time
|
||||
from odoo import api, models
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class ReportTrialBalance(models.AbstractModel):
|
||||
@@ -50,13 +51,18 @@ class ReportTrialBalance(models.AbstractModel):
|
||||
res['balance'] = account_result[account.id].get('balance')
|
||||
if display_account == 'all':
|
||||
account_res.append(res)
|
||||
if display_account in ['movement', 'not_zero'] and not currency.is_zero(res['balance']):
|
||||
if display_account == 'not_zero' and not currency.is_zero(res['balance']):
|
||||
account_res.append(res)
|
||||
if display_account == 'movement' and (not currency.is_zero(res['debit']) or not currency.is_zero(res['credit'])):
|
||||
account_res.append(res)
|
||||
return account_res
|
||||
|
||||
|
||||
@api.model
|
||||
def get_report_values(self, docids, data=None):
|
||||
if not data.get('form') or not self.env.context.get('active_model'):
|
||||
raise UserError(_("Some data are missing, this report cannot be printed."))
|
||||
|
||||
self.model = self.env.context.get('active_model')
|
||||
docs = self.env[self.model].browse(self.env.context.get('active_ids', []))
|
||||
display_account = data['form'].get('display_account')
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import time
|
||||
from odoo import api, models
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class ReportGeneralLedger(models.AbstractModel):
|
||||
@@ -109,6 +110,9 @@ class ReportGeneralLedger(models.AbstractModel):
|
||||
|
||||
@api.model
|
||||
def get_report_values(self, docids, data=None):
|
||||
if not data.get('form') or not self.env.context.get('active_model'):
|
||||
raise UserError(_("Some data are missing, this report cannot be printed."))
|
||||
|
||||
self.model = self.env.context.get('active_model')
|
||||
docs = self.env[self.model].browse(self.env.context.get('active_ids', []))
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import time
|
||||
from odoo import api, models
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class ReportJournal(models.AbstractModel):
|
||||
@@ -95,6 +96,9 @@ class ReportJournal(models.AbstractModel):
|
||||
|
||||
@api.model
|
||||
def get_report_values(self, docids, data=None):
|
||||
if not data.get('form'):
|
||||
raise UserError(_("Some data are missing, this report cannot be printed."))
|
||||
|
||||
target_move = data['form'].get('target_move', 'all')
|
||||
sort_selection = data['form'].get('sort_selection', 'date')
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from datetime import datetime
|
||||
import time
|
||||
from odoo import api, models
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tools import DEFAULT_SERVER_DATE_FORMAT
|
||||
|
||||
|
||||
@@ -69,6 +70,9 @@ class ReportPartnerLedger(models.AbstractModel):
|
||||
|
||||
@api.model
|
||||
def get_report_values(self, docids, data=None):
|
||||
if not data.get('form'):
|
||||
raise UserError(_("Some data are missing, this report cannot be printed."))
|
||||
|
||||
data['computed'] = {}
|
||||
|
||||
obj_partner = self.env['res.partner']
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import time
|
||||
from odoo import api, models
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tools import pycompat
|
||||
|
||||
|
||||
@@ -145,6 +146,9 @@ class ReportFinancial(models.AbstractModel):
|
||||
|
||||
@api.model
|
||||
def get_report_values(self, docids, data=None):
|
||||
if not data.get('form') or not self.env.context.get('active_model') or not self.env.context.get('active_id'):
|
||||
raise UserError(_("Some data are missing, this report cannot be printed."))
|
||||
|
||||
self.model = self.env.context.get('active_model')
|
||||
docs = self.env[self.model].browse(self.env.context.get('active_id'))
|
||||
report_lines = self.get_account_lines(data.get('form'))
|
||||
|
||||
@@ -729,7 +729,7 @@
|
||||
<field name="name">Bank Statement Lines</field>
|
||||
<field name="res_model">account.bank.statement.line</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">tree</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="domain">[]</field>
|
||||
<field name="context">{}</field>
|
||||
<field name="search_view_id" ref="view_bank_statement_line_search"/>
|
||||
|
||||
@@ -109,7 +109,7 @@ var BarcodeParser = Class.extend({
|
||||
ean = ean.substr(0,13);
|
||||
|
||||
for(var n = 0, count = (13 - ean.length); n < count; n++){
|
||||
ean = ean + '0';
|
||||
ean = '0' + ean;
|
||||
}
|
||||
return ean.substr(0,12) + this.ean_checksum(ean);
|
||||
},
|
||||
|
||||
@@ -281,6 +281,11 @@ class Lead(models.Model):
|
||||
|
||||
if vals.get('user_id') and 'date_open' not in vals:
|
||||
vals['date_open'] = fields.Datetime.now()
|
||||
|
||||
if context.get('default_partner_id') and not vals.get('email_from'):
|
||||
partner = self.env['res.partner'].browse(context['default_partner_id'])
|
||||
vals['email_from'] = partner.email
|
||||
|
||||
# context: no_log, because subtype already handle this
|
||||
return super(Lead, self.with_context(context, mail_create_nolog=True)).create(vals)
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import calendar
|
||||
from datetime import timedelta
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from odoo import api, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class HrHolidaySummaryReport(models.AbstractModel):
|
||||
@@ -107,6 +108,9 @@ class HrHolidaySummaryReport(models.AbstractModel):
|
||||
|
||||
@api.model
|
||||
def get_report_values(self, docids, data=None):
|
||||
if not data.get('form'):
|
||||
raise UserError(_("Some data are missing, this report cannot be printed."))
|
||||
|
||||
holidays_report = self.env['ir.actions.report']._get_report_from_name('hr_holidays.report_holidayssummary')
|
||||
holidays = self.env['hr.holidays'].browse(self.ids)
|
||||
return {
|
||||
|
||||
@@ -6,6 +6,7 @@ from datetime import datetime
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class ContributionRegisterReport(models.AbstractModel):
|
||||
@@ -29,6 +30,9 @@ class ContributionRegisterReport(models.AbstractModel):
|
||||
|
||||
@api.model
|
||||
def get_report_values(self, docids, data=None):
|
||||
if not data.get('form'):
|
||||
raise UserError(_("Some data are missing, this report cannot be printed."))
|
||||
|
||||
register_ids = self.env.context.get('active_ids', [])
|
||||
contrib_registers = self.env['hr.contribution.register'].browse(register_ids)
|
||||
date_from = data['form'].get('date_from', fields.Date.today())
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
</record>
|
||||
<record id="l10nch_chart_template" model="account.chart.template">
|
||||
<field name="name">Plan comptable 2015</field>
|
||||
<field name="code_digits">1</field>
|
||||
<field name="code_digits">4</field>
|
||||
<field name="bank_account_code_prefix">102</field>
|
||||
<field name="cash_account_code_prefix">100</field>
|
||||
<field name="currency_id" ref="base.CHF"/>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,7 @@
|
||||
from datetime import date
|
||||
|
||||
from odoo import api, models
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class ReportHrSalaryEmployeeBymonth(models.AbstractModel):
|
||||
@@ -93,6 +94,9 @@ class ReportHrSalaryEmployeeBymonth(models.AbstractModel):
|
||||
|
||||
@api.model
|
||||
def get_report_values(self, docids, data=None):
|
||||
if not data.get('form') or not self.env.context.get('active_model') or not self.env.context.get('active_id'):
|
||||
raise UserError(_("Some data are missing, this report cannot be printed."))
|
||||
|
||||
model = self.env.context.get('active_model')
|
||||
docs = self.env[model].browse(self.env.context.get('active_id'))
|
||||
get_periods, months, total_mnths = self.get_periods(data['form'])
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
from datetime import date
|
||||
|
||||
from odoo import api, models
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tools import pycompat
|
||||
|
||||
|
||||
@@ -128,6 +129,9 @@ class EmployeesYearlySalaryReport(models.AbstractModel):
|
||||
|
||||
@api.model
|
||||
def get_report_values(self, docids, data=None):
|
||||
if not self.env.context.get('active_model') or not self.env.context.get('active_id'):
|
||||
raise UserError(_("Some data are missing, this report cannot be printed."))
|
||||
|
||||
model = self.env.context.get('active_model')
|
||||
docs = self.env[model].browse(self.env.context.get('active_id'))
|
||||
return {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from operator import itemgetter
|
||||
from collections import OrderedDict
|
||||
|
||||
import json
|
||||
import datetime
|
||||
@@ -94,9 +94,9 @@ class LunchOrder(models.Model):
|
||||
}))
|
||||
|
||||
if self.previous_order_ids:
|
||||
lunch_data = []
|
||||
lunch_data = {}
|
||||
for line in self.previous_order_ids:
|
||||
lunch_data.append({
|
||||
lunch_data[line.id] = {
|
||||
'line_id': line.id,
|
||||
'product_id': line.product_id.id,
|
||||
'product_name': line.product_id.name,
|
||||
@@ -105,8 +105,9 @@ class LunchOrder(models.Model):
|
||||
'price': line.price,
|
||||
'date': line.date,
|
||||
'currency_id': line.currency_id.id,
|
||||
})
|
||||
lunch_data.sort(key=itemgetter('date', 'line_id'), reverse=True)
|
||||
}
|
||||
# sort the old lunch orders by (date, id)
|
||||
lunch_data = OrderedDict(sorted(lunch_data.items(), key=lambda t: (t[1]['date'], t[0]), reverse=True))
|
||||
self.previous_order_widget = json.dumps(lunch_data)
|
||||
|
||||
@api.one
|
||||
|
||||
@@ -75,8 +75,14 @@ class MailTracking(models.Model):
|
||||
assert type in ('new', 'old')
|
||||
result = []
|
||||
for record in self:
|
||||
if record.field_type in ['integer', 'float', 'char', 'text', 'datetime', 'monetary']:
|
||||
if record.field_type in ['integer', 'float', 'char', 'text', 'monetary']:
|
||||
result.append(getattr(record, '%s_value_%s' % (type, record.field_type)))
|
||||
elif record.field_type == 'datetime':
|
||||
if record['%s_value_datetime' % type]:
|
||||
new_datetime = getattr(record, '%s_value_datetime' % type)
|
||||
result.append('%sZ' % new_datetime)
|
||||
else:
|
||||
result.append(record['%s_value_datetime' % type])
|
||||
elif record.field_type == 'date':
|
||||
if record['%s_value_datetime' % type]:
|
||||
new_date = datetime.strptime(record['%s_value_datetime' % type], tools.DEFAULT_SERVER_DATETIME_FORMAT).date()
|
||||
|
||||
@@ -217,14 +217,22 @@ function make_message (data) {
|
||||
// can not be done in preprocess, since it alter the original value
|
||||
if (msg.tracking_value_ids && msg.tracking_value_ids.length) {
|
||||
_.each(msg.tracking_value_ids, function(f) {
|
||||
if (_.contains(['date', 'datetime'], f.field_type)) {
|
||||
var format = (f.field_type === 'date') ? 'LL' : 'LLL';
|
||||
if (f.field_type === 'datetime') {
|
||||
var format = 'LLL';
|
||||
if (f.old_value) {
|
||||
f.old_value = moment.utc(f.old_value).local().format(format);
|
||||
}
|
||||
if (f.new_value) {
|
||||
f.new_value = moment.utc(f.new_value).local().format(format);
|
||||
}
|
||||
} else if (f.field_type === 'date') {
|
||||
var format = 'LL';
|
||||
if (f.old_value) {
|
||||
f.old_value = moment(f.old_value).local().format(format);
|
||||
}
|
||||
if (f.new_value) {
|
||||
f.new_value = moment(f.new_value).local().format(format);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -312,7 +312,7 @@ class MassMailing(models.Model):
|
||||
create_date = fields.Datetime(string='Creation Date')
|
||||
sent_date = fields.Datetime(string='Sent Date', oldname='date', copy=False)
|
||||
schedule_date = fields.Datetime(string='Schedule in the Future')
|
||||
body_html = fields.Html(string='Body', translate=html_translate, sanitize_attributes=False)
|
||||
body_html = fields.Html(string='Body', sanitize_attributes=False)
|
||||
attachment_ids = fields.Many2many('ir.attachment', 'mass_mailing_ir_attachments_rel',
|
||||
'mass_mailing_id', 'attachment_id', string='Attachments')
|
||||
keep_archives = fields.Boolean(string='Keep Archives')
|
||||
|
||||
@@ -32,17 +32,13 @@ KanbanColumn.include({
|
||||
});
|
||||
|
||||
FieldTextHtml.include({
|
||||
get_datarecord: function () {
|
||||
getDatarecord: function () {
|
||||
/* Avoid extremely long URIs by whitelisting fields in the datarecord
|
||||
that get set as a get parameter */
|
||||
var datarecord = this._super();
|
||||
if (this.view.model === 'mail.mass_mailing') {
|
||||
if (this.model === 'mail.mass_mailing') {
|
||||
// these fields can potentially get very long, let's remove them
|
||||
var blacklist = ['mailing_domain', 'contact_list_ids'];
|
||||
for (var k in blacklist) {
|
||||
delete datarecord[blacklist[k]];
|
||||
}
|
||||
delete datarecord[this.name];
|
||||
datarecord = _.omit(datarecord, ['mailing_domain', 'contact_list_ids', 'body_html']);
|
||||
}
|
||||
return datarecord;
|
||||
},
|
||||
|
||||
@@ -170,6 +170,7 @@ snippets_editor.Class.include({
|
||||
className: classname || "",
|
||||
img: $theme.data("img") || "",
|
||||
template: $theme.html().trim(),
|
||||
nowrap: !!$theme.data('nowrap'),
|
||||
get_image_info: function (filename) {
|
||||
if (images_info[filename]) {
|
||||
return images_info[filename];
|
||||
@@ -331,13 +332,20 @@ snippets_editor.Class.include({
|
||||
var $old_layout = $editable_area.find(".o_layout");
|
||||
// This wrapper structure is the only way to have a responsive and
|
||||
// centered fixed-width content column on all mail clients
|
||||
var $new_wrapper = $('<table/>', {class: 'o_mail_wrapper'});
|
||||
var $new_wrapper_content = $("<td/>", {class: 'o_mail_no_resize o_mail_wrapper_td oe_structure'});
|
||||
$new_wrapper.append($('<tr/>').append(
|
||||
$("<td/>", {class: 'o_mail_no_resize'}),
|
||||
$new_wrapper_content,
|
||||
$("<td/>", {class: 'o_mail_no_resize'})
|
||||
));
|
||||
var $new_wrapper, $new_wrapper_content;
|
||||
|
||||
if (theme_params.nowrap) {
|
||||
$new_wrapper = $new_wrapper_content = $("<div/>", {"class": "oe_structure"});
|
||||
}
|
||||
else {
|
||||
$new_wrapper = $('<table/>', {class: 'o_mail_wrapper'});
|
||||
$new_wrapper_content = $("<td/>", {class: 'o_mail_no_resize o_mail_wrapper_td oe_structure'});
|
||||
$new_wrapper.append($('<tr/>').append(
|
||||
$("<td/>", {class: 'o_mail_no_resize'}),
|
||||
$new_wrapper_content,
|
||||
$("<td/>", {class: 'o_mail_no_resize'})
|
||||
));
|
||||
}
|
||||
var $new_layout = $("<div/>", {"class": "o_layout " + theme_params.className}).append($new_wrapper);
|
||||
|
||||
var $contents;
|
||||
|
||||
@@ -117,26 +117,48 @@ class MrpBom(models.Model):
|
||||
Quantity describes the number of times you need the BoM: so the quantity divided by the number created by the BoM
|
||||
and converted into its UoM
|
||||
"""
|
||||
from collections import defaultdict
|
||||
|
||||
graph = defaultdict(list)
|
||||
V = set()
|
||||
|
||||
def check_cycle(v, visited, recStack, graph):
|
||||
visited[v] = True
|
||||
recStack[v] = True
|
||||
for neighbour in graph[v]:
|
||||
if visited[neighbour] == False:
|
||||
if check_cycle(neighbour, visited, recStack, graph) == True:
|
||||
return True
|
||||
elif recStack[neighbour] == True:
|
||||
return True
|
||||
recStack[v] = False
|
||||
return False
|
||||
|
||||
boms_done = [(self, {'qty': quantity, 'product': product, 'original_qty': quantity, 'parent_line': False})]
|
||||
lines_done = []
|
||||
templates_done = product.product_tmpl_id
|
||||
V |= set([product.product_tmpl_id.id])
|
||||
|
||||
bom_lines = [(bom_line, product, quantity, False) for bom_line in self.bom_line_ids]
|
||||
for bom_line in self.bom_line_ids:
|
||||
V |= set([bom_line.product_id.product_tmpl_id.id])
|
||||
graph[product.product_tmpl_id.id].append(bom_line.product_id.product_tmpl_id.id)
|
||||
while bom_lines:
|
||||
current_line, current_product, current_qty, parent_line = bom_lines[0]
|
||||
bom_lines = bom_lines[1:]
|
||||
|
||||
if current_line._skip_bom_line(current_product):
|
||||
continue
|
||||
if current_line.product_id.product_tmpl_id in templates_done:
|
||||
raise UserError(_('Recursion error! A product with a Bill of Material should not have itself in its BoM or child BoMs!'))
|
||||
|
||||
line_quantity = current_qty * current_line.product_qty
|
||||
bom = self._bom_find(product=current_line.product_id, picking_type=picking_type or self.picking_type_id, company_id=self.company_id.id)
|
||||
if bom.type == 'phantom':
|
||||
converted_line_quantity = current_line.product_uom_id._compute_quantity(line_quantity / bom.product_qty, bom.product_uom_id)
|
||||
bom_lines = [(line, current_line.product_id, converted_line_quantity, current_line) for line in bom.bom_line_ids] + bom_lines
|
||||
templates_done |= current_line.product_id.product_tmpl_id
|
||||
for bom_line in bom.bom_line_ids:
|
||||
graph[current_line.product_id.product_tmpl_id.id].append(bom_line.product_id.product_tmpl_id.id)
|
||||
if bom_line.product_id.product_tmpl_id.id in V and check_cycle(bom_line.product_id.product_tmpl_id.id, {key: False for key in V}, {key: False for key in V}, graph):
|
||||
raise UserError(_('Recursion error! A product with a Bill of Material should not have itself in its BoM or child BoMs!'))
|
||||
V |= set([bom_line.product_id.product_tmpl_id.id])
|
||||
boms_done.append((bom, {'qty': converted_line_quantity, 'product': current_product, 'original_qty': quantity, 'parent_line': current_line}))
|
||||
else:
|
||||
# We round up here because the user expects that if he has to consume a little more, the whole UOM unit
|
||||
|
||||
@@ -208,11 +208,8 @@ class MrpProduction(models.Model):
|
||||
@api.depends('move_raw_ids.quantity_done', 'move_finished_ids.quantity_done')
|
||||
def _compute_post_visible(self):
|
||||
for order in self:
|
||||
if order.product_tmpl_id._is_cost_method_standard():
|
||||
order.post_visible = any((x.quantity_done > 0 and x.state not in ['done', 'cancel']) for x in order.move_raw_ids) or \
|
||||
any((x.quantity_done > 0 and x.state not in ['done' 'cancel']) for x in order.move_finished_ids)
|
||||
else:
|
||||
order.post_visible = any((x.quantity_done > 0 and x.state not in ['done' 'cancel']) for x in order.move_finished_ids)
|
||||
order.post_visible = any(order.move_raw_ids.filtered(lambda x: (x.quantity_done) > 0 and (x.state not in ['done', 'cancel']))) or \
|
||||
any(order.move_finished_ids.filtered(lambda x: (x.quantity_done) > 0 and (x.state not in ['done', 'cancel'])))
|
||||
|
||||
@api.multi
|
||||
@api.depends('workorder_ids.state', 'move_finished_ids')
|
||||
@@ -269,7 +266,10 @@ class MrpProduction(models.Model):
|
||||
@api.model
|
||||
def create(self, values):
|
||||
if not values.get('name', False) or values['name'] == _('New'):
|
||||
values['name'] = self.env['ir.sequence'].next_by_code('mrp.production') or _('New')
|
||||
if values.get('picking_type_id'):
|
||||
values['name'] = self.env['stock.picking.type'].browse(values['picking_type_id']).sequence_id.next_by_id()
|
||||
else:
|
||||
values['name'] = self.env['ir.sequence'].next_by_code('mrp.production') or _('New')
|
||||
if not values.get('procurement_group_id'):
|
||||
values['procurement_group_id'] = self.env["procurement.group"].create({'name': values['name']}).id
|
||||
production = super(MrpProduction, self).create(values)
|
||||
|
||||
@@ -391,9 +391,12 @@ class MrpWorkorder(models.Model):
|
||||
domain = [('workorder_id', 'in', self.ids), ('date_end', '=', False)]
|
||||
if not doall:
|
||||
domain.append(('user_id', '=', self.env.user.id))
|
||||
not_productive_timelines = timeline_obj.browse()
|
||||
for timeline in timeline_obj.search(domain, limit=None if doall else 1):
|
||||
wo = timeline.workorder_id
|
||||
if timeline.loss_type != 'productive':
|
||||
if wo.duration_expected <= wo.duration:
|
||||
if timeline.loss_type == 'productive':
|
||||
not_productive_timelines += timeline
|
||||
timeline.write({'date_end': fields.Datetime.now()})
|
||||
else:
|
||||
maxdate = fields.Datetime.from_string(timeline.date_start) + relativedelta(minutes=wo.duration_expected - wo.duration)
|
||||
@@ -402,10 +405,12 @@ class MrpWorkorder(models.Model):
|
||||
timeline.write({'date_end': enddate})
|
||||
else:
|
||||
timeline.write({'date_end': maxdate})
|
||||
loss_id = self.env['mrp.workcenter.productivity.loss'].search([('loss_type', '=', 'performance')], limit=1)
|
||||
if not len(loss_id):
|
||||
raise UserError(_("You need to define at least one unactive productivity loss in the category 'Performance'. Create one from the Manufacturing app, menu: Configuration / Productivity Losses."))
|
||||
timeline.copy({'date_start': maxdate, 'date_end': enddate, 'loss_id': loss_id.id})
|
||||
not_productive_timelines += timeline.copy({'date_start': maxdate, 'date_end': enddate})
|
||||
if not_productive_timelines:
|
||||
loss_id = self.env['mrp.workcenter.productivity.loss'].search([('loss_type', '=', 'performance')], limit=1)
|
||||
if not len(loss_id):
|
||||
raise UserError(_("You need to define at least one unactive productivity loss in the category 'Performance'. Create one from the Manufacturing app, menu: Configuration / Productivity Losses."))
|
||||
not_productive_timelines.write({'loss_id': loss_id.id})
|
||||
return True
|
||||
|
||||
@api.multi
|
||||
|
||||
@@ -131,7 +131,30 @@ class TestBoM(TestMrpCommon):
|
||||
self.assertEqual(set((test_bom_2 | self.bom_2).ids), set([b[0].id for b in boms]))
|
||||
self.assertEqual(set((test_bom_2_l1 | test_bom_2_l4 | self.bom_2.bom_line_ids).ids), set([l[0].id for l in lines]))
|
||||
|
||||
# check recursion
|
||||
test_bom_2_l3.write({'attribute_value_ids': [(6, 0, [self.prod_attr1_v1.id])]})
|
||||
#check recursion
|
||||
test_bom_3 = self.env['mrp.bom'].create({
|
||||
'product_id': self.product_9.id,
|
||||
'product_tmpl_id': self.product_9.product_tmpl_id.id,
|
||||
'product_uom_id': self.product_9.uom_id.id,
|
||||
'product_qty': 1.0,
|
||||
'type': 'normal'
|
||||
})
|
||||
test_bom_4 = self.env['mrp.bom'].create({
|
||||
'product_id': self.product_10.id,
|
||||
'product_tmpl_id': self.product_10.product_tmpl_id.id,
|
||||
'product_uom_id': self.product_10.uom_id.id,
|
||||
'product_qty': 1.0,
|
||||
'type': 'phantom'
|
||||
})
|
||||
test_bom_3_l1 = self.env['mrp.bom.line'].create({
|
||||
'bom_id': test_bom_3.id,
|
||||
'product_id': self.product_10.id,
|
||||
'product_qty': 1.0,
|
||||
})
|
||||
test_bom_4_l1 = self.env['mrp.bom.line'].create({
|
||||
'bom_id': test_bom_4.id,
|
||||
'product_id': self.product_9.id,
|
||||
'product_qty': 1.0,
|
||||
})
|
||||
with self.assertRaises(exceptions.UserError):
|
||||
test_bom_2.explode(self.product_7_1, 4)
|
||||
test_bom_3.explode(self.product_9, 1)
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
<button name="button_plan" attrs="{'invisible': ['|', ('state', '!=', 'confirmed'), ('routing_id', '=', False)]}" type="object" string="Create Workorders" class="oe_highlight"/>
|
||||
<button name="open_produce_product" attrs="{'invisible': ['|', '|', '|', ('check_to_done', '=', True), ('availability', 'not in', ['partially_available', 'assigned']), ('state', 'not in', ('confirmed','progress')), ('routing_id', '!=', False)]}" string="Produce" type="object" class="oe_highlight"/>
|
||||
<button name="open_produce_product" attrs="{'invisible': ['|', '|', '|', ('check_to_done', '=', True), ('availability', '!=', 'waiting'), ('state', 'not in', ('confirmed','progress')), ('routing_id', '!=', False)]}" string="Produce" type="object"/>
|
||||
<button name="post_inventory" string="Post Inventory" type="object" attrs="{'invisible': [('post_visible', '=', False)]}"/>
|
||||
<button name="post_inventory" string="Post Inventory" type="object" attrs="{'invisible': [('post_visible', '=', False)]}" groups="base.group_no_one"/>
|
||||
<button name="action_cancel" type="object" string="Cancel" attrs="{'invisible': ['|', ('state', 'in', ('done','cancel')), ('check_to_done', '=', True)]}"/>
|
||||
<button name="button_scrap" type="object" string="Scrap" attrs="{'invisible': ['|', ('availability', '=', 'none'), ('state', 'in', ('cancel'))]}"/>
|
||||
<button name="button_unreserve" type="object" string="Unreserve" attrs="{'invisible': [('unreserve_visible', '=', False)]}"/>
|
||||
|
||||
@@ -150,7 +150,7 @@
|
||||
<tree editable="bottom" create="0" delete="0">
|
||||
<field name="product_id"/>
|
||||
<field name="quantity" readonly="1"/>
|
||||
<field name="lot_id" context="{'default_product_id': product_id}"/>
|
||||
<field name="lot_id" domain="[('product_id', '=', product_id)]" context="{'default_product_id': product_id}"/>
|
||||
<field name="quantity_done"/>
|
||||
<field name="move_id" invisible="1"/>
|
||||
<field name="done_wo" invisible="1"/>
|
||||
|
||||
@@ -13,7 +13,6 @@ var FieldPad = AbstractField.extend({
|
||||
events: {
|
||||
'click .oe_pad_switch': '_onToggleFullScreen',
|
||||
},
|
||||
supportedFieldTypes: ['char'],
|
||||
|
||||
/**
|
||||
* @override
|
||||
|
||||
@@ -527,7 +527,9 @@ class PaymentTransaction(models.Model):
|
||||
tx.write({'reference': str(tx.id)})
|
||||
|
||||
# Generate callback hash if it is configured on the tx; avoid generating unnecessary stuff
|
||||
if tx.callback_model_id and tx.callback_res_id and tx.sudo().callback_method:
|
||||
# (limited sudo env for checking callback presence, must work for manual transactions too)
|
||||
tx_sudo = tx.sudo()
|
||||
if tx_sudo.callback_model_id and tx_sudo.callback_res_id and tx_sudo.callback_method:
|
||||
tx.write({'callback_hash': tx._generate_callback_hash()})
|
||||
|
||||
return tx
|
||||
@@ -657,7 +659,13 @@ class PaymentTransaction(models.Model):
|
||||
@api.multi
|
||||
def execute_callback(self):
|
||||
res = None
|
||||
for transaction in self.filtered(lambda tx: tx.callback_model_id and tx.callback_res_id and tx.sudo().callback_method):
|
||||
for transaction in self:
|
||||
# limited sudo env, only for checking callback presence, not for running it!
|
||||
# manual transactions have no callback, and can pass without being run by admin user
|
||||
tx_sudo = transaction.sudo()
|
||||
if not (tx_sudo.callback_model_id and tx_sudo.callback_res_id and tx_sudo.callback_method):
|
||||
continue
|
||||
|
||||
valid_token = transaction._generate_callback_hash()
|
||||
if not consteq(ustr(valid_token), transaction.callback_hash):
|
||||
_logger.warning("Invalid callback signature for transaction %d" % (transaction.id))
|
||||
@@ -665,7 +673,7 @@ class PaymentTransaction(models.Model):
|
||||
|
||||
record = self.env[transaction.callback_model_id.model].browse(transaction.callback_res_id).exists()
|
||||
if record:
|
||||
res = getattr(record, transaction.sudo().callback_method)(transaction)
|
||||
res = getattr(record, transaction.callback_method)(transaction)
|
||||
else:
|
||||
_logger.warning("Did not found record %s.%s for callback of transaction %d" % (transaction.callback_model_id.model, transaction.callback_res_id, transaction.id))
|
||||
return res
|
||||
|
||||
@@ -5,7 +5,9 @@ from StringIO import StringIO
|
||||
from xml.etree import ElementTree as ET
|
||||
from uuid import uuid4
|
||||
|
||||
from odoo import _
|
||||
from odoo.exceptions import ValidationError, UserError
|
||||
from odoo import _
|
||||
|
||||
XMLNS = 'AnetApi/xml/v1/schema/AnetApiSchema.xsd'
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import hmac
|
||||
import logging
|
||||
import time
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.addons.payment.models.payment_acquirer import ValidationError
|
||||
from odoo.addons.payment_authorize.controllers.main import AuthorizeController
|
||||
from odoo.tools.float_utils import float_compare
|
||||
|
||||
@@ -8,10 +8,10 @@ from odoo.addons.phone_validation.tools import phone_validation
|
||||
class PhoneValidationMixin(models.AbstractModel):
|
||||
_name = 'phone.validation.mixin'
|
||||
|
||||
def _phone_get_country_code(self):
|
||||
def _phone_get_country(self):
|
||||
if 'country_id' in self:
|
||||
return self.country_id.code
|
||||
return self.env.user.company_id.country_id.code
|
||||
return self.country_id
|
||||
return self.env.user.company_id.country_id
|
||||
|
||||
def _phone_get_always_international(self):
|
||||
if 'company_id' in self:
|
||||
@@ -19,11 +19,12 @@ class PhoneValidationMixin(models.AbstractModel):
|
||||
return self.env.user.company_id.phone_international_format
|
||||
|
||||
def phone_format(self, number, country=None, company=None):
|
||||
country_code = country.code if country else self._phone_get_country_code()
|
||||
country = country or self._phone_get_country()
|
||||
always_international = company.phone_international_format if company else self._phone_get_always_international()
|
||||
|
||||
return phone_validation.phone_format(
|
||||
number, country_code if country_code else None,
|
||||
number,
|
||||
country.code if country else None,
|
||||
country.phone_code if country else None,
|
||||
always_international=always_international,
|
||||
raise_exception=True
|
||||
)
|
||||
|
||||
@@ -9,7 +9,7 @@ class ResCompany(models.Model):
|
||||
|
||||
phone_international_format = fields.Boolean(
|
||||
string="Enforce International Format", default=False,
|
||||
help="Always encore phone numbers using international format. Otherwise"
|
||||
help="Always encode phone numbers using international format. Otherwise"
|
||||
"numbers coming from the company's country are nationaly formatted."
|
||||
"International numbers are always using international format."
|
||||
)
|
||||
|
||||
@@ -26,7 +26,15 @@ try:
|
||||
|
||||
return phone_nbr
|
||||
|
||||
def phone_format(number, country_code, always_international=True, raise_exception=True):
|
||||
def phone_format(number, country_code, country_phone_code, always_international=True, raise_exception=True):
|
||||
""" Format the given phone number according to the localisation and international options.
|
||||
:param number: number to convert
|
||||
:param country_code: the ISO country code in two chars
|
||||
:type country_code: str
|
||||
:param country_phone_code: country dial in codes, defined by the ITU-T (Ex: 32 for Belgium)
|
||||
:type country_phone_code: int
|
||||
:rtype: str
|
||||
"""
|
||||
try:
|
||||
phone_nbr = phone_parse(number, country_code)
|
||||
except (phonenumbers.phonenumberutil.NumberParseException, UserError) as e:
|
||||
@@ -35,8 +43,7 @@ try:
|
||||
else:
|
||||
_logger.warning(_('Unable to format %s:\n%s') % number, e)
|
||||
return number
|
||||
|
||||
if always_international and phone_nbr.country_code != country_code:
|
||||
if always_international and phone_nbr.country_code != country_phone_code:
|
||||
phone_fmt = phonenumbers.PhoneNumberFormat.INTERNATIONAL
|
||||
else:
|
||||
phone_fmt = phonenumbers.PhoneNumberFormat.NATIONAL
|
||||
@@ -48,7 +55,7 @@ except ImportError:
|
||||
def phone_parse(number, country_code):
|
||||
return False
|
||||
|
||||
def phone_format(number, country_code, always_international=True, raise_exception=True):
|
||||
def phone_format(number, country_code, country_phone_code, always_international=True, raise_exception=True):
|
||||
global _phonenumbers_lib_warning
|
||||
if not _phonenumbers_lib_warning:
|
||||
_logger.warning(
|
||||
|
||||
@@ -886,7 +886,8 @@ class PosOrderLine(models.Model):
|
||||
fiscal_position_id = line.order_id.fiscal_position_id
|
||||
if fiscal_position_id:
|
||||
taxes = fiscal_position_id.map_tax(taxes, line.product_id, line.order_id.partner_id)
|
||||
price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
|
||||
price = self.env['account.tax']._fix_tax_included_price(
|
||||
line.price_unit * (1 - (line.discount or 0.0) / 100.0), line.product_id.taxes_id, taxes)
|
||||
line.price_subtotal = line.price_subtotal_incl = price * line.qty
|
||||
if taxes:
|
||||
taxes = taxes.compute_all(price, currency, line.qty, product=line.product_id, partner=line.order_id.partner_id or False)
|
||||
|
||||
@@ -2352,7 +2352,6 @@ td {
|
||||
}
|
||||
.pos .popup-password .popup-input {
|
||||
width: 70%;
|
||||
-webkit-text-security: disc;
|
||||
}
|
||||
|
||||
.pos .popup .body ul,
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
odoo.define('point_of_sale.fieldtexthtml', function (require) {
|
||||
'use strict';
|
||||
|
||||
var FieldTextHtml = require('web_editor.backend').FieldTextHtml;
|
||||
|
||||
FieldTextHtml.include({
|
||||
// avoid '414 Request-URI Too Large' errors to
|
||||
// /point_of_sale/field/customer_facing_display_template by
|
||||
// filtering out biggest fields
|
||||
getDatarecord: function () {
|
||||
var datarecord = this._super();
|
||||
if (this.model === 'pos.config') {
|
||||
datarecord = _.omit(datarecord, function (val, key) {
|
||||
return _.isObject(val) || key === 'customer_facing_display_html';
|
||||
});
|
||||
}
|
||||
return datarecord;
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -308,6 +308,11 @@ var PasswordPopupWidget = NumberPopupWidget.extend({
|
||||
this._super();
|
||||
this.$('.popup').addClass('popup-password');
|
||||
},
|
||||
click_numpad: function(event){
|
||||
this._super.apply(this, arguments);
|
||||
var $value = this.$('.value');
|
||||
$value.text($value.text().replace(/./g, '•'));
|
||||
},
|
||||
});
|
||||
gui.define_popup({name:'password', widget: PasswordPopupWidget});
|
||||
|
||||
|
||||
@@ -179,22 +179,23 @@ class TestPointOfSaleFlow(TestPointOfSaleCommon):
|
||||
'price_unit': 450,
|
||||
'discount': 0.0,
|
||||
'qty': 2.0,
|
||||
'tax_ids': [(6, 0, self.product3.taxes_id.ids)],
|
||||
}), (0, 0, {
|
||||
'name': "OL/0002",
|
||||
'product_id': self.product4.id,
|
||||
'price_unit': 300,
|
||||
'discount': 0.0,
|
||||
'qty': 3.0,
|
||||
'tax_ids': [(6, 0, self.product4.taxes_id.ids)],
|
||||
})]
|
||||
})
|
||||
|
||||
# I click on the "Make Payment" wizard to pay the PoS order with the total amount (2*450 + 3*300 = 1800)
|
||||
context_make_payment = {
|
||||
"active_ids": [self.pos_order_pos1.id],
|
||||
"active_id": self.pos_order_pos1.id
|
||||
}
|
||||
self.pos_make_payment_2 = self.PosMakePayment.with_context(context_make_payment).create({
|
||||
'amount': 1800
|
||||
'amount': 1845
|
||||
})
|
||||
|
||||
# I click on the validate button to register the payment.
|
||||
@@ -241,22 +242,23 @@ class TestPointOfSaleFlow(TestPointOfSaleCommon):
|
||||
'price_unit': 450,
|
||||
'discount': 0.0,
|
||||
'qty': (-2.0),
|
||||
'tax_ids': [(6, 0, self.product3.taxes_id.ids)],
|
||||
}), (0, 0, {
|
||||
'name': "OL/0004",
|
||||
'product_id': self.product4.id,
|
||||
'price_unit': 300,
|
||||
'discount': 0.0,
|
||||
'qty': (-3.0),
|
||||
'tax_ids': [(6, 0, self.product4.taxes_id.ids)],
|
||||
})]
|
||||
})
|
||||
|
||||
# I click on the "Make Payment" wizard to pay the PoS order with the total amount (-2*450 + -3*300 = -1800)
|
||||
context_make_payment = {
|
||||
"active_ids": [self.pos_order_pos2.id],
|
||||
"active_id": self.pos_order_pos2.id
|
||||
}
|
||||
self.pos_make_payment_3 = self.PosMakePayment.with_context(context_make_payment).create({
|
||||
'amount': (-1800)
|
||||
'amount': (-1845)
|
||||
})
|
||||
|
||||
# I click on the validate button to register the payment.
|
||||
@@ -303,22 +305,23 @@ class TestPointOfSaleFlow(TestPointOfSaleCommon):
|
||||
'price_unit': 450,
|
||||
'discount': 0.0,
|
||||
'qty': (-2.0),
|
||||
'tax_ids': [(6, 0, self.product3.taxes_id.ids)],
|
||||
}), (0, 0, {
|
||||
'name': "OL/0006",
|
||||
'product_id': self.product4.id,
|
||||
'price_unit': 300,
|
||||
'discount': 0.0,
|
||||
'qty': 3.0,
|
||||
'tax_ids': [(6, 0, self.product4.taxes_id.ids)],
|
||||
})]
|
||||
})
|
||||
|
||||
# I click on the "Make Payment" wizard to pay the PoS order with the total amount (-2*450 + 3*300 = 0)
|
||||
context_make_payment = {
|
||||
"active_ids": [self.pos_order_pos3.id],
|
||||
"active_id": self.pos_order_pos3.id
|
||||
}
|
||||
self.pos_make_payment_4 = self.PosMakePayment.with_context(context_make_payment).create({
|
||||
'amount': 0
|
||||
'amount': 45,
|
||||
})
|
||||
|
||||
# I click on the validate button to register the payment.
|
||||
|
||||
@@ -72,6 +72,12 @@
|
||||
</script>
|
||||
</template>
|
||||
|
||||
<template id="pos_editor_fieldtexthtml_assets" inherit_id="web.assets_backend">
|
||||
<xpath expr="." position="inside">
|
||||
<script type="text/javascript" src="/point_of_sale/static/src/js/field_text_html.js"/>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<template id="pos_editor_assets" inherit_id="web_editor.assets_editor">
|
||||
<xpath expr="//script[last()]" position="after">
|
||||
<script type="text/javascript" src="/point_of_sale/static/src/js/pos.web_editor.js"/>
|
||||
|
||||
@@ -57,6 +57,10 @@ class ProductCategory(models.Model):
|
||||
raise ValidationError(_('Error ! You cannot create recursive categories.'))
|
||||
return True
|
||||
|
||||
@api.model
|
||||
def name_create(self, name):
|
||||
return self.create({'name': name}).name_get()[0]
|
||||
|
||||
|
||||
class ProductPriceHistory(models.Model):
|
||||
""" Keep track of the ``product.template`` standard prices as they are changed. """
|
||||
|
||||
@@ -90,3 +90,13 @@ class TestProductCommon(common.SavepointCase):
|
||||
'name': 'House',
|
||||
'uom_id': cls.uom_unit.id,
|
||||
'uom_po_id': cls.uom_unit.id})
|
||||
|
||||
cls.product_9 = Product.create({
|
||||
'name': 'Paper',
|
||||
'uom_id': cls.uom_unit.id,
|
||||
'uom_po_id': cls.uom_unit.id})
|
||||
|
||||
cls.product_10 = Product.create({
|
||||
'name': 'Stone',
|
||||
'uom_id': cls.uom_unit.id,
|
||||
'uom_po_id': cls.uom_unit.id})
|
||||
|
||||
@@ -61,5 +61,6 @@ class Holidays(models.Model):
|
||||
def action_refuse(self):
|
||||
""" Remove the timesheets linked to the refused holidays """
|
||||
result = super(Holidays, self).action_refuse()
|
||||
self.mapped('timesheet_ids').write({'holiday_id': False})
|
||||
self.mapped('timesheet_ids').unlink()
|
||||
return result
|
||||
|
||||
@@ -72,6 +72,10 @@ class TestTimesheetHolidays(TestTimesheet):
|
||||
holiday.sudo().action_validate()
|
||||
self.assertEquals(len(holiday.timesheet_ids), number_of_days, 'Number of generated timesheets should be the same as the leave duration (1 per day)')
|
||||
|
||||
# manager refuse the leave
|
||||
holiday.sudo().action_refuse()
|
||||
self.assertEquals(len(holiday.timesheet_ids), 0, 'Number of linked timesheets should be zero, since the leave is refused.')
|
||||
|
||||
def test_validate_without_timesheet(self):
|
||||
# employee creates a leave request
|
||||
number_of_days = (self.leave_end_datetime - self.leave_start_datetime).days
|
||||
|
||||
@@ -143,7 +143,7 @@ class PurchaseOrder(models.Model):
|
||||
picking_ids = fields.Many2many('stock.picking', compute='_compute_picking', string='Receptions', copy=False, store=True)
|
||||
|
||||
# There is no inverse function on purpose since the date may be different on each line
|
||||
date_planned = fields.Datetime(string='Scheduled Date', compute='_compute_date_planned', store=True, index=True, oldname='minimum_planned_date')
|
||||
date_planned = fields.Datetime(string='Scheduled Date', compute='_compute_date_planned', store=True, index=True)
|
||||
|
||||
amount_untaxed = fields.Monetary(string='Untaxed Amount', store=True, readonly=True, compute='_amount_all', track_visibility='always')
|
||||
amount_tax = fields.Monetary(string='Taxes', store=True, readonly=True, compute='_amount_all')
|
||||
@@ -670,7 +670,7 @@ class PurchaseOrderLine(models.Model):
|
||||
}
|
||||
# Fullfill all related procurements with this po line
|
||||
diff_quantity = self.product_qty - qty
|
||||
for procurement in self.procurement_ids:
|
||||
for procurement in self.procurement_ids.filtered(lambda p: p.state != 'cancel'):
|
||||
# If the procurement has some moves already, we should deduct their quantity
|
||||
sum_existing_moves = sum(x.product_qty for x in procurement.move_ids if x.state != 'cancel')
|
||||
existing_proc_qty = procurement.product_id.uom_id._compute_quantity(sum_existing_moves, procurement.product_uom)
|
||||
|
||||
@@ -33,7 +33,14 @@ class PurchaseRequisition(models.Model):
|
||||
_order = "id desc"
|
||||
|
||||
def _get_picking_in(self):
|
||||
return self.env.ref('stock.picking_type_in')
|
||||
pick_in = self.env.ref('stock.picking_type_in')
|
||||
if not pick_in:
|
||||
company = self.env['res.company']._company_default_get('purchase.requisition')
|
||||
pick_in = self.env['stock.picking.type'].search(
|
||||
[('warehouse_id.company_id', '=', company.id), ('code', '=', 'incoming')],
|
||||
limit=1,
|
||||
)
|
||||
return pick_in
|
||||
|
||||
def _get_type_id(self):
|
||||
return self.env['purchase.requisition.type'].search([], limit=1)
|
||||
|
||||
@@ -35,9 +35,6 @@ class Rating(http.Controller):
|
||||
return request.not_found()
|
||||
record_sudo = request.env[rating.res_model].sudo().browse(rating.res_id)
|
||||
record_sudo.rating_apply(rate, token=token, feedback=kwargs.get('feedback'))
|
||||
# redirect to the form view if logged person
|
||||
if request.session.uid:
|
||||
return werkzeug.utils.redirect('/web#model=%s&id=%s&view_type=form' % (record_sudo._name, record_sudo.id))
|
||||
lang = rating.partner_id.lang or 'en_US'
|
||||
return request.env['ir.ui.view'].with_context(lang=lang).render_template('rating.rating_external_page_view', {
|
||||
'web_base_url': request.env['ir.config_parameter'].sudo().get_param('web.base.url'),
|
||||
|
||||
@@ -307,6 +307,35 @@ class ResourceWorkingHours(TestResourceCommon):
|
||||
to_naive_utc(Datetime.from_string('2013-02-12 09:25:00'), self.env.user))
|
||||
self.assertEqual(res, to_naive_utc(Datetime.from_string('2013-02-26 11:25:00'), self.env.user))
|
||||
|
||||
def test_calendar_hours_scheduling_timezone_2(self):
|
||||
# Call schedule_hours for a user in Autralia, Sydney (GMT+10)
|
||||
# Two cases:
|
||||
# - start at 2013-02-15 08:00:00 => 2013-02-14 21:00:00 in UTC
|
||||
# - start at 2013-02-15 11:00:00 => 2013-02-15 00:00:00 in UTC
|
||||
self.env.user.tz = 'Australia/Sydney'
|
||||
self.env['resource.calendar.attendance'].create({
|
||||
'name': 'Day3 - 1',
|
||||
'dayofweek': '3',
|
||||
'hour_from': 8,
|
||||
'hour_to': 12,
|
||||
'calendar_id': self.calendar.id,
|
||||
})
|
||||
self.env['resource.calendar.attendance'].create({
|
||||
'name': 'Day3 - 2',
|
||||
'dayofweek': '3',
|
||||
'hour_from': 13,
|
||||
'hour_to': 17,
|
||||
'calendar_id': self.calendar.id,
|
||||
})
|
||||
hours = 1.0/60.0
|
||||
for test_date in ['2013-02-15 08:00:00', '2013-02-15 11:00:00']:
|
||||
start_dt = Datetime.from_string(test_date)
|
||||
start_dt_utc = to_naive_utc(start_dt, self.env.user)
|
||||
res = self.calendar._schedule_hours(hours, start_dt_utc)
|
||||
self.assertEqual(
|
||||
[(start_dt_utc, start_dt_utc.replace(minute=1))], res,
|
||||
'resource_calendar: wrong schedule_hours computation')
|
||||
|
||||
def test_calendar_hours_scheduling_forward_leaves_resource(self):
|
||||
res = self.calendar._schedule_hours(
|
||||
40, day_dt=Datetime.from_string('2013-02-12 09:00:00'),
|
||||
|
||||
@@ -127,7 +127,7 @@ class SaleOrder(models.Model):
|
||||
('cancel', 'Cancelled'),
|
||||
], string='Status', readonly=True, copy=False, index=True, track_visibility='onchange', default='draft')
|
||||
date_order = fields.Datetime(string='Order Date', required=True, readonly=True, index=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, copy=False, default=fields.Datetime.now)
|
||||
validity_date = fields.Date(string='Expiration Date', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]},
|
||||
validity_date = fields.Date(string='Expiration Date', readonly=True, copy=False, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]},
|
||||
help="Manually set the expiration date of your quotation (offer), or it will set the date automatically based on the template if online quotation is installed.")
|
||||
create_date = fields.Datetime(string='Creation Date', readonly=True, index=True, help="Date on which sales order is created.")
|
||||
confirmation_date = fields.Datetime(string='Confirmation Date', readonly=True, index=True, help="Date on which the sales order is confirmed.", oldname="date_confirm")
|
||||
|
||||
@@ -55,7 +55,7 @@ class SaleOrderLine(models.Model):
|
||||
|
||||
return super(SaleOrderLine, self).create(vals)
|
||||
|
||||
@api.depends('product_id', 'purchase_price', 'product_uom_qty', 'price_unit')
|
||||
@api.depends('product_id', 'purchase_price', 'product_uom_qty', 'price_unit', 'price_subtotal')
|
||||
def _product_margin(self):
|
||||
for line in self:
|
||||
currency = line.order_id.pricelist_id.currency_id
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//label" position="before">
|
||||
<div class="oe_button_box" name="button_box" groups="base.group_user">
|
||||
<button name="%(sale_timesheet.action_timesheet_from_invoice)d" type="action" class="oe_stat_button" icon="fa-calendar">
|
||||
<button name="%(sale_timesheet.action_timesheet_from_invoice)d" type="action" class="oe_stat_button" icon="fa-calendar" attrs="{'invisible':[('timesheet_count','=', 0)]}">
|
||||
<field name="timesheet_count" widget="statinfo" string="Timesheets"/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -419,9 +419,6 @@ class ProductTemplate(models.Model):
|
||||
relation="stock.location.route", string="Category Routes",
|
||||
related='categ_id.total_route_ids')
|
||||
|
||||
def _is_cost_method_standard(self):
|
||||
return True
|
||||
|
||||
def _compute_quantities(self):
|
||||
res = self._compute_quantities_dict()
|
||||
for template in self:
|
||||
|
||||
@@ -110,10 +110,11 @@ class PackOperation(models.Model):
|
||||
qty -= record.qty
|
||||
self.remaining_qty = float_round(qty, precision_rounding=self.product_id.uom_id.rounding)
|
||||
|
||||
@api.one
|
||||
@api.multi
|
||||
def _compute_location_description(self):
|
||||
self.from_loc = '%s%s' % (self.location_id.name, self.product_id and self.package_id.name or '')
|
||||
self.to_loc = '%s%s' % (self.location_dest_id.name, self.result_package_id.name or '')
|
||||
for operation, operation_sudo in zip(self, self.sudo()):
|
||||
operation.from_loc = '%s%s' % (operation_sudo.location_id.name, operation.product_id and operation_sudo.package_id.name or '')
|
||||
operation.to_loc = '%s%s' % (operation_sudo.location_dest_id.name, operation_sudo.result_package_id.name or '')
|
||||
|
||||
@api.one
|
||||
def _compute_lots_visible(self):
|
||||
|
||||
@@ -328,11 +328,11 @@ class Picking(models.Model):
|
||||
.filtered(lambda move: move.state not in ['cancel', 'done'])\
|
||||
.sorted(key=lambda move: (move.state == 'assigned' and 2) or (move.state == 'waiting' and 1) or 0)
|
||||
if self.move_type == 'one':
|
||||
self.state = moves_todo[0].state
|
||||
self.state = moves_todo[0].state or 'draft'
|
||||
elif moves_todo[0].state != 'assigned' and any(x.partially_available or x.state == 'assigned' for x in moves_todo):
|
||||
self.state = 'partially_available'
|
||||
else:
|
||||
self.state = moves_todo[-1].state
|
||||
self.state = moves_todo[-1].state or 'draft'
|
||||
|
||||
@api.one
|
||||
@api.depends('move_lines.priority')
|
||||
@@ -610,9 +610,13 @@ class Picking(models.Model):
|
||||
forced_qties[move.product_id] = forced_qty
|
||||
for vals in picking._prepare_pack_ops(picking_quants, forced_qties):
|
||||
vals['fresh_record'] = False
|
||||
PackOperation.create(vals)
|
||||
PackOperation |= PackOperation.create(vals)
|
||||
# recompute the remaining quantities all at once
|
||||
self.do_recompute_remaining_quantities()
|
||||
for pack in PackOperation:
|
||||
pack.ordered_qty = sum(
|
||||
pack.mapped('linked_move_operation_ids').mapped('move_id').filtered(lambda r: r.state != 'cancel').mapped('ordered_qty')
|
||||
)
|
||||
self.write({'recompute_pack_op': False})
|
||||
|
||||
@api.multi
|
||||
@@ -782,6 +786,8 @@ class Picking(models.Model):
|
||||
@api.multi
|
||||
def do_new_transfer(self):
|
||||
for pick in self:
|
||||
if pick.state == 'done':
|
||||
raise UserError(_('The pick is already validated'))
|
||||
pack_operations_delete = self.env['stock.pack.operation']
|
||||
if not pick.move_lines and not pick.pack_operation_ids:
|
||||
raise UserError(_('Please create some Initial Demand or Mark as Todo and create some Operations. '))
|
||||
|
||||
@@ -1576,7 +1576,6 @@ class TestStockFlow(TestStockCommon):
|
||||
'picking_id': picking_out.id,
|
||||
'location_id': self.stock_location,
|
||||
'location_dest_id': self.customer_location})
|
||||
|
||||
# validate this delivery order, it should be in the waiting state
|
||||
picking_out.action_assign()
|
||||
self.assertEquals(picking_out.state, "confirmed")
|
||||
|
||||
@@ -201,6 +201,7 @@
|
||||
<header>
|
||||
<field name="state" widget="statusbar"/>
|
||||
</header>
|
||||
<field name="scrapped" invisible="1"/>
|
||||
<group>
|
||||
<group string="#Products">
|
||||
<field name="product_id"/>
|
||||
|
||||
@@ -19,11 +19,10 @@ class StockBackorderConfirmation(models.TransientModel):
|
||||
|
||||
@api.one
|
||||
def _process(self, cancel_backorder=False):
|
||||
for pack in self.pick_id.pack_operation_ids:
|
||||
if pack.qty_done > 0:
|
||||
pack.product_qty = pack.qty_done
|
||||
else:
|
||||
pack.unlink()
|
||||
operations_to_delete = self.pick_id.pack_operation_ids.filtered(lambda o: o.qty_done <= 0)
|
||||
for pack in self.pick_id.pack_operation_ids - operations_to_delete:
|
||||
pack.product_qty = pack.qty_done
|
||||
operations_to_delete.unlink()
|
||||
self.pick_id.do_transfer()
|
||||
if cancel_backorder:
|
||||
backorder_pick = self.env['stock.picking'].search([('backorder_id', '=', self.pick_id.id)])
|
||||
|
||||
@@ -51,9 +51,6 @@ class ProductTemplate(models.Model):
|
||||
def _compute_cost_method(self):
|
||||
self.cost_method = self.property_cost_method or self.categ_id.property_cost_method
|
||||
|
||||
def _is_cost_method_standard(self):
|
||||
return self.property_cost_method == 'standard'
|
||||
|
||||
@api.one
|
||||
def _set_cost_method(self):
|
||||
return self.write({'property_cost_method': self.cost_method})
|
||||
|
||||
@@ -41,7 +41,7 @@ class StockHistory(models.Model):
|
||||
self._cr.execute("""SELECT DISTINCT ON (product_id, company_id) product_id, company_id, cost
|
||||
FROM product_price_history
|
||||
WHERE product_id in %s AND datetime <= %s
|
||||
ORDER BY product_id, company_id, datetime DESC""", (tuple(not_real_cost_method_products.ids), date))
|
||||
ORDER BY product_id, company_id, datetime DESC, id DESC""", (tuple(not_real_cost_method_products.ids), date))
|
||||
for history in self._cr.dictfetchall():
|
||||
histories_dict[(history['product_id'], history['company_id'])] = history['cost']
|
||||
|
||||
|
||||
@@ -155,6 +155,7 @@ ExtendedSearchProposition.Field = Widget.extend({
|
||||
|
||||
ExtendedSearchProposition.Char = ExtendedSearchProposition.Field.extend({
|
||||
tagName: 'input',
|
||||
className: 'o_input',
|
||||
attributes: {
|
||||
type: 'text'
|
||||
},
|
||||
@@ -186,7 +187,7 @@ ExtendedSearchProposition.DateTime = ExtendedSearchProposition.Field.extend({
|
||||
{value: "∄", text: _lt("is not set")}
|
||||
],
|
||||
get_value: function (index) {
|
||||
return this["datewidget_" + (index || 0)].get_value();
|
||||
return this["datewidget_" + (index || 0)].getValue();
|
||||
},
|
||||
get_domain: function (field, operator) {
|
||||
switch (operator.value) {
|
||||
@@ -251,6 +252,7 @@ ExtendedSearchProposition.Date = ExtendedSearchProposition.DateTime.extend({
|
||||
|
||||
ExtendedSearchProposition.Integer = ExtendedSearchProposition.Field.extend({
|
||||
tagName: 'input',
|
||||
className: 'o_input',
|
||||
attributes: {
|
||||
type: 'number',
|
||||
value: '0',
|
||||
|
||||
@@ -307,7 +307,7 @@ var ViewManager = Widget.extend(ControlPanelMixin, {
|
||||
self.trigger('switch_mode', view_type, view_options);
|
||||
}).fail(function(e) {
|
||||
if (!(e && e.code === 200 && e.data.exception_type)) {
|
||||
self.do_warn(_t("Error"), view.controller.display_name + _t(" view couldn't be loaded"));
|
||||
self.do_warn(_t("Error"), view.label + _t(" view couldn't be loaded"));
|
||||
}
|
||||
// Restore internal state
|
||||
self.active_view = old_view;
|
||||
|
||||
@@ -435,6 +435,22 @@ datetime.datetime = py.type('datetime', null, {
|
||||
this.second,
|
||||
this.microsecond / 1000);
|
||||
},
|
||||
__add__: function (other) {
|
||||
if (!py.PY_isInstance(other, datetime.timedelta)) {
|
||||
return py.NotImplemented;
|
||||
}
|
||||
var s = tmxxx(this.year, this.month, this.day + other.days, this.hour, this.minute, this.second + other.seconds);
|
||||
return datetime.datetime.fromJSON(s.year, s.month, s.day, s.hour, s.minute, s.second);
|
||||
},
|
||||
__sub__: function (other) {
|
||||
if (py.PY_isInstance(other, datetime.timedelta)) {
|
||||
return py.PY_add(this, py.PY_negative(other));
|
||||
}
|
||||
return py.NotImplemented;
|
||||
},
|
||||
fromJSON: function (year, month, day, hour, minute, second) {
|
||||
return py.PY_call(datetime.datetime, [year, month, day, hour, minute, second]);
|
||||
},
|
||||
});
|
||||
|
||||
datetime.date = py.type('date', null, {
|
||||
|
||||
@@ -142,6 +142,21 @@ var AbstractField = Widget.extend({
|
||||
// this is the last value that was set by the user, unparsed. This is
|
||||
// used to avoid setting the value twice in a row with the exact value.
|
||||
this.lastSetValue = undefined;
|
||||
|
||||
// formatType is used to determine which format (and parse) functions
|
||||
// to call to format the field's value to insert into the DOM (typically
|
||||
// put into a span or an input), and to parse the value from the input
|
||||
// to send it to the server. These functions are chosen according to
|
||||
// the 'widget' attrs if is is given, and if it is a valid key, with a
|
||||
// fallback on the field type, ensuring that the value is formatted and
|
||||
// displayed according to the choosen widget, if any.
|
||||
this.formatType = this.attrs.widget in field_utils.format ?
|
||||
this.attrs.widget :
|
||||
this.field.type;
|
||||
// formatOptions (resp. parseOptions) is a dict of options passed to
|
||||
// calls to the format (resp. parse) function.
|
||||
this.formatOptions = {};
|
||||
this.parseOptions = {};
|
||||
},
|
||||
/**
|
||||
* When a field widget is appended to the DOM, its start method is called,
|
||||
@@ -266,15 +281,15 @@ var AbstractField = Widget.extend({
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* convert the value from the field to a string representation
|
||||
* Converts the value from the field to a string representation.
|
||||
*
|
||||
* @private
|
||||
* @param {any} value (from the field type)
|
||||
* @returns {string}
|
||||
*/
|
||||
_formatValue: function (value) {
|
||||
var options = _.extend({}, this.nodeOptions, { data: this.recordData });
|
||||
return field_utils.format[this.field.type](value, this.field, options);
|
||||
var options = _.extend({}, this.nodeOptions, { data: this.recordData }, this.formatOptions);
|
||||
return field_utils.format[this.formatType](value, this.field, options);
|
||||
},
|
||||
/**
|
||||
* This method check if a value is the same as the current value of the
|
||||
@@ -291,15 +306,14 @@ var AbstractField = Widget.extend({
|
||||
return this.value === value;
|
||||
},
|
||||
/**
|
||||
* convert a string representation to a valid value, depending on the field
|
||||
* type.
|
||||
* Converts a string representation to a valid value.
|
||||
*
|
||||
* @private
|
||||
* @param {string} value
|
||||
* @returns {any}
|
||||
*/
|
||||
_parseValue: function (value) {
|
||||
return field_utils.parse[this.field.type](value, this.field);
|
||||
return field_utils.parse[this.formatType](value, this.field, this.parseOptions);
|
||||
},
|
||||
/**
|
||||
* main rendering function. Override this if your widget has the same render
|
||||
@@ -426,22 +440,18 @@ var AbstractField = Widget.extend({
|
||||
this.trigger_up('navigation_move', {direction: 'cancel'});
|
||||
break;
|
||||
case $.ui.keyCode.UP:
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this.trigger_up('navigation_move', {direction: 'up'});
|
||||
break;
|
||||
case $.ui.keyCode.RIGHT:
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this.trigger_up('navigation_move', {direction: 'right'});
|
||||
break;
|
||||
case $.ui.keyCode.DOWN:
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this.trigger_up('navigation_move', {direction: 'down'});
|
||||
break;
|
||||
case $.ui.keyCode.LEFT:
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this.trigger_up('navigation_move', {direction: 'left'});
|
||||
break;
|
||||
|
||||
@@ -285,11 +285,15 @@ var InputField = DebouncedField.extend({
|
||||
*/
|
||||
_onNavigationMove: function (ev) {
|
||||
this._super.apply(this, arguments);
|
||||
var input = this.$input[0];
|
||||
var selecting = (input.selectionEnd !== input.selectionStart);
|
||||
if ((ev.data.direction === "left" && (selecting || input.selectionStart !== 0))
|
||||
|| (ev.data.direction === "right" && (selecting || input.selectionStart !== input.value.length))) {
|
||||
ev.stopPropagation();
|
||||
|
||||
// the following code only makes sense in edit mode, with an input
|
||||
if (this.mode === 'edit') {
|
||||
var input = this.$input[0];
|
||||
var selecting = (input.selectionEnd !== input.selectionStart);
|
||||
if ((ev.data.direction === "left" && (selecting || input.selectionStart !== 0))
|
||||
|| (ev.data.direction === "right" && (selecting || input.selectionStart !== input.value.length))) {
|
||||
ev.stopPropagation();
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -321,6 +325,14 @@ var FieldDate = InputField.extend({
|
||||
tagName: "span",
|
||||
supportedFieldTypes: ['date'],
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
init: function () {
|
||||
this._super.apply(this, arguments);
|
||||
// use the session timezone when formatting dates
|
||||
this.formatOptions.timezone = true;
|
||||
},
|
||||
/**
|
||||
* In edit mode, instantiates a DateWidget datepicker and listen to changes.
|
||||
*
|
||||
@@ -396,17 +408,6 @@ var FieldDate = InputField.extend({
|
||||
this.datewidget.setValue(this.value);
|
||||
this.$input = this.datewidget.$input;
|
||||
},
|
||||
|
||||
/**
|
||||
* FieldDateTime overrides _formatValue to use the session timezone
|
||||
*
|
||||
* @override
|
||||
* @private
|
||||
* @param {Moment} value
|
||||
*/
|
||||
_formatValue: function (value) {
|
||||
return field_utils.format.date(value, {timezone: true});
|
||||
},
|
||||
});
|
||||
|
||||
var FieldDateTime = FieldDate.extend({
|
||||
@@ -448,17 +449,6 @@ var FieldDateTime = FieldDate.extend({
|
||||
this.datewidget.setValue(value);
|
||||
this.$input = this.datewidget.$input;
|
||||
},
|
||||
|
||||
/**
|
||||
* FieldDateTime overrides _formatValue to use the session timezone
|
||||
*
|
||||
* @override
|
||||
* @private
|
||||
* @param {Moment} value
|
||||
*/
|
||||
_formatValue: function (value) {
|
||||
return field_utils.format.datetime(value, {timezone: true});
|
||||
},
|
||||
});
|
||||
|
||||
var FieldMonetary = InputField.extend({
|
||||
@@ -490,6 +480,13 @@ var FieldMonetary = InputField.extend({
|
||||
this.tagName = 'div';
|
||||
this.className += ' o_input';
|
||||
}
|
||||
|
||||
// use the formatFloat function in edit
|
||||
if (this.mode === 'edit') {
|
||||
this.formatType = 'float';
|
||||
}
|
||||
this.formatOptions.currency = this.currency;
|
||||
this.formatOptions.digits = [16, 2];
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
@@ -562,22 +559,6 @@ var FieldMonetary = InputField.extend({
|
||||
var currencyID = this.record.data[currencyField] && this.record.data[currencyField].res_id;
|
||||
this.currency = session.get_currency(currencyID);
|
||||
},
|
||||
/**
|
||||
* FieldMonetary overrides _formatValue to use the format monetary method
|
||||
* in readonly.
|
||||
*
|
||||
* @override
|
||||
* @private
|
||||
* @param {float} value
|
||||
* @returns {string}
|
||||
*/
|
||||
_formatValue: function (value) {
|
||||
var format = field_utils.format[this.mode === 'edit' ? "float" : "monetary"];
|
||||
return format(value, this.field, {
|
||||
digits: [16, 2],
|
||||
currency: this.currency,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
var FieldBoolean = AbstractField.extend({
|
||||
@@ -686,6 +667,11 @@ var FieldBoolean = AbstractField.extend({
|
||||
this.$input.prop('checked', !this.value);
|
||||
this._setValue(!this.value);
|
||||
return;
|
||||
case $.ui.keyCode.UP:
|
||||
case $.ui.keyCode.RIGHT:
|
||||
case $.ui.keyCode.DOWN:
|
||||
case $.ui.keyCode.LEFT:
|
||||
ev.preventDefault();
|
||||
}
|
||||
this._super.apply(this, arguments);
|
||||
},
|
||||
@@ -734,7 +720,7 @@ var FieldInteger = InputField.extend({
|
||||
}
|
||||
return value;
|
||||
}
|
||||
return field_utils.format.integer(value, this.field);
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -771,31 +757,11 @@ var FieldFloat = InputField.extend({
|
||||
});
|
||||
|
||||
var FieldFloatTime = FieldFloat.extend({
|
||||
supportedFieldTypes: ['float'],
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Format the float time value into a human-readable time format.
|
||||
*
|
||||
* @override
|
||||
* @private
|
||||
*/
|
||||
_formatValue: function (value) {
|
||||
return field_utils.format.float_time(value);
|
||||
},
|
||||
|
||||
/**
|
||||
* Parse the human-readable formatted time value into a float.
|
||||
*
|
||||
* @override
|
||||
* @private
|
||||
*/
|
||||
_parseValue: function (value) {
|
||||
return field_utils.parse.float_time(value);
|
||||
},
|
||||
// this is not strictly necessary, as for this widget to be used, the 'widget'
|
||||
// attrs must be set to 'float_time', so the formatType is automatically
|
||||
// 'float_time', but for the sake of clarity, we explicitely define a
|
||||
// FieldFloatTime widget with formatType = 'float_time'.
|
||||
formatType: 'float_time',
|
||||
});
|
||||
|
||||
var FieldText = InputField.extend(TranslatableFieldMixin, {
|
||||
@@ -852,6 +818,19 @@ var FieldEmail = InputField.extend({
|
||||
this.tagName = this.mode === 'readonly' ? 'a' : 'input';
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Returns the associated link.
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
getFocusableElement: function () {
|
||||
return this.mode === 'readonly' ? this.$el : this._super.apply(this, arguments);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
@@ -887,6 +866,17 @@ var FieldPhone = FieldEmail.extend({
|
||||
this.tagName = 'span';
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Returns the associated link only if there is one.
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
getFocusableElement: function () {
|
||||
if (this._canCall()) {
|
||||
return this._super.apply(this, arguments);
|
||||
}
|
||||
return $();
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
@@ -940,6 +930,19 @@ var UrlWidget = InputField.extend({
|
||||
this.tagName = this.mode === 'readonly' ? 'a' : 'input';
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Returns the associated link.
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
getFocusableElement: function () {
|
||||
return this.mode === 'readonly' ? this.$el : this._super.apply(this, arguments);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
@@ -1041,7 +1044,13 @@ var AbstractFieldBinary = AbstractField.extend({
|
||||
set_filename: function (value) {
|
||||
var filename = this.attrs.filename;
|
||||
if (filename && filename in this.fields) {
|
||||
this.trigger_up('update_field', { name: filename, value: value });
|
||||
var changes = {};
|
||||
changes[filename] = value;
|
||||
this.trigger_up('field_changed', {
|
||||
dataPointID: this.dataPointID,
|
||||
changes: changes,
|
||||
viewType: this.viewType,
|
||||
});
|
||||
}
|
||||
},
|
||||
on_clear: function () {
|
||||
|
||||
@@ -124,7 +124,7 @@ var FieldMany2One = AbstractField.extend({
|
||||
* @returns {jQuery}
|
||||
*/
|
||||
getFocusableElement: function () {
|
||||
return this.$input || $();
|
||||
return this.mode === 'edit' && this.$input || this.$el;
|
||||
},
|
||||
/**
|
||||
* TODO
|
||||
@@ -205,6 +205,17 @@ var FieldMany2One = AbstractField.extend({
|
||||
_getSearchBlacklist: function () {
|
||||
return [];
|
||||
},
|
||||
/**
|
||||
* Returns the display_name from a string which contains it but was altered
|
||||
* as a result of the show_address option using a horrible hack.
|
||||
*
|
||||
* @private
|
||||
* @param {string} value
|
||||
* @returns {string} display_name without show_address mess
|
||||
*/
|
||||
_getDisplayName: function (value) {
|
||||
return value.split('\n')[0];
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {string} name
|
||||
@@ -244,7 +255,7 @@ var FieldMany2One = AbstractField.extend({
|
||||
// and hope for the best that noone tries to uses this mechanism to do
|
||||
// something else.
|
||||
if (this.nodeOptions.always_reload) {
|
||||
value = value.split('\n')[0];
|
||||
value = this._getDisplayName(value);
|
||||
}
|
||||
this.$input.val(value);
|
||||
if (!this.autocomplete_bound) {
|
||||
@@ -302,7 +313,7 @@ var FieldMany2One = AbstractField.extend({
|
||||
.then(function (result) {
|
||||
// possible selections for the m2o
|
||||
var values = _.map(result, function (x) {
|
||||
x[1] = x[1].split("\n")[0];
|
||||
x[1] = self._getDisplayName(x[1]);
|
||||
return {
|
||||
label: _.str.escapeHTML(x[1].trim()) || data.noDisplayContent,
|
||||
value: x[1],
|
||||
@@ -478,7 +489,7 @@ var FieldMany2One = AbstractField.extend({
|
||||
_onInputKeyup: function () {
|
||||
if (this.$input.val() === "") {
|
||||
this.reinitialize(false);
|
||||
} else if (this.m2o_value !== this.$input.val()) {
|
||||
} else if (this._getDisplayName(this.m2o_value) !== this.$input.val()) {
|
||||
this.floating = true;
|
||||
this._updateExternalButton();
|
||||
}
|
||||
@@ -1585,7 +1596,7 @@ var FormFieldMany2ManyTags = FieldMany2ManyTags.extend({
|
||||
var KanbanFieldMany2ManyTags = FieldMany2ManyTags.extend({
|
||||
_render: function () {
|
||||
var self = this;
|
||||
this.$el.addClass('o_field_many2manytags o_kanban_tags');
|
||||
this.$el.empty().addClass('o_field_many2manytags o_kanban_tags');
|
||||
_.each(this.value.data, function (m2m) {
|
||||
// 10th color is invisible
|
||||
if ('color' in m2m.data && m2m.data.color !== 10) {
|
||||
@@ -1763,6 +1774,7 @@ var FieldSelection = AbstractField.extend({
|
||||
this._super.apply(this, arguments);
|
||||
if (this.field.type === 'many2one') {
|
||||
this.values = this.record.specialData[this.name];
|
||||
this.formatType = 'many2one';
|
||||
} else {
|
||||
this.values = _.reject(this.field.selection, function (v) {
|
||||
return v[0] === false && v[1] === '';
|
||||
|
||||
@@ -6,6 +6,7 @@ odoo.define('web.upgrade_widgets', function (require) {
|
||||
* When checked, an upgrade popup is showed to the user.
|
||||
*/
|
||||
|
||||
var AbstractField = require('web.AbstractField');
|
||||
var basic_fields = require('web.basic_fields');
|
||||
var core = require('web.core');
|
||||
var Dialog = require('web.Dialog');
|
||||
@@ -58,6 +59,7 @@ var AbstractFieldUpgrade = {
|
||||
* Opens the Upgrade dialog.
|
||||
*
|
||||
* @private
|
||||
* @returns {Dialog} the instance of the opened Dialog
|
||||
*/
|
||||
_openDialog: function () {
|
||||
var message = $(QWeb.render('EnterpriseUpgrade'));
|
||||
@@ -75,7 +77,7 @@ var AbstractFieldUpgrade = {
|
||||
},
|
||||
];
|
||||
|
||||
new Dialog(this, {
|
||||
return new Dialog(this, {
|
||||
size: 'medium',
|
||||
buttons: buttons,
|
||||
$content: $('<div>', {
|
||||
@@ -122,7 +124,7 @@ var AbstractFieldUpgrade = {
|
||||
|
||||
var UpgradeBoolean = FieldBoolean.extend(AbstractFieldUpgrade, {
|
||||
supportedFieldTypes: [],
|
||||
events: _.extend({}, FieldBoolean.prototype.events, {
|
||||
events: _.extend({}, AbstractField.prototype.events, {
|
||||
'click input': '_onInputClicked',
|
||||
}),
|
||||
|
||||
|
||||
@@ -423,7 +423,7 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
|
||||
});
|
||||
if (!options.stayInEdit) {
|
||||
saveDef = saveDef.then(function (fieldNames) {
|
||||
var def = fieldNames.length ? self._confirmSave(recordID) : self._setMode('readonly');
|
||||
var def = fieldNames.length ? self._confirmSave(recordID) : self._setMode('readonly', recordID);
|
||||
return def.then(function () {
|
||||
return fieldNames;
|
||||
});
|
||||
@@ -534,6 +534,7 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
|
||||
* @param {function} ev.data.action the function to execute in the mutex
|
||||
*/
|
||||
_onMutexify: function (ev) {
|
||||
ev.stopPropagation(); // prevent other controllers from handling this request
|
||||
this.mutex.exec(ev.data.action);
|
||||
},
|
||||
/**
|
||||
@@ -578,6 +579,7 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
|
||||
* @param {OdooEvent} event
|
||||
*/
|
||||
_onTranslate: function (event) {
|
||||
event.stopPropagation();
|
||||
var record = this.model.get(event.data.id, {raw: true});
|
||||
this._rpc({
|
||||
route: '/web/dataset/call_button',
|
||||
|
||||
@@ -1399,7 +1399,8 @@ var BasicModel = AbstractModel.extend({
|
||||
* @param {string} [optinos.viewType] the type of view for which the record
|
||||
* is fetched (usefull to load the adequate fields), by defaults, uses
|
||||
* record.viewType
|
||||
* @returns {Deferred -> Object} resolves to the record
|
||||
* @returns {Deferred -> Object} resolves to the record or is rejected in
|
||||
* case no id given were valid ids
|
||||
*/
|
||||
_fetchRecord: function (record, options) {
|
||||
var self = this;
|
||||
@@ -1412,6 +1413,9 @@ var BasicModel = AbstractModel.extend({
|
||||
context: _.extend({}, record.context, {bin_size: true}),
|
||||
})
|
||||
.then(function (result) {
|
||||
if (result.length === 0) {
|
||||
return $.Deferred().reject();
|
||||
}
|
||||
result = result[0];
|
||||
record.data = _.extend({}, record.data, result);
|
||||
})
|
||||
@@ -2194,12 +2198,6 @@ var BasicModel = AbstractModel.extend({
|
||||
if (!field || field.name === 'id') {
|
||||
continue;
|
||||
}
|
||||
if (field.type === 'float' ||
|
||||
field.type === 'integer' ||
|
||||
field.type === 'monetary') {
|
||||
context[fieldName] = context[fieldName] || 0;
|
||||
continue;
|
||||
}
|
||||
if (field.type === 'date' || field.type === 'datetime') {
|
||||
if (context[fieldName]) {
|
||||
context[fieldName] = JSON.parse(JSON.stringify(context[fieldName]));
|
||||
@@ -2445,7 +2443,11 @@ var BasicModel = AbstractModel.extend({
|
||||
var fieldName = fieldNames[i];
|
||||
if (!(fieldName in result)) {
|
||||
var field = params.fields[fieldName];
|
||||
if (field.type === 'one2many' || field.type === 'many2many') {
|
||||
if (field.type === 'float' ||
|
||||
field.type === 'integer' ||
|
||||
field.type === 'monetary') {
|
||||
result[fieldName] = 0;
|
||||
} else if (field.type === 'one2many' || field.type === 'many2many') {
|
||||
result[fieldName] = [];
|
||||
} else {
|
||||
result[fieldName] = null;
|
||||
|
||||
@@ -828,6 +828,7 @@ var FormRenderer = BasicRenderer.extend({
|
||||
*/
|
||||
_onNavigationMove: function (ev) {
|
||||
ev.stopPropagation();
|
||||
|
||||
var index;
|
||||
if (ev.data.direction === "next") {
|
||||
index = this.allFieldWidgets[this.state.id].indexOf(ev.data.target);
|
||||
|
||||
@@ -40,8 +40,6 @@ var KanbanColumn = Widget.extend({
|
||||
|
||||
var value = data.value;
|
||||
this.id = data.res_id || value;
|
||||
// todo: handle group_by_m2o (nameget)
|
||||
this.title = value === undefined ? _t('Undefined') : value;
|
||||
this.folded = !data.isOpen;
|
||||
this.has_active_field = 'active' in data.fields;
|
||||
this.size = data.count;
|
||||
@@ -62,6 +60,14 @@ var KanbanColumn = Widget.extend({
|
||||
|
||||
this.record_options = _.clone(recordOptions);
|
||||
|
||||
if (options.grouped_by_m2o) {
|
||||
// For many2one, a false value means that the field is not set.
|
||||
this.title = value ? value : _t('Undefined');
|
||||
} else {
|
||||
// False and 0 might be valid values for these fields.
|
||||
this.title = value === undefined ? _t('Undefined') : value;
|
||||
}
|
||||
|
||||
if (options.group_by_tooltip) {
|
||||
this.tooltipInfo = _.map(options.group_by_tooltip, function (help, field) {
|
||||
return (data.tooltipData && data.tooltipData[field] && "<div>" + help + "<br>" + data.tooltipData[field] + "</div>") || '';
|
||||
|
||||
@@ -153,7 +153,7 @@ var KanbanModel = BasicModel.extend({
|
||||
}).then(function () {
|
||||
// Remove record from its current group
|
||||
var old_group;
|
||||
for (var i = 0; i < parent.count; i++) {
|
||||
for (var i = 0; i < parent.data.length; i++) {
|
||||
old_group = self.localData[parent.data[i]];
|
||||
var index = _.indexOf(old_group.data, recordID);
|
||||
if (index >= 0) {
|
||||
@@ -244,9 +244,14 @@ var KanbanModel = BasicModel.extend({
|
||||
if (groupedByField.type !== 'many2one') {
|
||||
return $.when();
|
||||
}
|
||||
var groupIds = _.map(list.data, function (id) {
|
||||
return self.get(id, {raw: true}).res_id;
|
||||
});
|
||||
var groupIds = _.reduce(list.data, function (groupIds, id) {
|
||||
var res_id = self.get(id, {raw: true}).res_id;
|
||||
// The field on which we are grouping might not be set on all records
|
||||
if (res_id) {
|
||||
groupIds.push(res_id);
|
||||
}
|
||||
return groupIds;
|
||||
}, []);
|
||||
var tooltipFields = [];
|
||||
var groupedByFieldInfo = list.fieldsInfo.kanban[list.groupedBy[0]];
|
||||
if (groupedByFieldInfo && groupedByFieldInfo.options) {
|
||||
|
||||
@@ -271,10 +271,14 @@ var ListRenderer = BasicRenderer.extend({
|
||||
_renderButton: function (record, node) {
|
||||
var $button = $('<button>', {
|
||||
type: 'button',
|
||||
class: 'o_icon_button',
|
||||
title: node.attrs.string,
|
||||
});
|
||||
$button.append($('<i>', {class: 'fa ' + node.attrs.icon}));
|
||||
if (node.attrs.icon) {
|
||||
$button.addClass('o_icon_button');
|
||||
$button.append($('<i>', {class: 'fa ' + node.attrs.icon}));
|
||||
} else {
|
||||
$button.text(node.attrs.string);
|
||||
}
|
||||
|
||||
this._registerModifiers(node, record, $button);
|
||||
|
||||
|
||||
@@ -28,6 +28,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Need to put datetimepicker widget above everything else
|
||||
// The '.dropdown-menu' part is needed to override default bootstrap rule as
|
||||
// this file is unfortunately placed in common assets.
|
||||
.bootstrap-datetimepicker-widget.dropdown-menu {
|
||||
z-index: @zindex-modal + 1;
|
||||
}
|
||||
|
||||
.datepicker {
|
||||
.table-condensed {
|
||||
> thead {
|
||||
@@ -66,5 +73,5 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -961,7 +961,8 @@
|
||||
<span t-if="attrs.help">?</span>
|
||||
</label>
|
||||
<div t-att-style="style">
|
||||
<input type="text" size="15" t-att-name="attrs.name"
|
||||
<input type="text" size="15" class="o_input"
|
||||
t-att-name="attrs.name"
|
||||
t-att-autofocus="attrs.default_focus === '1' ? 'autofocus' : undefined"
|
||||
t-att-id="element_id"
|
||||
t-att-value="defaults[attrs.name] || ''"/>
|
||||
@@ -990,7 +991,9 @@
|
||||
<span t-if="attrs.help">?</span>
|
||||
</label>
|
||||
<div t-att-style="style">
|
||||
<select t-att-name="attrs.name" t-att-id="element_id"
|
||||
<select class="o_input"
|
||||
t-att-name="attrs.name"
|
||||
t-att-id="element_id"
|
||||
t-att-autofocus="attrs.default_focus === '1' || undefined">
|
||||
<option t-if="prepend_empty"/>
|
||||
<t t-foreach="attrs.selection" t-as="option">
|
||||
@@ -1012,7 +1015,7 @@
|
||||
<li class="o_filter_condition">
|
||||
<span class="o_or_filter">or</span>
|
||||
<span>
|
||||
<select class="o_searchview_extended_prop_field">
|
||||
<select class="o_input o_searchview_extended_prop_field">
|
||||
<t t-foreach="widget.attrs.fields" t-as="field">
|
||||
<option t-att="{'selected': field === widget.attrs.selected ? 'selected' : null}"
|
||||
t-att-value="field.name">
|
||||
@@ -1022,18 +1025,19 @@
|
||||
</select>
|
||||
<span class="o_searchview_extended_delete_prop fa fa-trash-o"/>
|
||||
</span>
|
||||
<select class="o_searchview_extended_prop_op"/>
|
||||
<select class="o_input o_searchview_extended_prop_op"/>
|
||||
<span class="o_searchview_extended_prop_value"/>
|
||||
</li>
|
||||
</t>
|
||||
<t t-name="SearchView.extended_search.proposition.float">
|
||||
<input t-att-type="widget.decimal_point === '.' ? 'number' : 'text'"
|
||||
<input class="o_input"
|
||||
t-att-type="widget.decimal_point === '.' ? 'number' : 'text'"
|
||||
t-attf-title="Number using #{widget.decimal_point || '.' } as decimal separator."
|
||||
t-attf-pattern="[0-9]+([\\#{widget.decimal_point || '.' }][0-9]+)?"
|
||||
t-attf-value="0#{widget.decimal_point || '.' }0" step="0.01"/>
|
||||
</t>
|
||||
<t t-name="SearchView.extended_search.proposition.selection">
|
||||
<select>
|
||||
<select class="o_input">
|
||||
<option t-foreach="widget.field.selection" t-as="element" t-att-value="element[0]">
|
||||
<t t-esc="element[1]"/>
|
||||
</option>
|
||||
@@ -1051,7 +1055,7 @@
|
||||
</ul>
|
||||
</div>
|
||||
<t t-name="GroupByMenuSelector">
|
||||
<li><select class="o_add_group o_group_selector">
|
||||
<li><select class="o_input o_add_group o_group_selector">
|
||||
<t t-foreach="groupableFields" t-as="field">
|
||||
<option t-att-data-name="field.name"><t t-esc="field.string"/></option>
|
||||
</t>
|
||||
|
||||
@@ -343,6 +343,27 @@ QUnit.module('core', function () {
|
||||
'2015-02-02');
|
||||
});
|
||||
|
||||
|
||||
QUnit.test('timedelta', function (assert) {
|
||||
assert.expect(4);
|
||||
assert.strictEqual(
|
||||
py.eval("(datetime.datetime(2017, 2, 15, 1, 7, 31) + datetime.timedelta(days=1)).strftime('%Y-%m-%d %H:%M:%S')",
|
||||
pyEval.context()),
|
||||
"2017-02-16 01:07:31");
|
||||
assert.strictEqual(
|
||||
py.eval("(datetime.datetime(2012, 2, 15, 1, 7, 31) - datetime.timedelta(hours=1)).strftime('%Y-%m-%d %H:%M:%S')",
|
||||
pyEval.context()),
|
||||
"2012-02-15 00:07:31");
|
||||
assert.strictEqual(
|
||||
py.eval("(datetime.datetime(2012, 2, 15, 1, 7, 31) + datetime.timedelta(hours=-1)).strftime('%Y-%m-%d %H:%M:%S')",
|
||||
pyEval.context()),
|
||||
"2012-02-15 00:07:31");
|
||||
assert.strictEqual(
|
||||
py.eval("(datetime.datetime(2012, 2, 15, 1, 7, 31) + datetime.timedelta(minutes=100)).strftime('%Y-%m-%d %H:%M:%S')",
|
||||
pyEval.context()),
|
||||
"2012-02-15 02:47:31");
|
||||
});
|
||||
|
||||
QUnit.test('datetime.tojson', function (assert) {
|
||||
assert.expect(7);
|
||||
|
||||
|
||||
@@ -394,7 +394,7 @@ QUnit.module('basic_fields', {
|
||||
QUnit.module('FieldFloat');
|
||||
|
||||
QUnit.test('float field when unset', function (assert) {
|
||||
assert.expect(1);
|
||||
assert.expect(2);
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
@@ -408,8 +408,10 @@ QUnit.module('basic_fields', {
|
||||
res_id: 4,
|
||||
});
|
||||
|
||||
assert.ok(form.$('.o_field_widget').hasClass('o_field_empty'),
|
||||
'Non-set float field should be recognized as unset.');
|
||||
assert.notOk(form.$('.o_field_widget').hasClass('o_field_empty'),
|
||||
'Non-set float field should be considered as 0.');
|
||||
assert.strictEqual(form.$('.o_field_widget').text(), "0.000",
|
||||
'Non-set float field should be considered as 0.');
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
@@ -536,6 +538,35 @@ QUnit.module('basic_fields', {
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('float widget on monetary field', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
this.data.partner.fields.monetary = {string: "Monetary", type: 'monetary'};
|
||||
this.data.partner.records[0].monetary = 9.99;
|
||||
this.data.partner.records[0].currency_id = 1;
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch:'<form string="Partners">' +
|
||||
'<sheet>' +
|
||||
'<field name="monetary" widget="float"/>' +
|
||||
'<field name="currency_id" invisible="1"/>' +
|
||||
'</sheet>' +
|
||||
'</form>',
|
||||
res_id: 1,
|
||||
session: {
|
||||
currencies: _.indexBy(this.data.currency.records, 'id'),
|
||||
},
|
||||
});
|
||||
|
||||
assert.strictEqual(form.$('.o_field_widget[name=monetary]').text(), '9.99',
|
||||
'value should be correctly formatted (with the float formatter)');
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.module('FieldEmail');
|
||||
|
||||
QUnit.test('email field in form view', function (assert) {
|
||||
@@ -1015,7 +1046,7 @@ QUnit.module('basic_fields', {
|
||||
QUnit.module('FieldBinary');
|
||||
|
||||
QUnit.test('binary fields are correctly rendered', function (assert) {
|
||||
assert.expect(9);
|
||||
assert.expect(16);
|
||||
|
||||
// save the session function
|
||||
var oldGetFile = session.get_file;
|
||||
@@ -1028,18 +1059,24 @@ QUnit.module('basic_fields', {
|
||||
return $.when();
|
||||
};
|
||||
|
||||
this.data.partner.records[0].foo = 'coucou.txt';
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<form string="Partners">' +
|
||||
'<field name="document"/>' +
|
||||
'<field name="document" filename="foo"/>' +
|
||||
'<field name="foo"/>' +
|
||||
'</form>',
|
||||
res_id: 1,
|
||||
});
|
||||
|
||||
assert.strictEqual(form.$('a.o_field_widget[name="document"] > .fa-download').length, 1,
|
||||
"the binary field should be rendered as a downloadable link in readonly");
|
||||
assert.strictEqual(form.$('a.o_field_widget[name="document"]').text().trim(), 'coucou.txt',
|
||||
"the binary field should display the name of the file in the link");
|
||||
assert.strictEqual(form.$('.o_field_char').text(), 'coucou.txt',
|
||||
"the filename field should have the file name as value");
|
||||
|
||||
form.$('a.o_field_widget[name="document"]').click();
|
||||
|
||||
@@ -1047,12 +1084,15 @@ QUnit.module('basic_fields', {
|
||||
|
||||
assert.strictEqual(form.$('a.o_field_widget[name="document"] > .fa-download').length, 0,
|
||||
"the binary field should not be rendered as a downloadable link in edit");
|
||||
assert.strictEqual(form.$('div.o_field_binary_file[name="document"]').length, 1,
|
||||
"the binary field should be correctly rendered in edit");
|
||||
assert.strictEqual(form.$('div.o_field_binary_file[name="document"] > input').val(), 'coucou.txt',
|
||||
"the binary field should display the file name in the input edit mode");
|
||||
assert.strictEqual(form.$('.o_field_binary_file > input').attr('readonly'), 'readonly',
|
||||
"the input should be readonly");
|
||||
assert.strictEqual(form.$('.o_field_binary_file > .o_clear_file_button').length, 1,
|
||||
"there shoud be a button to clear the file");
|
||||
assert.strictEqual(form.$('input.o_field_char').val(), 'coucou.txt',
|
||||
"the filename field should have the file name as value");
|
||||
|
||||
|
||||
form.$('.o_field_binary_file > .o_clear_file_button').click();
|
||||
|
||||
@@ -1060,6 +1100,16 @@ QUnit.module('basic_fields', {
|
||||
"the input should be hidden");
|
||||
assert.strictEqual(form.$('.o_field_binary_file > .o_select_file_button:not(.o_hidden)').length, 1,
|
||||
"there shoud be a button to upload the file");
|
||||
assert.strictEqual(form.$('input.o_field_char').val(), '',
|
||||
"the filename field should be empty since we removed the file");
|
||||
|
||||
form.$buttons.find('.o_form_button_save').click();
|
||||
assert.strictEqual(form.$('a.o_field_widget[name="document"] > .fa-download').length, 0,
|
||||
"the binary field should not render as a downloadable link since we removed the file");
|
||||
assert.strictEqual(form.$('a.o_field_widget[name="document"]').text().trim(), '',
|
||||
"the binary field should not display a filename in the link since we removed the file");
|
||||
assert.strictEqual(form.$('.o_field_char').text().trim(), '',
|
||||
"the filename field should be empty since we removed the file");
|
||||
|
||||
form.destroy();
|
||||
|
||||
@@ -1926,7 +1976,7 @@ QUnit.module('basic_fields', {
|
||||
QUnit.module('FieldInteger');
|
||||
|
||||
QUnit.test('integer field when unset', function (assert) {
|
||||
assert.expect(1);
|
||||
assert.expect(2);
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
@@ -1936,8 +1986,10 @@ QUnit.module('basic_fields', {
|
||||
res_id: 4,
|
||||
});
|
||||
|
||||
assert.ok(form.$('.o_field_widget').hasClass('o_field_empty'),
|
||||
'Non-set integer field should be recognized as unset.');
|
||||
assert.notOk(form.$('.o_field_widget').hasClass('o_field_empty'),
|
||||
'Non-set integer field should be recognized as 0.');
|
||||
assert.strictEqual(form.$('.o_field_widget').text(), "0",
|
||||
'Non-set integer field should be recognized as 0.');
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
@@ -199,6 +199,115 @@ QUnit.module('relational_fields', {
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('many2ones in form views with show_adress', function (assert) {
|
||||
assert.expect(4);
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<form string="Partners">' +
|
||||
'<sheet>' +
|
||||
'<group>' +
|
||||
'<field ' +
|
||||
'name="trululu" ' +
|
||||
'string="custom label" ' +
|
||||
'context="{\'search_default_customer\':1, \'show_address\': 1}" ' +
|
||||
'options="{\'always_reload\': True}"' +
|
||||
'/>' +
|
||||
'</group>' +
|
||||
'</sheet>' +
|
||||
'</form>',
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === 'name_get') {
|
||||
return this._super(route, args).then(function (result) {
|
||||
result[0][1] += '\nStreet\nCity ZIP';
|
||||
return result;
|
||||
});
|
||||
}
|
||||
return this._super(route, args);
|
||||
},
|
||||
res_id: 1,
|
||||
});
|
||||
|
||||
assert.strictEqual($('a.o_form_uri').html(), 'aaa<br>Street<br>City ZIP',
|
||||
"input should have a multi-line content in readonly due to show_address");
|
||||
form.$buttons.find('.o_form_button_edit').click();
|
||||
assert.strictEqual(form.$('button.o_external_button:visible').length, 1,
|
||||
"should have an open record button");
|
||||
|
||||
form.$('input.o_input').click();
|
||||
form.$('input.o_input').trigger($.Event('keyup', {
|
||||
which: $.ui.keyCode.ESC,
|
||||
keyCode: $.ui.keyCode.ESC,
|
||||
}));
|
||||
|
||||
assert.strictEqual(form.$('button.o_external_button:visible').length, 1,
|
||||
"should still have an open record button");
|
||||
form.$('input.o_input').trigger('focusout');
|
||||
assert.strictEqual($('.modal button:contains(Create and edit)').length, 0,
|
||||
"there should not be a quick create modal");
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('many2ones in form views with search more', function (assert) {
|
||||
assert.expect(3);
|
||||
this.data.partner.records.push({
|
||||
id: 5,
|
||||
display_name: "Partner 4",
|
||||
}, {
|
||||
id: 6,
|
||||
display_name: "Partner 5",
|
||||
}, {
|
||||
id: 7,
|
||||
display_name: "Partner 6",
|
||||
}, {
|
||||
id: 8,
|
||||
display_name: "Partner 7",
|
||||
}, {
|
||||
id: 9,
|
||||
display_name: "Partner 8",
|
||||
}, {
|
||||
id: 10,
|
||||
display_name: "Partner 9",
|
||||
});
|
||||
this.data.partner.fields.datetime.searchable = true;
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<form string="Partners">' +
|
||||
'<sheet>' +
|
||||
'<group>' +
|
||||
'<field name="trululu"/>' +
|
||||
'</group>' +
|
||||
'</sheet>' +
|
||||
'</form>',
|
||||
archs: {
|
||||
'partner,false,list': '<tree><field name="display_name"/></tree>',
|
||||
'partner,false,search': '<search><field name="datetime"/></search>',
|
||||
},
|
||||
res_id: 1,
|
||||
});
|
||||
|
||||
form.$buttons.find('.o_form_button_edit').click();
|
||||
var $dropdown = form.$('.o_field_many2one input').autocomplete('widget');
|
||||
form.$('.o_field_many2one input').click();
|
||||
$dropdown.find('.o_m2o_dropdown_option:contains(Search)').mouseenter().click(); // Open Search More
|
||||
|
||||
assert.strictEqual($('tr.o_data_row').length, 9, "should display 9 records");
|
||||
|
||||
$('.o_searchview_more').click(); // Magnifying class for more filters
|
||||
$('button:contains(Filters)').click();
|
||||
$('.o_add_filter').click(); // Add a custom filter, datetime field is selected
|
||||
assert.strictEqual($('li.o_filter_condition select.o_searchview_extended_prop_field').val(), 'datetime',
|
||||
"datetime field should be selected");
|
||||
$('.o_apply_filter').click();
|
||||
|
||||
assert.strictEqual($('tr.o_data_row').length, 0, "should display 0 records");
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('onchanges on many2ones trigger when editing record in form view', function (assert) {
|
||||
assert.expect(9);
|
||||
|
||||
@@ -2927,8 +3036,8 @@ QUnit.module('relational_fields', {
|
||||
|
||||
assert.strictEqual(form.$('.o_data_row').length, 3,
|
||||
"sould have 3 records in one2many list");
|
||||
assert.strictEqual(form.$('.o_data_row').text(), "blip1.59yop1.5tototo1.550xphone",
|
||||
"sould display the record values in one2many list");
|
||||
assert.strictEqual(form.$('.o_data_row').text(), "blip1.59yop1.50tototo1.550xphone",
|
||||
"should display the record values in one2many list");
|
||||
|
||||
$('.o_form_button_save').click();
|
||||
|
||||
@@ -5153,7 +5262,7 @@ QUnit.module('relational_fields', {
|
||||
});
|
||||
|
||||
QUnit.test('widget selection, edition and on many2one field', function (assert) {
|
||||
assert.expect(15);
|
||||
assert.expect(18);
|
||||
|
||||
this.data.partner.onchanges = {product_id: function () {}};
|
||||
this.data.partner.records[0].product_id = 37;
|
||||
@@ -5178,6 +5287,13 @@ QUnit.module('relational_fields', {
|
||||
});
|
||||
|
||||
assert.ok(!form.$('select').length, "should not have a select tag in dom");
|
||||
assert.strictEqual(form.$('.o_field_widget[name=product_id]').text(), 'xphone',
|
||||
"should have rendered the many2one field correctly");
|
||||
assert.strictEqual(form.$('.o_field_widget[name=trululu]').text(), '',
|
||||
"should have rendered the unset many2one field correctly");
|
||||
assert.strictEqual(form.$('.o_field_widget[name=color]').text(), 'Red',
|
||||
"should have rendered the selection field correctly");
|
||||
|
||||
form.$buttons.find('.o_form_button_edit').click();
|
||||
|
||||
assert.strictEqual(form.$('select').length, 3,
|
||||
@@ -5811,6 +5927,52 @@ QUnit.module('relational_fields', {
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('name_create in form dialog', function (assert) {
|
||||
var done = assert.async();
|
||||
assert.expect(2);
|
||||
|
||||
var M2O_DELAY = relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY;
|
||||
relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = 0;
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<form>' +
|
||||
'<group>' +
|
||||
'<field name="p">' +
|
||||
'<tree>' +
|
||||
'<field name="bar"/>' +
|
||||
'</tree>' +
|
||||
'<form>' +
|
||||
'<field name="product_id"/>' +
|
||||
'</form>' +
|
||||
'</field>' +
|
||||
'</group>' +
|
||||
'</form>',
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === 'name_create') {
|
||||
assert.step('name_create');
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
|
||||
form.$buttons.find('.o_form_button_edit').click();
|
||||
form.$('.o_field_x2many_list_row_add a').click();
|
||||
var $dropdown = $('.modal .o_field_many2one input').autocomplete('widget');
|
||||
$('.modal input').val('new record').trigger('keydown');
|
||||
concurrency.delay(0).then(function () {
|
||||
$dropdown.find('li:first()').click(); // quick create 'new record'
|
||||
assert.verifySteps(['name_create']);
|
||||
|
||||
relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = M2O_DELAY;
|
||||
form.destroy();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
odoo.define('web.upgrade_fields_tests', function (require) {
|
||||
"use strict";
|
||||
|
||||
var FormView = require('web.FormView');
|
||||
var testUtils = require('web.test_utils');
|
||||
|
||||
var createView = testUtils.createView;
|
||||
|
||||
QUnit.module('fields', {}, function () {
|
||||
|
||||
QUnit.module('upgrade_fields', {
|
||||
beforeEach: function () {
|
||||
this.data = {
|
||||
partner: {
|
||||
fields: {
|
||||
bar: {string: "Bar", type: "boolean"},
|
||||
},
|
||||
}
|
||||
};
|
||||
},
|
||||
}, function () {
|
||||
|
||||
QUnit.module('UpgradeBoolean');
|
||||
|
||||
QUnit.test('widget upgrade_boolean in a form view', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<form><field name="bar" widget="upgrade_boolean"/></form>',
|
||||
});
|
||||
|
||||
form.$('input:checkbox').click();
|
||||
assert.strictEqual($('.modal').length, 1,
|
||||
"the 'Upgrade to Enterprise' dialog should be opened");
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -537,11 +537,14 @@ var MockServer = Class.extend({
|
||||
ids = [ids];
|
||||
}
|
||||
var fields = args[1] && args[1].length ? _.uniq(args[1].concat(['id'])) : Object.keys(this.data[model].fields);
|
||||
var results = _.map(ids, function (id) {
|
||||
var record = _.findWhere(self.data[model].records, {id: id});
|
||||
if (!record) {
|
||||
throw "mock read: id does not exist...";
|
||||
var records = _.reduce(ids, function (records, id) {
|
||||
if (!id) {
|
||||
throw "mock read: falsy value given as id, would result in an access error in actual server !";
|
||||
}
|
||||
var record = _.findWhere(self.data[model].records, {id: id});
|
||||
return record ? records.concat(record) : records;
|
||||
}, []);
|
||||
var results = _.map(records, function (record) {
|
||||
var result = {};
|
||||
for (var i = 0; i < fields.length; i++) {
|
||||
var field = self.data[model].fields[fields[i]];
|
||||
@@ -549,7 +552,12 @@ var MockServer = Class.extend({
|
||||
// the field doens't exist on the model, so skip it
|
||||
continue;
|
||||
}
|
||||
if (field.type === 'many2one') {
|
||||
if (field.type === 'float' ||
|
||||
field.type === 'integer' ||
|
||||
field.type === 'monetary') {
|
||||
// read should return 0 for unset numeric fields
|
||||
result[fields[i]] = record[fields[i]] || 0;
|
||||
} else if (field.type === 'many2one') {
|
||||
var relatedRecord = _.findWhere(self.data[field.relation].records, {
|
||||
id: record[fields[i]]
|
||||
});
|
||||
|
||||
@@ -92,6 +92,23 @@ QUnit.module('Views', {
|
||||
model.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('rejects loading a record with invalid id', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
this.params.res_id = 99;
|
||||
|
||||
var model = createModel({
|
||||
Model: BasicModel,
|
||||
data: this.data,
|
||||
});
|
||||
|
||||
model.load(this.params).always(function (error) {
|
||||
assert.strictEqual(this.state(), 'rejected',
|
||||
"load should return a rejected deferred for an invalid id")
|
||||
});
|
||||
model.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('notify change with many2one', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
@@ -1347,8 +1364,8 @@ QUnit.module('Views', {
|
||||
delete this.params.res_id;
|
||||
model.load(this.params).then(function (resultID) {
|
||||
var record = model.get(resultID);
|
||||
assert.strictEqual(record.data.bar, false,
|
||||
"should be initialized with correct value");
|
||||
assert.strictEqual(record.data.bar, 0,
|
||||
"should be initialized with correct value (0 as integer)");
|
||||
|
||||
model.notifyChanges(resultID, {foo: "anotherverylongstring"});
|
||||
|
||||
|
||||
@@ -625,10 +625,10 @@ QUnit.module('Views', {
|
||||
res_id: 2,
|
||||
});
|
||||
|
||||
assert.strictEqual(form.$('.o_field_widget.o_field_empty').length, 2,
|
||||
"should have 2 empty fields with correct class");
|
||||
assert.strictEqual(form.$('.o_form_label_empty').length, 2,
|
||||
"should have 2 muted labels (for the empty fieds) in readonly");
|
||||
assert.strictEqual(form.$('.o_field_widget.o_field_empty').length, 1,
|
||||
"should have 1 empty field with correct class");
|
||||
assert.strictEqual(form.$('.o_form_label_empty').length, 1,
|
||||
"should have 1 muted label (for the empty fied) in readonly");
|
||||
|
||||
form.$buttons.find('.o_form_button_edit').click();
|
||||
|
||||
@@ -771,7 +771,7 @@ QUnit.module('Views', {
|
||||
});
|
||||
|
||||
QUnit.test('required float fields works as expected', function (assert) {
|
||||
assert.expect(8);
|
||||
assert.expect(10);
|
||||
|
||||
this.data.partner.fields.qux.required = true;
|
||||
var form = createView({
|
||||
@@ -793,24 +793,26 @@ QUnit.module('Views', {
|
||||
|
||||
assert.ok(form.$('input[name="qux"]').hasClass('o_required_modifier'),
|
||||
"qux input is flagged as required");
|
||||
assert.strictEqual(form.$('input[name="qux"]').val(), "",
|
||||
"qux input is empty");
|
||||
assert.strictEqual(form.$('input[name="qux"]').val(), "0.0",
|
||||
"qux input is 0 by default (float field)");
|
||||
|
||||
form.$buttons.find('.o_form_button_save').click();
|
||||
|
||||
assert.ok(form.$('input[name="qux"]').hasClass('o_field_invalid'),
|
||||
"qux input is displayed as invalid");
|
||||
assert.notOk(form.$('input[name="qux"]').hasClass('o_field_invalid'),
|
||||
"qux input is not displayed as invalid");
|
||||
|
||||
form.$('input[name="qux"]').val("0").trigger('input');
|
||||
form.$buttons.find('.o_form_button_edit').click();
|
||||
|
||||
form.$('input[name="qux"]').val("1").trigger('input');
|
||||
|
||||
form.$buttons.find('.o_form_button_save').click();
|
||||
|
||||
form.$buttons.find('.o_form_button_edit').click();
|
||||
|
||||
assert.strictEqual(form.$('input[name="qux"]').val(), "0.0",
|
||||
assert.strictEqual(form.$('input[name="qux"]').val(), "1.0",
|
||||
"qux input is properly formatted");
|
||||
|
||||
assert.verifySteps(['default_get', 'create', 'read']);
|
||||
assert.verifySteps(['default_get', 'create', 'read', 'write', 'read']);
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
@@ -3278,7 +3280,7 @@ QUnit.module('Views', {
|
||||
});
|
||||
|
||||
QUnit.test('navigation with tab key in form view', function (assert) {
|
||||
assert.expect(2);
|
||||
assert.expect(3);
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
@@ -3287,8 +3289,9 @@ QUnit.module('Views', {
|
||||
arch: '<form string="Partners">' +
|
||||
'<sheet>' +
|
||||
'<group>' +
|
||||
'<field name="foo"/>' +
|
||||
'<field name="foo" widget="email"/>' +
|
||||
'<field name="bar"/>' +
|
||||
'<field name="display_name" widget="url"/>' +
|
||||
'</group>' +
|
||||
'</sheet>' +
|
||||
'</form>',
|
||||
@@ -3300,18 +3303,73 @@ QUnit.module('Views', {
|
||||
|
||||
// focus first input, trigger tab
|
||||
form.$('input[name="foo"]').focus();
|
||||
|
||||
form.$('input[name="foo"]').trigger($.Event('keydown', {which: $.ui.keyCode.TAB}));
|
||||
assert.ok($.contains(form.$('div[name="bar"]')[0], document.activeElement),
|
||||
"bar checkbox should be focused");
|
||||
|
||||
form.$('div[name="bar"]').trigger($.Event('keydown', {which: $.ui.keyCode.TAB}));
|
||||
assert.strictEqual(form.$('input[name="display_name"]')[0], document.activeElement,
|
||||
"display_name should be focused");
|
||||
|
||||
// simulate shift+tab on active element
|
||||
$(document.activeElement).trigger($.Event('keydown', {which: $.ui.keyCode.TAB, shiftKey: true}));
|
||||
$(document.activeElement).trigger($.Event('keydown', {which: $.ui.keyCode.TAB, shiftKey: true}));
|
||||
assert.strictEqual(document.activeElement, form.$('input[name="foo"]')[0],
|
||||
"first input should be focused");
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('navigation with tab key in readonly form view', function (assert) {
|
||||
// The behavior of the phone widget is completely altered by crm_voip so
|
||||
// this test fails if crm_voip is installed. The enterprise module is
|
||||
// responsible for testing its own behavior in its own tests.
|
||||
if ('crm.voip' in odoo.__DEBUG__.services) {
|
||||
assert.expect(0);
|
||||
return;
|
||||
}
|
||||
|
||||
assert.expect(3);
|
||||
|
||||
this.data.partner.records[1].product_id = 37;
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<form string="Partners">' +
|
||||
'<sheet>' +
|
||||
'<group>' +
|
||||
'<field name="trululu"/>' +
|
||||
'<field name="foo"/>' +
|
||||
'<field name="product_id"/>' +
|
||||
'<field name="foo" widget="phone"/>' +
|
||||
'<field name="display_name" widget="url"/>' +
|
||||
'</group>' +
|
||||
'</sheet>' +
|
||||
'</form>',
|
||||
res_id: 2,
|
||||
});
|
||||
|
||||
// focus first field, trigger tab
|
||||
form.$('[name="trululu"]').focus();
|
||||
form.$('[name="trululu"]').trigger($.Event('keydown', {which: $.ui.keyCode.TAB}));
|
||||
assert.strictEqual(form.$('[name="product_id"]')[0], document.activeElement,
|
||||
"product_id should be focused");
|
||||
form.$('[name="product_id"]').trigger($.Event('keydown', {which: $.ui.keyCode.TAB}));
|
||||
assert.strictEqual(form.$('[name="display_name"]')[0], document.activeElement,
|
||||
"display_name should be focused (emails are focusable but phone aren't)");
|
||||
|
||||
// simulate shift+tab on active element
|
||||
$(document.activeElement).trigger($.Event('keydown', {which: $.ui.keyCode.TAB, shiftKey: true}));
|
||||
$(document.activeElement).trigger($.Event('keydown', {which: $.ui.keyCode.TAB, shiftKey: true}));
|
||||
assert.strictEqual(document.activeElement, form.$('[name="trululu"]')[0],
|
||||
"first many2one should be focused");
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('skip invisible fields when navigating with TAB', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
@@ -4640,6 +4698,64 @@ QUnit.module('Views', {
|
||||
_t.database.multi_lang = multi_lang;
|
||||
});
|
||||
|
||||
QUnit.test('translate event correctly handled with multiple controllers', function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
this.data.product.fields.name.translate = true;
|
||||
this.data.partner.records[0].product_id = 37;
|
||||
var nbTranslateCalls = 0;
|
||||
|
||||
var multi_lang = _t.database.multi_lang;
|
||||
_t.database.multi_lang = true;
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<form string="Partners">' +
|
||||
'<sheet>' +
|
||||
'<group>' +
|
||||
'<field name="product_id"/>' +
|
||||
'</group>' +
|
||||
'</sheet>' +
|
||||
'</form>',
|
||||
archs: {
|
||||
'product,false,form': '<form>' +
|
||||
'<sheet>' +
|
||||
'<group>' +
|
||||
'<field name="name"/>' +
|
||||
'<field name="partner_type_id"/>' +
|
||||
'</group>' +
|
||||
'</sheet>' +
|
||||
'</form>',
|
||||
},
|
||||
res_id: 1,
|
||||
mockRPC: function (route, args) {
|
||||
if (route === '/web/dataset/call_kw/product/get_formview_id') {
|
||||
return $.when(false);
|
||||
} else if (route === "/web/dataset/call_button" && args.method === 'translate_fields') {
|
||||
assert.deepEqual(args.args, ["product",37,"name",{}], 'should call "call_button" route');
|
||||
nbTranslateCalls++;
|
||||
return $.when();
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
|
||||
form.$buttons.find('.o_form_button_edit').click();
|
||||
form.$('[name="product_id"] .o_external_button').click();
|
||||
|
||||
assert.strictEqual($('.modal-body .o_field_translate').length, 1,
|
||||
"there should be a translate button in the modal");
|
||||
|
||||
$('.modal-body .o_field_translate').click();
|
||||
|
||||
assert.strictEqual(nbTranslateCalls, 1, "should call_button translate once");
|
||||
|
||||
form.destroy();
|
||||
_t.database.multi_lang = multi_lang;
|
||||
});
|
||||
|
||||
QUnit.test('buttons are disabled until action is resolved', function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
|
||||
@@ -445,7 +445,7 @@ QUnit.module('Views', {
|
||||
});
|
||||
|
||||
QUnit.test('many2many_tags are correctly fetched and displayed', function (assert) {
|
||||
assert.expect(5);
|
||||
assert.expect(10);
|
||||
|
||||
this.data.partner.records[0].category_ids = [6, 7];
|
||||
this.data.partner.records[1].category_ids = [7];
|
||||
@@ -459,6 +459,7 @@ QUnit.module('Views', {
|
||||
'<div>' +
|
||||
'<field name="category_ids"/>' +
|
||||
'<field name="foo"/>' +
|
||||
'<field name="state" widget="priority"/>' +
|
||||
'</div>' +
|
||||
'</t></templates>' +
|
||||
'</kanban>',
|
||||
@@ -474,7 +475,20 @@ QUnit.module('Views', {
|
||||
assert.ok($first_record.find('.o_tag:first()').hasClass('o_tag_color_2'),
|
||||
'first tag should have color 2');
|
||||
assert.verifySteps(['/web/dataset/search_read', '/web/dataset/call_kw/category/read'],
|
||||
'two RPC should have been done(one search read and one read for the m2m');
|
||||
'two RPC should have been done (one search read and one read for the m2m)');
|
||||
|
||||
// Write on the record using the priority widget to trigger a re-render in readonly
|
||||
kanban.$('.o_field_widget.o_priority a.o_priority_star.fa-star-o').first().click();
|
||||
assert.verifySteps([
|
||||
'/web/dataset/search_read',
|
||||
'/web/dataset/call_kw/category/read',
|
||||
'/web/dataset/call_kw/partner/write',
|
||||
'/web/dataset/call_kw/partner/read',
|
||||
'/web/dataset/call_kw/category/read'
|
||||
], 'five RPCs should have been done (previous 2, 1 write (triggers a re-render), same 2 at re-render');
|
||||
assert.strictEqual(kanban.$('.o_kanban_record:first()').find('.o_field_many2manytags .o_tag').length, 2,
|
||||
'first record should still contain only 2 tags');
|
||||
|
||||
kanban.destroy();
|
||||
});
|
||||
|
||||
@@ -1505,7 +1519,8 @@ QUnit.module('Views', {
|
||||
});
|
||||
|
||||
QUnit.test('group_by_tooltip option when grouping on a many2one', function (assert) {
|
||||
assert.expect(5);
|
||||
assert.expect(12);
|
||||
delete this.data.partner.records[3].product_id;
|
||||
var kanban = createView({
|
||||
View: KanbanView,
|
||||
model: 'partner',
|
||||
@@ -1528,18 +1543,89 @@ QUnit.module('Views', {
|
||||
},
|
||||
});
|
||||
|
||||
assert.ok(kanban.$('.o_kanban_view').hasClass('o_kanban_grouped'),
|
||||
"should have classname 'o_kanban_grouped'");
|
||||
assert.strictEqual(kanban.$('.o_kanban_group').length, 2, "should have " + 2 + " columns");
|
||||
|
||||
// simulate an update coming from the searchview, with another groupby given
|
||||
kanban.update({groupBy: ['product_id']});
|
||||
assert.ok(kanban.$('.o_kanban_group:first span.o_column_title:contains(hello)').length,
|
||||
"first column should have a title with a value from the many2one");
|
||||
assert.strictEqual(kanban.$('.o_kanban_group').length, 3, "should have " + 3 + " columns");
|
||||
assert.strictEqual(kanban.$('.o_kanban_group:nth-child(1) .o_kanban_record').length, 1,
|
||||
"column should contain 1 record(s)");
|
||||
assert.strictEqual(kanban.$('.o_kanban_group:nth-child(2) .o_kanban_record').length, 2,
|
||||
"column should contain 2 record(s)");
|
||||
assert.strictEqual(kanban.$('.o_kanban_group:nth-child(3) .o_kanban_record').length, 1,
|
||||
"column should contain 1 record(s)");
|
||||
assert.ok(kanban.$('.o_kanban_group:first span.o_column_title:contains(Undefined)').length,
|
||||
"first column should have a default title for when no value is provided");
|
||||
assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_header').data('original-title'),
|
||||
"<p>1 records</p>",
|
||||
"first column should have a tooltip with the number of records, but not" +
|
||||
"the group_by_tooltip title and the many2one field value since it has no value");
|
||||
assert.ok(kanban.$('.o_kanban_group:eq(1) span.o_column_title:contains(hello)').length,
|
||||
"second column should have a title with a value from the many2one");
|
||||
assert.strictEqual(kanban.$('.o_kanban_group:eq(1) .o_kanban_header').data('original-title'),
|
||||
"<p>2 records</p><div>Kikou<br>hello</div>",
|
||||
"first column should have a tooltip with the number of records, the group_by_tooltip title and many2one field value");
|
||||
"second column should have a tooltip with the number of records, the group_by_tooltip title and many2one field value");
|
||||
|
||||
kanban.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('move a record then put it again in the same column', function (assert) {
|
||||
assert.expect(6);
|
||||
|
||||
this.data.partner.records = [];
|
||||
|
||||
var kanban = createView({
|
||||
View: KanbanView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<kanban>' +
|
||||
'<field name="product_id"/>' +
|
||||
'<templates><t t-name="kanban-box">' +
|
||||
'<div><field name="display_name"/></div>' +
|
||||
'</t></templates></kanban>',
|
||||
groupBy: ['product_id'],
|
||||
});
|
||||
|
||||
kanban.$('.o_column_quick_create').click();
|
||||
kanban.$('.o_column_quick_create input').val('column1');
|
||||
kanban.$('.o_column_quick_create button.o_kanban_add').click();
|
||||
|
||||
kanban.$('.o_column_quick_create').click();
|
||||
kanban.$('.o_column_quick_create input').val('column2');
|
||||
kanban.$('.o_column_quick_create button.o_kanban_add').click();
|
||||
|
||||
kanban.$('.o_kanban_group:eq(1) .o_kanban_quick_add i').click();
|
||||
var $quickCreate = kanban.$('.o_kanban_group:eq(1) .o_kanban_quick_create');
|
||||
$quickCreate.find('input').val('new partner');
|
||||
$quickCreate.find('button.o_kanban_add').click();
|
||||
|
||||
assert.strictEqual(kanban.$('.o_kanban_group:eq(0) .o_kanban_record').length, 0,
|
||||
"column should contain 0 record");
|
||||
assert.strictEqual(kanban.$('.o_kanban_group:eq(1) .o_kanban_record').length, 1,
|
||||
"column should contain 1 records");
|
||||
|
||||
var $record = kanban.$('.o_kanban_group:eq(1) .o_kanban_record:eq(0)');
|
||||
var $group = kanban.$('.o_kanban_group:eq(0)');
|
||||
testUtils.dragAndDrop($record, $group);
|
||||
|
||||
assert.strictEqual(kanban.$('.o_kanban_group:eq(0) .o_kanban_record').length, 1,
|
||||
"column should contain 1 records");
|
||||
assert.strictEqual(kanban.$('.o_kanban_group:eq(1) .o_kanban_record').length, 0,
|
||||
"column should contain 0 records");
|
||||
|
||||
$record = kanban.$('.o_kanban_group:eq(0) .o_kanban_record:eq(0)');
|
||||
$group = kanban.$('.o_kanban_group:eq(1)');
|
||||
|
||||
testUtils.dragAndDrop($record, $group);
|
||||
|
||||
assert.strictEqual(kanban.$('.o_kanban_group:eq(0) .o_kanban_record').length, 0,
|
||||
"column should contain 0 records");
|
||||
assert.strictEqual(kanban.$('.o_kanban_group:eq(1) .o_kanban_record').length, 1,
|
||||
"column should contain 1 records");
|
||||
kanban.destroy();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -136,7 +136,7 @@ QUnit.module('Views', {
|
||||
});
|
||||
|
||||
QUnit.test('simple editable rendering', function (assert) {
|
||||
assert.expect(9);
|
||||
assert.expect(12);
|
||||
|
||||
var list = createView({
|
||||
View: ListView,
|
||||
@@ -164,6 +164,15 @@ QUnit.module('Views', {
|
||||
"should have a visible save button");
|
||||
assert.ok(list.$buttons.find('.o_list_button_discard').is(':visible'),
|
||||
"should have a visible discard button");
|
||||
|
||||
list.$buttons.find('.o_list_button_save').click();
|
||||
|
||||
assert.ok(list.$buttons.find('.o_list_button_add').is(':visible'),
|
||||
"should have a visible Create button");
|
||||
assert.ok(!list.$buttons.find('.o_list_button_save').is(':visible'),
|
||||
"should not have a visible save button");
|
||||
assert.ok(!list.$buttons.find('.o_list_button_discard').is(':visible'),
|
||||
"should not have a visible discard button");
|
||||
list.destroy();
|
||||
});
|
||||
|
||||
@@ -1046,6 +1055,24 @@ QUnit.module('Views', {
|
||||
list.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('list view with a button without icon', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
var list = createView({
|
||||
View: ListView,
|
||||
model: 'foo',
|
||||
data: this.data,
|
||||
arch: '<tree string="Phonecalls" editable="top">' +
|
||||
'<field name="foo"/>' +
|
||||
'<button string="abc" type="object" name="schedule_another_phonecall"/>' +
|
||||
'</tree>',
|
||||
});
|
||||
|
||||
assert.strictEqual(list.$('table button').first().text(), 'abc',
|
||||
"should have rendered a button with string attribute as label");
|
||||
list.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('list view, editable, can discard', function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
|
||||
@@ -438,6 +438,7 @@
|
||||
<script type="text/javascript" src="/web/static/tests/fields/field_utils_tests.js"></script>
|
||||
<script type="text/javascript" src="/web/static/tests/fields/relational_fields_tests.js"></script>
|
||||
<script type="text/javascript" src="/web/static/tests/fields/special_fields.js"></script>
|
||||
<script type="text/javascript" src="/web/static/tests/fields/upgrade_fields_tests.js"></script>
|
||||
|
||||
<script type="text/javascript" src="/web/static/tests/views/abstract_view_tests.js"></script>
|
||||
<script type="text/javascript" src="/web/static/tests/views/form_tests.js"></script>
|
||||
|
||||
@@ -6,7 +6,7 @@ import logging
|
||||
from lxml import etree, html
|
||||
|
||||
from odoo.exceptions import AccessError
|
||||
from odoo import api, models
|
||||
from odoo import api, fields, models
|
||||
from odoo.tools import pycompat
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
@@ -34,6 +34,10 @@ class IrUiView(models.Model):
|
||||
def extract_embedded_fields(self, arch):
|
||||
return arch.xpath('//*[@data-oe-model != "ir.ui.view"]')
|
||||
|
||||
@api.model
|
||||
def get_default_lang_code(self):
|
||||
return False
|
||||
|
||||
@api.model
|
||||
def save_embedded_field(self, el):
|
||||
Model = self.env[el.get('data-oe-model')]
|
||||
@@ -45,7 +49,10 @@ class IrUiView(models.Model):
|
||||
|
||||
if value is not None:
|
||||
# TODO: batch writes?
|
||||
Model.browse(int(el.get('data-oe-id'))).write({field: value})
|
||||
if self.get_default_lang_code() and field != 'arch' and not isinstance(Model._fields[field], fields.Html):
|
||||
Model.browse(int(el.get('data-oe-id'))).with_context(lang=self.get_default_lang_code()).write({field: value})
|
||||
else:
|
||||
Model.browse(int(el.get('data-oe-id'))).write({field: value})
|
||||
|
||||
def _pretty_arch(self, arch):
|
||||
# remove_blank_string does not seem to work on HTMLParser, and
|
||||
|
||||
@@ -192,6 +192,7 @@ var FieldTextHtmlSimple = basic_fields.DebouncedField.extend({
|
||||
var FieldTextHtml = AbstractField.extend({
|
||||
template: 'web_editor.FieldTextHtml',
|
||||
supportedFieldTypes: ['html'],
|
||||
|
||||
start: function () {
|
||||
var self = this;
|
||||
|
||||
@@ -206,7 +207,7 @@ var FieldTextHtml = AbstractField.extend({
|
||||
};
|
||||
window.odoo[this.callback+"_updown"] = null;
|
||||
window.odoo[this.callback+"_downup"] = function (value) {
|
||||
self.set_value(value);
|
||||
self._setValue(value);
|
||||
self.resize();
|
||||
};
|
||||
|
||||
@@ -236,10 +237,13 @@ var FieldTextHtml = AbstractField.extend({
|
||||
// this.$translate = $();
|
||||
return def;
|
||||
},
|
||||
getDatarecord: function () {
|
||||
return this.recordData;
|
||||
},
|
||||
get_url: function (_attr) {
|
||||
var src = this.nodeOptions.editor_url || "/mass_mailing/field/email_template";
|
||||
var k;
|
||||
var datarecord = this.recordData;
|
||||
var datarecord = this.getDatarecord();
|
||||
var attr = {
|
||||
'model': this.model,
|
||||
'field': this.name,
|
||||
|
||||
@@ -145,3 +145,13 @@ class View(models.Model):
|
||||
menu_data=self.env['ir.ui.menu'].load_menus_root() if request.website.is_user() else None,
|
||||
)
|
||||
return qcontext
|
||||
|
||||
|
||||
@api.model
|
||||
def get_default_lang_code(self):
|
||||
website_id = self.env.context.get('website_id')
|
||||
if website_id:
|
||||
lang_code = self.env['website'].browse(website_id).default_lang_code
|
||||
return lang_code
|
||||
else:
|
||||
return super(View, self).get_default_lang_code()
|
||||
@@ -7,6 +7,12 @@ class TestUi(odoo.tests.HttpCase):
|
||||
at_install = False
|
||||
|
||||
def test_admin(self):
|
||||
# Seen that:
|
||||
# - this test relies on demo data that are entirely in USD (pricelists)
|
||||
# - that main demo company is gelocated in US
|
||||
# - that this test awaits for hardcoded USDs amount
|
||||
# we have to force company currency as USDs only for this test
|
||||
self.env.ref('base.main_company').write({'currency_id': self.env.ref('base.USD').id})
|
||||
self.phantom_js("/", "odoo.__DEBUG__.services['web_tour.tour'].run('event_buy_tickets')", "odoo.__DEBUG__.services['web_tour.tour'].tours.event_buy_tickets.ready", login="admin")
|
||||
|
||||
def test_demo(self):
|
||||
|
||||
@@ -788,6 +788,11 @@ msgstr ""
|
||||
msgid "Close"
|
||||
msgstr ""
|
||||
|
||||
#. module: website_forum
|
||||
#: model:ir.ui.view,arch_db:website_forum.close_post
|
||||
msgid "Close Post"
|
||||
msgstr ""
|
||||
|
||||
#. module: website_forum
|
||||
#: model:ir.model.fields,field_description:website_forum.field_forum_forum_karma_close_all
|
||||
msgid "Close all posts"
|
||||
@@ -798,6 +803,11 @@ msgstr ""
|
||||
msgid "Close own posts"
|
||||
msgstr ""
|
||||
|
||||
#. module: website_forum
|
||||
#: model:ir.ui.view,arch_db:website_forum.close_post
|
||||
msgid "Close post"
|
||||
msgstr ""
|
||||
|
||||
#. module: website_forum
|
||||
#: model:ir.model.fields,field_description:website_forum.field_forum_post_closed_uid
|
||||
msgid "Closed by"
|
||||
@@ -1205,6 +1215,7 @@ msgid "First upvote"
|
||||
msgstr ""
|
||||
|
||||
#. module: website_forum
|
||||
#: model:ir.ui.view,arch_db:website_forum.post_answers
|
||||
#: model:ir.ui.view,arch_db:website_forum.post_description_full
|
||||
msgid "Flag"
|
||||
msgstr ""
|
||||
@@ -1218,6 +1229,7 @@ msgstr ""
|
||||
#. module: website_forum
|
||||
#: selection:forum.post,state:0
|
||||
#: model:ir.ui.view,arch_db:website_forum.moderation_queue
|
||||
#: model:ir.ui.view,arch_db:website_forum.post_answers
|
||||
#: model:ir.ui.view,arch_db:website_forum.post_description_full
|
||||
msgid "Flagged"
|
||||
msgstr ""
|
||||
@@ -1591,6 +1603,11 @@ msgstr ""
|
||||
msgid "Links"
|
||||
msgstr ""
|
||||
|
||||
#. module: website_forum
|
||||
#: model:ir.ui.view,arch_db:website_forum.close_post
|
||||
msgid "Mark as offensive"
|
||||
msgstr ""
|
||||
|
||||
#. module: website_forum
|
||||
#: model:ir.model.fields,field_description:website_forum.field_forum_forum_karma_moderate
|
||||
msgid "Moderate posts"
|
||||
@@ -1757,6 +1774,11 @@ msgstr ""
|
||||
msgid "Offensive"
|
||||
msgstr ""
|
||||
|
||||
#. module: website_forum
|
||||
#: model:ir.ui.view,arch_db:website_forum.close_post
|
||||
msgid "Offensive Post"
|
||||
msgstr ""
|
||||
|
||||
#. module: website_forum
|
||||
#: model:ir.ui.view,arch_db:website_forum.post_answers
|
||||
msgid "On"
|
||||
|
||||
@@ -631,7 +631,10 @@
|
||||
<!-- Moderation: close a post -->
|
||||
<template id="close_post">
|
||||
<t t-call="website_forum.header">
|
||||
<h1 class="mt0"><t t-esc="offensive and 'Offensive' or 'Close'"/> Post</h1>
|
||||
<h1 class="mt0">
|
||||
<t t-if="offensive">Offensive Post</t>
|
||||
<t t-if="not offensive">Close Post</t>
|
||||
</h1>
|
||||
<p class="text-muted" t-if="not offensive">
|
||||
If you close this post, it will be hidden for most users. Only
|
||||
users having a high karma can see closed posts to moderate
|
||||
@@ -668,7 +671,10 @@
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-md-offset-3 col-md-8">
|
||||
<button type="submit" class="btn btn-primary"><t t-esc="offensive and 'Mark as offensive' or 'Close post'"/></button>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<t t-if="offensive">Mark as offensive</t>
|
||||
<t t-if="not offensive">Close post</t>
|
||||
</button>
|
||||
<span class="text-muted">or</span>
|
||||
<a class="btn btn-link" t-attf-href="/forum/#{ slug(forum) }/question/#{ slug(question) }">back to post</a>
|
||||
</div>
|
||||
@@ -1005,9 +1011,9 @@
|
||||
<a t-attf-style="cursor: pointer;#{not answer.can_flag and 'color:#999;' or ''}"
|
||||
t-attf-class="flag fa fa-flag #{not answer.can_flag and 'karma_required' or ''}"
|
||||
t-attf-data-karma="#{not answer.can_flag and answer.forum_id.karma_flag or 0}"
|
||||
t-attf-data-href="/forum/#{slug(forum)}/post/#{slug(answer)}/flag"
|
||||
>
|
||||
<t class="flag_flagged_label" t-esc="answer.state == 'flagged' and 'Flagged' or 'Flag'"/>
|
||||
t-attf-data-href="/forum/#{slug(forum)}/post/#{slug(answer)}/flag">
|
||||
<t t-if="answer.state == 'flagged'">Flagged</t>
|
||||
<t t-if="answer.state != 'flagged'">Flag</t>
|
||||
</a>
|
||||
</li>
|
||||
<li t-if="question.post_type == 'question'">
|
||||
|
||||
@@ -88,6 +88,7 @@ class WebsiteSaleBackend(WebsiteBackend):
|
||||
order_carts_abandoned_count=request.env['sale.order'].search_count(sale_order_domain + [
|
||||
('state', '=', 'draft'),
|
||||
('order_line', '!=', 'False'),
|
||||
('partner_id', '!=', request.env.ref('base.public_partner').id),
|
||||
('date_order', '<=', fields.Datetime.to_string(datetime.now() - timedelta(hours=1))),
|
||||
]),
|
||||
payment_to_capture_count=request.env['payment.transaction'].search_count([
|
||||
|
||||
@@ -7,8 +7,6 @@
|
||||
<field name="arch" type="xml">
|
||||
<search>
|
||||
<filter string="Confirmed Orders" name="order_confirmed" domain="[('state', 'in', ('sale', 'done'))]"/>
|
||||
<separator/>
|
||||
<filter string="From Website" name="from_website" domain="[('team_id.team_type', '=', 'website')]"/>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
@@ -17,8 +15,8 @@
|
||||
<field name="name">Orders</field>
|
||||
<field name="res_model">sale.order</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="domain">[]</field>
|
||||
<field name="context">{'show_sale': True, 'search_default_confirm_order': 1, 'search_default_from_website': 1}</field>
|
||||
<field name="domain">[('team_id.team_type', '=', 'website')]</field>
|
||||
<field name="context">{'show_sale': True, 'search_default_order_confirmed': 1}</field>
|
||||
<field name="search_view_id" ref="view_sales_order_filter_ecommerce"/>
|
||||
<field name="help">You don't have any confirmed order from the website.</field>
|
||||
</record>
|
||||
@@ -27,8 +25,8 @@
|
||||
<field name="name">Unpaid Orders</field>
|
||||
<field name="res_model">sale.order</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="domain">[('state', '=', 'sent')]</field>
|
||||
<field name="context">{'show_sale': True, 'search_default_from_website': 1, 'create': False}</field>
|
||||
<field name="domain">[('state', '=', 'sent'), ('team_id.team_type', '=', 'website')]</field>
|
||||
<field name="context">{'show_sale': True, 'create': False}</field>
|
||||
<field name="search_view_id" ref="view_sales_order_filter_ecommerce"/>
|
||||
<field name="help">You don't have any unpaid order from the website.</field>
|
||||
</record>
|
||||
@@ -37,8 +35,8 @@
|
||||
<field name="name">Abandoned Carts</field>
|
||||
<field name="res_model">sale.order</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="domain">[('state', '=', 'draft'), ('order_line', '!=', False), ('date_order', '<=', (context_today() - datetime.timedelta(hours=1)).strftime('%Y-%m-%d'))]</field>
|
||||
<field name="context">{'show_sale': True, 'search_default_from_website': 1, 'create': False}</field>
|
||||
<field name="domain">[('state', '=', 'draft'), ('order_line', '!=', False), ('partner_id','!=', context.get('public_partner_id')), ('team_id.team_type', '=', 'website'), ('date_order', '<=', (datetime.datetime.utcnow() - datetime.timedelta(hours=1)).strftime('%Y-%m-%d %H:%M:%S'))]</field>
|
||||
<field name="context" eval="{'show_sale': True, 'create': False, 'public_partner_id': ref('base.public_partner')}"/>
|
||||
<field name="search_view_id" ref="view_sales_order_filter_ecommerce"/>
|
||||
<field name="help" type="xml">
|
||||
<p>Find here all the abandoned carts, i.e. the carts generated by your website's visitors more than one hour ago and not confirmed.</p>
|
||||
@@ -49,8 +47,8 @@
|
||||
<field name="name">Orders To Invoice</field>
|
||||
<field name="res_model">sale.order</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="domain">[('state', 'in', ('sale', 'done')), ('invoice_status', '=', 'to invoice')]</field>
|
||||
<field name="context">{'show_sale': True, 'search_default_from_website': 1, 'create': False}</field>
|
||||
<field name="domain">[('state', 'in', ('sale', 'done')), ('invoice_status', '=', 'to invoice'), ('team_id.team_type', '=', 'website')]</field>
|
||||
<field name="context">{'show_sale': True, 'search_default_order_confirmed': 1, 'create': False}</field>
|
||||
<field name="search_view_id" ref="view_sales_order_filter_ecommerce"/>
|
||||
<field name="help">You don't have any order from the website.</field>
|
||||
</record>
|
||||
|
||||
Vendored
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 374 KiB |
Vendored
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 224 KiB |
Vendored
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 246 KiB |
@@ -1,9 +0,0 @@
|
||||
==================
|
||||
Business Mementoes
|
||||
==================
|
||||
|
||||
.. toctree::
|
||||
|
||||
Accounting Memento (US GAAP) <https://odoo.com/documentation/functional/accounting.html>
|
||||
Double-Entry Inventory <https://odoo.com/documentation/functional/double-entry.html>
|
||||
Inventory Valuations <https://odoo.com/documentation/functional/valuation.html>
|
||||
@@ -0,0 +1,9 @@
|
||||
Poland, 2017-06-25
|
||||
|
||||
I hereby agree to the terms of the Odoo Individual Contributor License Agreement v1.0.
|
||||
|
||||
I declare that I am authorized and able to make this agreement and sign this declaration.
|
||||
|
||||
Signed,
|
||||
|
||||
Marcin Pieczyński marcin-pieczynski@wp.pl https://github.com/piemar1
|
||||
@@ -0,0 +1,11 @@
|
||||
Belgium, 22/06/2017
|
||||
|
||||
I hereby agree to the terms of the Odoo Individual Contributor License
|
||||
Agreement v1.0.
|
||||
|
||||
I declare that I am authorized and able to make this agreement and sign this
|
||||
declaration.
|
||||
|
||||
Signed,
|
||||
|
||||
Ward Marissen ward.marissen@gmail.com https://github.com/wardm95
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user