[MERGE] forward port branch 12.0 up to 11b1e12cde
This commit is contained in:
@@ -580,8 +580,7 @@ class AccountJournal(models.Model):
|
||||
if self.company_id != self.env.ref('base.main_company'):
|
||||
alias_name += '-' + str(self.company_id.name)
|
||||
return {
|
||||
'alias_defaults': {'type': 'in_invoice'},
|
||||
'alias_user_id': self.env.user.id,
|
||||
'alias_defaults': {'type': 'in_invoice', 'company_id': self.company_id.id},
|
||||
'alias_parent_thread_id': self.id,
|
||||
'alias_name': re.sub(r'[^\w]+', '-', alias_name)
|
||||
}
|
||||
|
||||
@@ -1889,12 +1889,12 @@ class AccountInvoiceLine(models.Model):
|
||||
}
|
||||
return data
|
||||
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
if vals.get('display_type', self.default_get(['display_type'])['display_type']):
|
||||
vals.update(price_unit=0, account_id=False, quantity=0)
|
||||
|
||||
return super(AccountInvoiceLine, self).create(vals)
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
for vals in vals_list:
|
||||
if vals.get('display_type', self.default_get(['display_type'])['display_type']):
|
||||
vals.update(price_unit=0, account_id=False, quantity=0)
|
||||
return super(AccountInvoiceLine, self).create(vals_list)
|
||||
|
||||
@api.multi
|
||||
def write(self, values):
|
||||
|
||||
@@ -114,7 +114,8 @@ class AccountInvoiceReport(models.Model):
|
||||
ai.partner_bank_id,
|
||||
SUM ((invoice_type.sign_qty * ail.quantity) / u.factor * u2.factor) AS product_qty,
|
||||
SUM(ail.price_subtotal_signed * invoice_type.sign) AS price_total,
|
||||
SUM(ai.amount_total * invoice_type.sign) AS amount_total,
|
||||
(ai.amount_total * invoice_type.sign) / (SELECT count(*) FROM account_invoice_line l where invoice_id = ai.id) *
|
||||
count(*) * invoice_type.sign AS amount_total,
|
||||
SUM(ABS(ail.price_subtotal_signed)) / CASE
|
||||
WHEN SUM(ail.quantity / u.factor * u2.factor) <> 0::numeric
|
||||
THEN SUM(ail.quantity / u.factor * u2.factor)
|
||||
|
||||
@@ -142,7 +142,7 @@
|
||||
% set website_url = object.env['ir.config_parameter'].sudo().get_param('web.base.url')
|
||||
Your Odoo domain is: <b><a href='${website_url}'>${website_url}</a></b><br />
|
||||
Your sign in email is: <b><a href="/web/login?login=${object.email}" target="_blank">${object.email}</a></b><br /><br />
|
||||
Never heard of Odoo? It’s a all-in-one business software loved by 3+ million users. It will considerably improve your experience at work and increase your productivity.
|
||||
Never heard of Odoo? It’s an all-in-one business software loved by 3+ million users. It will considerably improve your experience at work and increase your productivity.
|
||||
<br /><br />
|
||||
Have a look at the <a href="https://www.odoo.com/page/tour?utm_source=db&utm_medium=auth" style="color: #875A7B;">Odoo Tour</a> to discover the tool.
|
||||
<br /><br />
|
||||
|
||||
@@ -70,13 +70,21 @@ class ResPartnerBank(models.Model):
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
if vals.get('acc_number'):
|
||||
vals['acc_number'] = pretty_iban(normalize_iban(vals['acc_number']))
|
||||
try:
|
||||
validate_iban(vals['acc_number'])
|
||||
vals['acc_number'] = pretty_iban(normalize_iban(vals['acc_number']))
|
||||
except ValidationError:
|
||||
pass
|
||||
return super(ResPartnerBank, self).create(vals)
|
||||
|
||||
@api.multi
|
||||
def write(self, vals):
|
||||
if vals.get('acc_number'):
|
||||
vals['acc_number'] = pretty_iban(normalize_iban(vals['acc_number']))
|
||||
try:
|
||||
validate_iban(vals['acc_number'])
|
||||
vals['acc_number'] = pretty_iban(normalize_iban(vals['acc_number']))
|
||||
except ValidationError:
|
||||
pass
|
||||
return super(ResPartnerBank, self).write(vals)
|
||||
|
||||
@api.one
|
||||
|
||||
@@ -277,6 +277,13 @@ class Lead(models.Model):
|
||||
if self.state_id:
|
||||
self.country_id = self.state_id.country_id.id
|
||||
|
||||
@api.onchange('country_id')
|
||||
def _onchange_country_id(self):
|
||||
res = {'domain': {'state_id': []}}
|
||||
if self.country_id:
|
||||
res['domain']['state_id'] = [('country_id', '=', self.country_id.id)]
|
||||
return res
|
||||
|
||||
# ----------------------------------------
|
||||
# ORM override (CRUD, fields_view_get, ...)
|
||||
# ----------------------------------------
|
||||
|
||||
@@ -48,13 +48,18 @@ class Team(models.Model):
|
||||
team.unassigned_leads_count = counts.get(team.id, 0)
|
||||
|
||||
def _compute_opportunities(self):
|
||||
opportunity_data = self.env['crm.lead'].read_group([
|
||||
opportunity_data = self.env['crm.lead'].search([
|
||||
('team_id', 'in', self.ids),
|
||||
('probability', '<', 100),
|
||||
('type', '=', 'opportunity'),
|
||||
], ['planned_revenue', 'team_id'], ['team_id'])
|
||||
counts = {datum['team_id'][0]: datum['team_id_count'] for datum in opportunity_data}
|
||||
amounts = {datum['team_id'][0]: (datum['planned_revenue']) for datum in opportunity_data}
|
||||
]).read(['planned_revenue', 'team_id'])
|
||||
counts = {}
|
||||
amounts = {}
|
||||
for datum in opportunity_data:
|
||||
counts.setdefault(datum['team_id'][0], 0)
|
||||
amounts.setdefault(datum['team_id'][0], 0)
|
||||
counts[datum['team_id'][0]] += 1
|
||||
amounts[datum['team_id'][0]] += (datum.get('planned_revenue', 0))
|
||||
for team in self:
|
||||
team.opportunities_count = counts.get(team.id, 0)
|
||||
team.opportunities_amount = amounts.get(team.id, 0)
|
||||
|
||||
@@ -277,7 +277,7 @@ class FleetVehicleLogContract(models.Model):
|
||||
'fleet.mail_act_fleet_contract_to_renew', contract.expiration_date,
|
||||
user_id=contract.user_id.id)
|
||||
|
||||
expired_contracts = self.search([('state', '!=', 'expired'), ('expiration_date', '<',fields.Date.today() )])
|
||||
expired_contracts = self.search([('state', 'not in', ['expired', 'closed']), ('expiration_date', '<',fields.Date.today() )])
|
||||
expired_contracts.write({'state': 'expired'})
|
||||
|
||||
futur_contracts = self.search([('state', 'not in', ['futur', 'closed']), ('start_date', '>', fields.Date.today())])
|
||||
|
||||
@@ -47,6 +47,7 @@ A synchronization with an internal agenda (Meetings of the CRM module) is also p
|
||||
'report/hr_leave_reports.xml',
|
||||
|
||||
'views/hr_views.xml',
|
||||
'views/hr_leave_template.xml',
|
||||
],
|
||||
'demo': [
|
||||
'data/hr_holidays_demo.xml',
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
@include media-breakpoint-down(sm) {
|
||||
.o_hr_holidays_dates {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<template id="assets_backend" inherit_id="web.assets_backend">
|
||||
<xpath expr="link[last()]" position="after">
|
||||
<link rel="stylesheet" type="text/scss" href="/hr_holidays/static/scss/hr_leave_mobile.scss"/>
|
||||
</xpath>
|
||||
</template>
|
||||
</odoo>
|
||||
@@ -171,7 +171,7 @@
|
||||
<div>
|
||||
<field name="date_from" invisible="1"/>
|
||||
<field name="date_to" invisible="1"/>
|
||||
<div class="o_row o_row_readonly">
|
||||
<div class="o_row o_row_readonly o_hr_holidays_dates">
|
||||
<field name="request_date_from" class="oe_inline"
|
||||
attrs="{'readonly': [('state', 'not in', ('draft', 'confirm'))]}"/>
|
||||
<span class="oe_inline"
|
||||
|
||||
@@ -148,7 +148,7 @@ class AccountAnalyticLine(models.Model):
|
||||
:return: a dictionary mapping each record id to its corresponding
|
||||
dictionnary values to write (may be empty).
|
||||
"""
|
||||
result = dict.fromkeys(self.ids, dict())
|
||||
result = {id_: {} for id_ in self.ids}
|
||||
sudo_self = self.sudo() # this creates only one env for all operation that required sudo()
|
||||
# (re)compute the amount (depending on unit_amount, employee_id for the cost, and account_id for currency)
|
||||
if any([field_name in values for field_name in ['unit_amount', 'employee_id', 'account_id']]):
|
||||
|
||||
@@ -232,3 +232,35 @@ class TestTimesheet(TestCommonTimesheet):
|
||||
self.task1.write({
|
||||
'project_id': False
|
||||
})
|
||||
|
||||
def test_recompute_amount_for_multiple_timesheets(self):
|
||||
""" Check that amount is recomputed correctly when setting unit_amount for multiple timesheets at once. """
|
||||
Timesheet = self.env['account.analytic.line']
|
||||
self.empl_employee.timesheet_cost = 5.0
|
||||
self.empl_employee2.timesheet_cost = 6.0
|
||||
# create a timesheet for each employee
|
||||
timesheet_1 = Timesheet.sudo(self.user_employee).create({
|
||||
'project_id': self.project_customer.id,
|
||||
'task_id': self.task1.id,
|
||||
'name': '/',
|
||||
'unit_amount': 1,
|
||||
})
|
||||
timesheet_2 = Timesheet.sudo(self.user_employee2).create({
|
||||
'project_id': self.project_customer.id,
|
||||
'task_id': self.task1.id,
|
||||
'name': '/',
|
||||
'unit_amount': 1,
|
||||
})
|
||||
timesheets = timesheet_1 + timesheet_2
|
||||
|
||||
# increase unit_amount to trigger amount recomputation
|
||||
timesheets.sudo().write({
|
||||
'unit_amount': 2,
|
||||
})
|
||||
|
||||
# since timesheet costs are different for both employees, we should get different amounts
|
||||
self.assertRecordValues(timesheets, [{
|
||||
'amount': -10.0,
|
||||
}, {
|
||||
'amount': -12.0,
|
||||
}])
|
||||
|
||||
@@ -13,8 +13,8 @@ Bu modül kurulduktan sonra, Muhasebe yapılandırma sihirbazı çalışır
|
||||
* Sihirbaz sizden hesap planı şablonu, planın kurulacağı şirket, banka hesap
|
||||
bilgileriniz, ilgili para birimi gibi bilgiler isteyecek.
|
||||
""",
|
||||
'author': 'Ahmet Altınışık',
|
||||
'maintainer':'https://launchpad.net/~openerp-turkey',
|
||||
'author': 'Ahmet Altınışık, Can Tecim',
|
||||
'maintainer':'https://launchpad.net/~openerp-turkey, http://www.cantecim.com',
|
||||
'depends': [
|
||||
'account',
|
||||
],
|
||||
@@ -25,5 +25,6 @@ Bu modül kurulduktan sonra, Muhasebe yapılandırma sihirbazı çalışır
|
||||
'data/account_data.xml',
|
||||
'data/account_tax_template_data.xml',
|
||||
'data/account_chart_template_data.xml',
|
||||
'data/res.country.state.csv'
|
||||
],
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
"id","country_id:id","name","code"
|
||||
state_tr_01,base.tr,"Adana","01"
|
||||
state_tr_02,base.tr,"Adıyaman","02"
|
||||
state_tr_03,base.tr,"Afyon","03"
|
||||
state_tr_04,base.tr,"Ağrı","04"
|
||||
state_tr_05,base.tr,"Amasya","05"
|
||||
state_tr_06,base.tr,"Ankara","06"
|
||||
state_tr_07,base.tr,"Antalya","07"
|
||||
state_tr_08,base.tr,"Artvin","08"
|
||||
state_tr_09,base.tr,"Aydın","09"
|
||||
state_tr_10,base.tr,"Balıkesir","10"
|
||||
state_tr_11,base.tr,"Bilecik","11"
|
||||
state_tr_12,base.tr,"Bingöl","12"
|
||||
state_tr_13,base.tr,"Bitlis","13"
|
||||
state_tr_14,base.tr,"Bolu","14"
|
||||
state_tr_15,base.tr,"Burdur","15"
|
||||
state_tr_16,base.tr,"Bursa","16"
|
||||
state_tr_17,base.tr,"Çanakkale","17"
|
||||
state_tr_18,base.tr,"Çankırı","18"
|
||||
state_tr_19,base.tr,"Çorum","19"
|
||||
state_tr_20,base.tr,"Denizli","20"
|
||||
state_tr_21,base.tr,"Diyarbakır","21"
|
||||
state_tr_22,base.tr,"Edirne","22"
|
||||
state_tr_23,base.tr,"Elazığ","23"
|
||||
state_tr_24,base.tr,"Erzincan","24"
|
||||
state_tr_25,base.tr,"Erzurum","25"
|
||||
state_tr_26,base.tr,"Eskişehir","26"
|
||||
state_tr_27,base.tr,"Gaziantep","27"
|
||||
state_tr_28,base.tr,"Giresun","28"
|
||||
state_tr_29,base.tr,"Gümüşhane","29"
|
||||
state_tr_30,base.tr,"Hakkari","30"
|
||||
state_tr_31,base.tr,"Hatay","31"
|
||||
state_tr_32,base.tr,"Isparta","32"
|
||||
state_tr_33,base.tr,"İçel","33"
|
||||
state_tr_34,base.tr,"İstanbul","34"
|
||||
state_tr_35,base.tr,"İzmir","35"
|
||||
state_tr_36,base.tr,"Kars","36"
|
||||
state_tr_37,base.tr,"Kastamonu","37"
|
||||
state_tr_38,base.tr,"Kayseri","38"
|
||||
state_tr_39,base.tr,"Kırklareli","39"
|
||||
state_tr_40,base.tr,"Kırşehir","40"
|
||||
state_tr_41,base.tr,"Kocaeli","41"
|
||||
state_tr_42,base.tr,"Konya","42"
|
||||
state_tr_43,base.tr,"Kütahya","43"
|
||||
state_tr_44,base.tr,"Malatya","44"
|
||||
state_tr_45,base.tr,"Manisa","45"
|
||||
state_tr_46,base.tr,"K.maraş","46"
|
||||
state_tr_47,base.tr,"Mardin","47"
|
||||
state_tr_48,base.tr,"Muğla","48"
|
||||
state_tr_49,base.tr,"Muş","49"
|
||||
state_tr_50,base.tr,"Nevşehir","50"
|
||||
state_tr_51,base.tr,"Niğde","51"
|
||||
state_tr_52,base.tr,"Ordu","52"
|
||||
state_tr_53,base.tr,"Rize","53"
|
||||
state_tr_54,base.tr,"Sakarya","54"
|
||||
state_tr_55,base.tr,"Samsun","55"
|
||||
state_tr_56,base.tr,"Siirt","56"
|
||||
state_tr_57,base.tr,"Sinop","57"
|
||||
state_tr_58,base.tr,"Sivas","58"
|
||||
state_tr_59,base.tr,"Tekirdağ","59"
|
||||
state_tr_60,base.tr,"Tokat","60"
|
||||
state_tr_61,base.tr,"Trabzon","61"
|
||||
state_tr_62,base.tr,"Tunceli","62"
|
||||
state_tr_63,base.tr,"Şanlıurfa","63"
|
||||
state_tr_64,base.tr,"Uşak","64"
|
||||
state_tr_65,base.tr,"Van","65"
|
||||
state_tr_66,base.tr,"Yozgat","66"
|
||||
state_tr_67,base.tr,"Zonguldak","67"
|
||||
state_tr_68,base.tr,"Aksaray","68"
|
||||
state_tr_69,base.tr,"Bayburt","69"
|
||||
state_tr_70,base.tr,"Karaman","70"
|
||||
state_tr_71,base.tr,"Kırıkkale","71"
|
||||
state_tr_72,base.tr,"Batman","72"
|
||||
state_tr_73,base.tr,"Şırnak","73"
|
||||
state_tr_74,base.tr,"Bartın","74"
|
||||
state_tr_75,base.tr,"Ardahan","75"
|
||||
state_tr_76,base.tr,"Iğdır","76"
|
||||
state_tr_77,base.tr,"Yalova","77"
|
||||
state_tr_78,base.tr,"Karabük","78"
|
||||
state_tr_79,base.tr,"Kilis","79"
|
||||
state_tr_80,base.tr,"Osmaniye","80"
|
||||
state_tr_81,base.tr,"Düzce","81"
|
||||
|
@@ -433,19 +433,21 @@ class MailActivity(models.Model):
|
||||
activity_domain.append(('res_id', 'in', res.ids))
|
||||
grouped_activities = self.env['mail.activity'].read_group(
|
||||
activity_domain,
|
||||
['res_id', 'activity_type_id', 'res_name:max(res_name)', 'ids:array_agg(id)', 'date_deadline:min(date_deadline)'],
|
||||
['res_id', 'activity_type_id', 'ids:array_agg(id)', 'date_deadline:min(date_deadline)'],
|
||||
['res_id', 'activity_type_id'],
|
||||
lazy=False)
|
||||
# filter out unreadable records
|
||||
if not domain:
|
||||
res_ids = tuple(a['res_id'] for a in grouped_activities)
|
||||
res = self.env[res_model].search([('id', 'in', res_ids)])
|
||||
grouped_activities = [a for a in grouped_activities if a['res_id'] in res.ids]
|
||||
activity_type_ids = self.env['mail.activity.type']
|
||||
res_id_to_name = {}
|
||||
res_id_to_deadline = {}
|
||||
activity_data = defaultdict(dict)
|
||||
for group in grouped_activities:
|
||||
res_id = group['res_id']
|
||||
res_name = group['res_name']
|
||||
activity_type_id = group['activity_type_id'][0]
|
||||
activity_type_ids |= self.env['mail.activity.type'].browse(activity_type_id) # we will get the name when reading mail_template_ids
|
||||
res_id_to_name[res_id] = res_name
|
||||
res_id_to_deadline[res_id] = group['date_deadline'] if (res_id not in res_id_to_deadline or group['date_deadline'] < res_id_to_deadline[res_id]) else res_id_to_deadline[res_id]
|
||||
state = self._compute_state_from_date(group['date_deadline'], self.user_id.sudo().tz)
|
||||
activity_data[res_id][activity_type_id] = {
|
||||
@@ -455,6 +457,7 @@ class MailActivity(models.Model):
|
||||
'o_closest_deadline': group['date_deadline'],
|
||||
}
|
||||
res_ids_sorted = sorted(res_id_to_deadline, key=lambda item: res_id_to_deadline[item])
|
||||
res_id_to_name = dict(self.env[res_model].browse(res_ids_sorted).name_get())
|
||||
activity_type_infos = []
|
||||
for elem in sorted(activity_type_ids, key=lambda item: item.sequence):
|
||||
mail_template_info = []
|
||||
|
||||
@@ -18,6 +18,31 @@ from odoo.tools.pycompat import to_text
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
# https://docs.adyen.com/developers/development-resources/currency-codes
|
||||
CURRENCY_CODE_MAPS = {
|
||||
"BHD": 3,
|
||||
"CVE": 0,
|
||||
"DJF": 0,
|
||||
"GNF": 0,
|
||||
"IDR": 0,
|
||||
"JOD": 3,
|
||||
"JPY": 0,
|
||||
"KMF": 0,
|
||||
"KRW": 0,
|
||||
"KWD": 3,
|
||||
"LYD": 3,
|
||||
"OMR": 3,
|
||||
"PYG": 0,
|
||||
"RWF": 0,
|
||||
"TND": 3,
|
||||
"UGX": 0,
|
||||
"VND": 0,
|
||||
"VUV": 0,
|
||||
"XAF": 0,
|
||||
"XOF": 0,
|
||||
"XPF": 0,
|
||||
}
|
||||
|
||||
|
||||
class AcquirerAdyen(models.Model):
|
||||
_inherit = 'payment.acquirer'
|
||||
@@ -27,6 +52,16 @@ class AcquirerAdyen(models.Model):
|
||||
adyen_skin_code = fields.Char('Skin Code', required_if_provider='adyen', groups='base.group_user')
|
||||
adyen_skin_hmac_key = fields.Char('Skin HMAC Key', required_if_provider='adyen', groups='base.group_user')
|
||||
|
||||
@api.model
|
||||
def _adyen_convert_amount(self, amount, currency):
|
||||
"""
|
||||
Adyen requires the amount to be multiplied by 10^k,
|
||||
where k depends on the currency code.
|
||||
"""
|
||||
k = CURRENCY_CODE_MAPS.get(currency.name, 2)
|
||||
paymentAmount = int(tools.float_round(amount, k) * (10**k))
|
||||
return paymentAmount
|
||||
|
||||
@api.model
|
||||
def _get_adyen_urls(self, environment):
|
||||
""" Adyen URLs: yhpp: hosted payment page: pay.shtml for single, select.shtml for multiple """
|
||||
@@ -120,12 +155,13 @@ class AcquirerAdyen(models.Model):
|
||||
import datetime
|
||||
from dateutil import relativedelta
|
||||
|
||||
paymentAmount = self._adyen_convert_amount(values['amount'], values['currency'])
|
||||
if self.provider == 'adyen' and len(self.adyen_skin_hmac_key) == 64:
|
||||
tmp_date = datetime.datetime.today() + relativedelta.relativedelta(days=1)
|
||||
|
||||
values.update({
|
||||
'merchantReference': values['reference'],
|
||||
'paymentAmount': '%d' % int(tools.float_round(values['amount'], 2) * 100),
|
||||
'paymentAmount': '%d' % paymentAmount,
|
||||
'currencyCode': values['currency'] and values['currency'].name or '',
|
||||
'shipBeforeDate': tmp_date.strftime('%Y-%m-%d'),
|
||||
'skinCode': self.adyen_skin_code,
|
||||
@@ -143,7 +179,7 @@ class AcquirerAdyen(models.Model):
|
||||
|
||||
values.update({
|
||||
'merchantReference': values['reference'],
|
||||
'paymentAmount': '%d' % int(tools.float_round(values['amount'], 2) * 100),
|
||||
'paymentAmount': '%d' % paymentAmount,
|
||||
'currencyCode': values['currency'] and values['currency'].name or '',
|
||||
'shipBeforeDate': tmp_date,
|
||||
'skinCode': self.adyen_skin_code,
|
||||
|
||||
@@ -21,9 +21,13 @@ odoo.define('payment_stripe.stripe', function(require) {
|
||||
$.blockUI.defaults.css["background-color"] = '';
|
||||
$.blockUI.defaults.overlayCSS["opacity"] = '0.9';
|
||||
}
|
||||
var stripeHandler;
|
||||
function getStripeHandler()
|
||||
{
|
||||
var handler = StripeCheckout.configure({
|
||||
if (stripeHandler) {
|
||||
return stripeHandler;
|
||||
}
|
||||
var handler = stripeHandler = StripeCheckout.configure({
|
||||
key: $("input[name='stripe_key']").val(),
|
||||
image: $("input[name='stripe_image']").val(),
|
||||
locale: 'auto',
|
||||
|
||||
@@ -21,7 +21,12 @@
|
||||
<input type="hidden" name='email' t-att-value='email'/>
|
||||
<input id="acquirer_stripe" type="hidden" name="acquirer" t-att-value="acquirer.id"/>
|
||||
<input type="hidden" name="stripe_key" t-att-value="acquirer.stripe_publishable_key"/>
|
||||
<script type="text/javascript" src="/payment_stripe/static/src/js/stripe.js"></script>
|
||||
<script type="text/javascript">
|
||||
odoo.define(function (require) {
|
||||
var ajax = require('web.ajax');
|
||||
ajax.loadJS("/payment_stripe/static/src/js/stripe.js");
|
||||
});
|
||||
</script>
|
||||
</template>
|
||||
|
||||
<template id="stripe_s2s_form">
|
||||
|
||||
@@ -141,16 +141,16 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h2>IotBox / Hardware Proxy</h2>
|
||||
<h2>IoT Box</h2>
|
||||
<div class="row mt16 o_settings_container" id="posbox_reference">
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="is_posbox"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="is_posbox"/>
|
||||
<label for="is_posbox" string="IoT Box"/>
|
||||
<div class="text-muted">
|
||||
Use an integrated hardware setup like <a href="https://www.odoo.com/documentation/user/online/iot/pos.html" target="_blank">IoT Box</a>
|
||||
Connect devices to your PoS (ticket printer, barcode scanner, scale, etc.)
|
||||
</div>
|
||||
<div class="content-group" attrs="{'invisible' : [('is_posbox', '=', False)]}">
|
||||
<div class="row mt16">
|
||||
|
||||
@@ -69,7 +69,6 @@
|
||||
<field name="image_small"/>
|
||||
<field name="lst_price"/>
|
||||
<field name="product_variant_count"/>
|
||||
<field name="product_variant_ids"/>
|
||||
<field name="currency_id"/>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
|
||||
@@ -38,7 +38,7 @@ class PurchaseOrder(models.Model):
|
||||
else:
|
||||
self.origin = requisition.name
|
||||
self.notes = requisition.description
|
||||
self.date_order = requisition.date_end or fields.Datetime.now()
|
||||
self.date_order = fields.Datetime.now()
|
||||
self.picking_type_id = requisition.picking_type_id.id
|
||||
|
||||
if requisition.type_id.line_copy != 'copy':
|
||||
|
||||
@@ -46,7 +46,8 @@ class AccountInvoice(models.Model):
|
||||
def _onchange_delivery_address(self):
|
||||
addr = self.partner_id.address_get(['delivery'])
|
||||
self.partner_shipping_id = addr and addr.get('delivery')
|
||||
if self.env.context.get('type', 'out_invoice') == 'out_invoice':
|
||||
inv_type = self.type or self.env.context.get('type', 'out_invoice')
|
||||
if inv_type == 'out_invoice':
|
||||
company = self.company_id or self.env.user.company_id
|
||||
self.comment = company.with_context(lang=self.partner_id.lang).sale_note
|
||||
|
||||
|
||||
@@ -505,6 +505,8 @@ class SaleOrder(models.Model):
|
||||
# We only want to create sections that have at least one invoiceable line
|
||||
pending_section = None
|
||||
|
||||
# Create lines in batch to avoid performance problems
|
||||
line_vals_list = []
|
||||
for line in order.order_line:
|
||||
if line.display_type == 'line_section':
|
||||
pending_section = line
|
||||
@@ -526,14 +528,21 @@ class SaleOrder(models.Model):
|
||||
|
||||
if line.qty_to_invoice > 0 or (line.qty_to_invoice < 0 and final):
|
||||
if pending_section:
|
||||
pending_section.invoice_line_create(invoices[group_key].id, pending_section.qty_to_invoice)
|
||||
line_vals_list.extend(pending_section.invoice_line_create_vals(
|
||||
invoices[group_key].id,
|
||||
pending_section.qty_to_invoice
|
||||
))
|
||||
pending_section = None
|
||||
line.invoice_line_create(invoices[group_key].id, line.qty_to_invoice)
|
||||
line_vals_list.extend(line.invoice_line_create_vals(
|
||||
invoices[group_key].id, line.qty_to_invoice
|
||||
))
|
||||
|
||||
if references.get(invoices.get(group_key)):
|
||||
if order not in references[invoices[group_key]]:
|
||||
references[invoices[group_key]] |= order
|
||||
|
||||
self.env['account.invoice.line'].create(line_vals_list)
|
||||
|
||||
for group_key in invoices:
|
||||
invoices[group_key].write({'name': ', '.join(invoices_name[group_key]),
|
||||
'origin': ', '.join(invoices_origin[group_key])})
|
||||
@@ -1346,18 +1355,34 @@ class SaleOrderLine(models.Model):
|
||||
@api.multi
|
||||
def invoice_line_create(self, invoice_id, qty):
|
||||
""" Create an invoice line. The quantity to invoice can be positive (invoice) or negative (refund).
|
||||
|
||||
.. deprecated:: 12.0
|
||||
Replaced by :func:`invoice_line_create_vals` which can be used for creating
|
||||
`account.invoice.line` records in batch
|
||||
|
||||
:param invoice_id: integer
|
||||
:param qty: float quantity to invoice
|
||||
:returns recordset of account.invoice.line created
|
||||
"""
|
||||
invoice_lines = self.env['account.invoice.line']
|
||||
return self.env['account.invoice.line'].create(
|
||||
self.invoice_line_create_vals(invoice_id, qty))
|
||||
|
||||
def invoice_line_create_vals(self, invoice_id, qty):
|
||||
""" Create an invoice line. The quantity to invoice can be positive (invoice) or negative
|
||||
(refund).
|
||||
|
||||
:param invoice_id: integer
|
||||
:param qty: float quantity to invoice
|
||||
:returns list of dict containing creation values for account.invoice.line records
|
||||
"""
|
||||
vals_list = []
|
||||
precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
|
||||
for line in self:
|
||||
if not float_is_zero(qty, precision_digits=precision) or not line.product_id:
|
||||
vals = line._prepare_invoice_line(qty=qty)
|
||||
vals.update({'invoice_id': invoice_id, 'sale_line_ids': [(6, 0, [line.id])]})
|
||||
invoice_lines |= self.env['account.invoice.line'].create(vals)
|
||||
return invoice_lines
|
||||
vals_list.append(vals)
|
||||
return vals_list
|
||||
|
||||
@api.multi
|
||||
def _prepare_procurement_values(self, group_id=False):
|
||||
|
||||
@@ -116,14 +116,14 @@
|
||||
</td>
|
||||
</t>
|
||||
<t t-if="line.display_type == 'line_section'">
|
||||
<td t-att-colspan="colspan">
|
||||
<td t-att-colspan="99">
|
||||
<span t-field="line.name"/>
|
||||
</td>
|
||||
<t t-set="current_section" t-value="line"/>
|
||||
<t t-set="current_subtotal" t-value="0"/>
|
||||
</t>
|
||||
<t t-if="line.display_type == 'line_note'">
|
||||
<td t-att-colspan="colspan">
|
||||
<td t-att-colspan="99">
|
||||
<span t-field="line.name"/>
|
||||
</td>
|
||||
</t>
|
||||
@@ -131,7 +131,7 @@
|
||||
|
||||
<t t-if="current_section and (line_last or doc.order_line[line_index+1].display_type == 'line_section')">
|
||||
<tr class="is-subtotal text-right">
|
||||
<td t-att-colspan="colspan">
|
||||
<td t-att-colspan="99">
|
||||
<strong class="mr16">Subtotal</strong>
|
||||
<span
|
||||
t-esc="current_subtotal"
|
||||
|
||||
@@ -161,7 +161,7 @@
|
||||
<t t-if="sale_order.remaining_validity_days > 1">days</t><t t-else="">day</t>
|
||||
</div>
|
||||
</li>
|
||||
<li t-if="sale_order.amount_undiscounted > sale_order.amount_untaxed" class="list-group-item flex-grow-1">
|
||||
<li t-if="sale_order.amount_undiscounted - sale_order.amount_untaxed > 0.01" class="list-group-item flex-grow-1">
|
||||
<small><b class="text-muted">Your advantage</b></small>
|
||||
<small>
|
||||
<b t-field="sale_order.amount_undiscounted"
|
||||
|
||||
@@ -39,7 +39,7 @@ var ReportWidget = Widget.extend({
|
||||
this.do_action({
|
||||
type: 'ir.actions.client',
|
||||
tag: 'stock_report_generic',
|
||||
name: $el.data('lot_name'),
|
||||
name: $el.data('lot_name') !== undefined && $el.data('lot_name').toString(),
|
||||
context: {
|
||||
active_id : $el.data('lot_id'),
|
||||
active_model : 'stock.production.lot',
|
||||
|
||||
@@ -359,7 +359,7 @@
|
||||
<filter name="done" string="Done" domain="[('state','=','done')]" help="Pickings already processed"/>
|
||||
<filter name="cancel" string="Cancelled" domain="[('state','=', 'cancel')]" help="Cancelled Moves"/>
|
||||
<separator/>
|
||||
<filter name="late" string="Late" domain="[('scheduled_date','<', time.strftime('%%Y-%%m-%%d %%H:%%M:%%S'))]" help="Pickings that are late on scheduled time"/>
|
||||
<filter name="late" string="Late" domain="[('scheduled_date','<', time.strftime('%%Y-%%m-%%d %%H:%%M:%%S')), ('state', 'in', ('assigned', 'waiting', 'confirmed'))]" help="Pickings that are late on scheduled time"/>
|
||||
<separator/>
|
||||
<filter name="backorder" string="Backorders" domain="[('backorder_id','!=', False)]" help="Remaining parts of picking partially processed"/>
|
||||
<separator/>
|
||||
|
||||
@@ -13,7 +13,7 @@ class SaleOrderLine(models.Model):
|
||||
def _get_qty_procurement(self):
|
||||
# People without purchase rights should be able to do this operation
|
||||
purchase_lines_sudo = self.sudo().purchase_line_ids
|
||||
if not self.move_ids.filtered(lambda r: r.state != 'cancel') and purchase_lines_sudo.filtered(lambda r: r.state != 'cancel'):
|
||||
if purchase_lines_sudo.filtered(lambda r: r.state != 'cancel'):
|
||||
qty = 0.0
|
||||
for po_line in purchase_lines_sudo.filtered(lambda r: r.state != 'cancel'):
|
||||
qty += po_line.product_uom._compute_quantity(po_line.product_qty, self.product_uom, rounding_method='HALF-UP')
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Picking Reference</th>
|
||||
<th>Barcode</th>
|
||||
<th>Status</th>
|
||||
<th>Commitment Date</th>
|
||||
<th>Scheduled Date</th>
|
||||
@@ -30,6 +31,9 @@
|
||||
<td>
|
||||
<span t-field="pick.name"/>
|
||||
</td>
|
||||
<td>
|
||||
<img t-att-src="'/report/barcode/?type=%s&value=%s&width=%s&height=%s' % ('Code128', pick.name, 300, 50)" alt="Barcode"/>
|
||||
</td>
|
||||
<td>
|
||||
<span t-field="pick.state"/>
|
||||
</td>
|
||||
|
||||
@@ -502,7 +502,7 @@ class TestMailgateway(BaseFunctionalTest, MockEmails):
|
||||
self.assertEqual(msg.model, False,
|
||||
'message_post: private discussion: parameter model not correctly ignored when having no res_id')
|
||||
# Test: message-id
|
||||
self.assertIn('openerp-private', msg.message_id, 'message_post: private discussion: message-id should contain the private keyword')
|
||||
self.assertIn('openerp-private', msg.message_id.split('@')[0], 'message_post: private discussion: message-id should contain the private keyword')
|
||||
|
||||
# Do: Bert replies through mailgateway (is a customer)
|
||||
self.format_and_process(
|
||||
@@ -530,9 +530,9 @@ class TestMailgateway(BaseFunctionalTest, MockEmails):
|
||||
@mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models', 'odoo.addons.mail.models.mail_mail')
|
||||
def test_forward_parent_id(self):
|
||||
msg = self.test_record.sudo(self.user_employee).message_post(no_auto_thread=True, subtype='mail.mt_comment')
|
||||
self.assertNotIn(msg.model, msg.message_id)
|
||||
self.assertNotIn('-%d-' % msg.res_id, msg.message_id)
|
||||
self.assertIn('reply_to', msg.message_id)
|
||||
self.assertNotIn(msg.model, msg.message_id.split('@')[0])
|
||||
self.assertNotIn('-%d-' % msg.res_id, msg.message_id.split('@')[0])
|
||||
self.assertIn('reply_to', msg.message_id.split('@')[0])
|
||||
|
||||
# forward it to a new thread AND an existing thread
|
||||
fw_msg_id = '<THIS.IS.A.FW.MESSAGE.1@bert.fr>'
|
||||
|
||||
@@ -31,7 +31,7 @@ class TestMessageValues(common.BaseFunctionalTest, common.MockEmails):
|
||||
'reply_to': 'test.reply@example.com',
|
||||
'email_from': 'test.from@example.com',
|
||||
})
|
||||
self.assertIn('-private', msg.message_id, 'mail_message: message_id for a void message should be a "private" one')
|
||||
self.assertIn('-private', msg.message_id.split('@')[0], 'mail_message: message_id for a void message should be a "private" one')
|
||||
self.assertEqual(msg.reply_to, 'test.reply@example.com')
|
||||
self.assertEqual(msg.email_from, 'test.from@example.com')
|
||||
|
||||
@@ -39,7 +39,7 @@ class TestMessageValues(common.BaseFunctionalTest, common.MockEmails):
|
||||
self.env['ir.config_parameter'].search([('key', '=', 'mail.catchall.domain')]).unlink()
|
||||
|
||||
msg = self.Message.create({})
|
||||
self.assertIn('-private', msg.message_id, 'mail_message: message_id for a void message should be a "private" one')
|
||||
self.assertIn('-private', msg.message_id.split('@')[0], 'mail_message: message_id for a void message should be a "private" one')
|
||||
self.assertEqual(msg.reply_to, '%s <%s>' % (self.user_employee.name, self.user_employee.email))
|
||||
self.assertEqual(msg.email_from, '%s <%s>' % (self.user_employee.name, self.user_employee.email))
|
||||
|
||||
@@ -50,7 +50,7 @@ class TestMessageValues(common.BaseFunctionalTest, common.MockEmails):
|
||||
self.env['ir.config_parameter'].search([('key', '=', 'mail.catchall.alias')]).unlink()
|
||||
|
||||
msg = self.Message.create({})
|
||||
self.assertIn('-private', msg.message_id, 'mail_message: message_id for a void message should be a "private" one')
|
||||
self.assertIn('-private', msg.message_id.split('@')[0], 'mail_message: message_id for a void message should be a "private" one')
|
||||
self.assertEqual(msg.reply_to, '%s <%s>' % (self.user_employee.name, self.user_employee.email))
|
||||
self.assertEqual(msg.email_from, '%s <%s>' % (self.user_employee.name, self.user_employee.email))
|
||||
|
||||
@@ -61,7 +61,7 @@ class TestMessageValues(common.BaseFunctionalTest, common.MockEmails):
|
||||
self.env['ir.config_parameter'].set_param('mail.catchall.alias', alias_catchall)
|
||||
|
||||
msg = self.Message.create({})
|
||||
self.assertIn('-private', msg.message_id, 'mail_message: message_id for a void message should be a "private" one')
|
||||
self.assertIn('-private', msg.message_id.split('@')[0], 'mail_message: message_id for a void message should be a "private" one')
|
||||
reply_to_name = self.env.user.company_id.name
|
||||
reply_to_email = '%s@%s' % (alias_catchall, alias_domain)
|
||||
self.assertEqual(msg.reply_to, formataddr((reply_to_name, reply_to_email)))
|
||||
@@ -74,7 +74,7 @@ class TestMessageValues(common.BaseFunctionalTest, common.MockEmails):
|
||||
'model': 'mail.test',
|
||||
'res_id': self.alias_record.id
|
||||
})
|
||||
self.assertIn('-openerp-%d-mail.test' % self.alias_record.id, msg.message_id)
|
||||
self.assertIn('-openerp-%d-mail.test' % self.alias_record.id, msg.message_id.split('@')[0])
|
||||
self.assertEqual(msg.reply_to, '%s <%s>' % (self.user_employee.name, self.user_employee.email))
|
||||
self.assertEqual(msg.email_from, '%s <%s>' % (self.user_employee.name, self.user_employee.email))
|
||||
|
||||
@@ -88,7 +88,7 @@ class TestMessageValues(common.BaseFunctionalTest, common.MockEmails):
|
||||
'model': 'mail.test',
|
||||
'res_id': self.alias_record.id
|
||||
})
|
||||
self.assertIn('-openerp-%d-mail.test' % self.alias_record.id, msg.message_id)
|
||||
self.assertIn('-openerp-%d-mail.test' % self.alias_record.id, msg.message_id.split('@')[0])
|
||||
reply_to_name = '%s %s' % (self.env.user.company_id.name, self.alias_record.name)
|
||||
reply_to_email = '%s@%s' % (self.alias_record.alias_name, alias_domain)
|
||||
self.assertEqual(msg.reply_to, formataddr((reply_to_name, reply_to_email)))
|
||||
@@ -104,7 +104,7 @@ class TestMessageValues(common.BaseFunctionalTest, common.MockEmails):
|
||||
'model': 'mail.test',
|
||||
'res_id': self.alias_record.id
|
||||
})
|
||||
self.assertIn('-openerp-%d-mail.test' % self.alias_record.id, msg.message_id)
|
||||
self.assertIn('-openerp-%d-mail.test' % self.alias_record.id, msg.message_id.split('@')[0])
|
||||
reply_to_name = '%s %s' % (self.env.user.company_id.name, self.alias_record.name)
|
||||
reply_to_email = '%s@%s' % (self.alias_record.alias_name, alias_domain)
|
||||
self.assertEqual(msg.reply_to, formataddr((reply_to_name, reply_to_email)))
|
||||
@@ -116,9 +116,9 @@ class TestMessageValues(common.BaseFunctionalTest, common.MockEmails):
|
||||
'res_id': self.alias_record.id,
|
||||
'no_auto_thread': True,
|
||||
})
|
||||
self.assertIn('reply_to', msg.message_id)
|
||||
self.assertNotIn('mail.test', msg.message_id)
|
||||
self.assertNotIn('-%d-' % self.alias_record.id, msg.message_id)
|
||||
self.assertIn('reply_to', msg.message_id.split('@')[0])
|
||||
self.assertNotIn('mail.test', msg.message_id.split('@')[0])
|
||||
self.assertNotIn('-%d-' % self.alias_record.id, msg.message_id.split('@')[0])
|
||||
|
||||
def test_mail_message_base64_image(self):
|
||||
msg = self.env['mail.message'].sudo(self.user_employee).create({
|
||||
|
||||
@@ -19,6 +19,10 @@ var QuickCreate = require('web.CalendarQuickCreate');
|
||||
var _t = core._t;
|
||||
var QWeb = core.qweb;
|
||||
|
||||
function dateToServer (date) {
|
||||
return date.clone().utc().locale('en').format('YYYY-MM-DD HH:mm:ss');
|
||||
}
|
||||
|
||||
var CalendarController = AbstractController.extend({
|
||||
custom_events: _.extend({}, AbstractController.prototype.custom_events, {
|
||||
changeDate: '_onChangeDate',
|
||||
@@ -236,7 +240,7 @@ var CalendarController = AbstractController.extend({
|
||||
|
||||
for (var k in context) {
|
||||
if (context[k] && context[k]._isAMomentObject) {
|
||||
context[k] = context[k].clone().utc().format('YYYY-MM-DD HH:mm:ss');
|
||||
context[k] = dateToServer(context[k]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -112,11 +112,13 @@ var SidebarFilter = Widget.extend(FieldManagerMixin, {
|
||||
_onFieldChanged: function (event) {
|
||||
var self = this;
|
||||
event.stopPropagation();
|
||||
var createValues = {'user_id': session.uid};
|
||||
var value = event.data.changes[this.write_field].id;
|
||||
createValues[this.write_field] = value;
|
||||
this._rpc({
|
||||
model: this.write_model,
|
||||
method: 'create',
|
||||
args: [{'user_id': session.uid,'partner_id': value,}],
|
||||
args: [createValues],
|
||||
})
|
||||
.then(function () {
|
||||
self.trigger_up('changeFilter', {
|
||||
|
||||
@@ -1465,7 +1465,7 @@
|
||||
<t t-set="att_prev" t-value="{'accesskey': 'p'}" />
|
||||
<t t-set="att_next" t-value="{'accesskey': 'n'}" />
|
||||
</t>
|
||||
<button type="button" class="fa fa-chevron-left btn btn-secondary o_pager_previous" t-att="att_prev" aria-label="Previous" title="Next" tabindex="-1"/>
|
||||
<button type="button" class="fa fa-chevron-left btn btn-secondary o_pager_previous" t-att="att_prev" aria-label="Previous" title="Previous" tabindex="-1"/>
|
||||
<button type="button" class="fa fa-chevron-right btn btn-secondary o_pager_next" t-att="att_next" aria-label="Next" title="Next" tabindex="-1"/>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -373,6 +373,7 @@ var SnippetEditor = Widget.extend({
|
||||
*/
|
||||
_onDragAndDropStart: function () {
|
||||
var self = this;
|
||||
this.dropped = false;
|
||||
self.size = {
|
||||
width: self.$target.width(),
|
||||
height: self.$target.height()
|
||||
@@ -418,10 +419,21 @@ var SnippetEditor = Widget.extend({
|
||||
* 'move' button.
|
||||
*
|
||||
* @private
|
||||
* @param {Event} ev
|
||||
* @param {Object} ui
|
||||
*/
|
||||
_onDragAndDropStop: function () {
|
||||
_onDragAndDropStop: function (ev, ui) {
|
||||
var self = this;
|
||||
|
||||
// TODO lot of this is duplicated code of the d&d feature of snippets
|
||||
if (!this.dropped) {
|
||||
var $el = $.nearest({x: ui.position.left, y: ui.position.top}, '.oe_drop_zone').first();
|
||||
if ($el.length) {
|
||||
$el.after(this.$target);
|
||||
this.dropped = true;
|
||||
}
|
||||
}
|
||||
|
||||
$('.oe_drop_zone').droppable('destroy').remove();
|
||||
|
||||
var prev = this.$target.first()[0].previousSibling;
|
||||
|
||||
@@ -174,7 +174,7 @@ function getMatchedCSSRules(a) {
|
||||
break;
|
||||
}
|
||||
$el = $el.parent();
|
||||
} while (!$el.is('html'));
|
||||
} while ($el.length && !$el.is('html'));
|
||||
}
|
||||
|
||||
return style;
|
||||
|
||||
@@ -159,7 +159,7 @@ ul.oe_menu_editor {
|
||||
@mixin o-spacing-all($factor: 1) {
|
||||
// Generate vertical margin/padding classes used by the editor
|
||||
@for $i from 0 through (256 / 8) {
|
||||
@include o-vspacing($i * 8);
|
||||
@include o-vspacing($i * 8, $factor);
|
||||
}
|
||||
@include o-vspacing(4, $factor);
|
||||
|
||||
|
||||
@@ -181,10 +181,7 @@ class WebsiteEventController(http.Controller):
|
||||
def event_register(self, event, **post):
|
||||
if not event.can_access_from_current_website():
|
||||
raise werkzeug.exceptions.NotFound()
|
||||
if not request.context.get('pricelist'):
|
||||
pricelist = request.website.get_current_pricelist()
|
||||
if pricelist:
|
||||
event = event.with_context(pricelist=pricelist.id)
|
||||
|
||||
values = {
|
||||
'event': event,
|
||||
'main_object': event,
|
||||
|
||||
@@ -11,6 +11,10 @@ class WebsiteEventSaleController(WebsiteEventController):
|
||||
@http.route()
|
||||
def event_register(self, event, **post):
|
||||
event = event.with_context(pricelist=request.website.id)
|
||||
if not request.context.get('pricelist'):
|
||||
pricelist = request.website.get_current_pricelist()
|
||||
if pricelist:
|
||||
event = event.with_context(pricelist=pricelist.id)
|
||||
return super(WebsiteEventSaleController, self).event_register(event, **post)
|
||||
|
||||
def _process_tickets_details(self, data):
|
||||
|
||||
@@ -243,3 +243,7 @@ img.o_forum_avatar_big {
|
||||
min-height: 75px;
|
||||
}
|
||||
}
|
||||
|
||||
.website_forum {
|
||||
margin-bottom: $spacer;
|
||||
}
|
||||
|
||||
@@ -15,3 +15,4 @@ List of contributors:
|
||||
Brain Wang wangbuke@inspur.com
|
||||
David Yu yudw@inspur.com
|
||||
Alex Aisin-Gioro wubai@inspur.com
|
||||
Colin Li lizheng02@inspur.com https://github.com/Colinliz
|
||||
@@ -0,0 +1,11 @@
|
||||
Russia, 2019-03-15
|
||||
|
||||
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,
|
||||
|
||||
Ildar Nasyrov iledarn@gmail.com https://github.com/iledarn
|
||||
+82
-96
@@ -55,7 +55,7 @@ The ``create`` method
|
||||
|
||||
:param str contract: (required) your enterprise contract reference
|
||||
:param str email: (required) your email address
|
||||
:param str target: (required) the Odoo version you want to upgrade to. Valid choices: 6.0, 6.1, 7.0, 8.0
|
||||
:param str target: (required) the Odoo version you want to upgrade to. Valid choices: 10.0, 11.0, 12.0
|
||||
:param str aim: (required) the purpose of your upgrade database request. Valid choices: test, production.
|
||||
:param str filename: (required) a purely informative name for you database dump file
|
||||
:param str timezone: (optional) the timezone used by your server. Only for Odoo source version < 6.1
|
||||
@@ -96,10 +96,9 @@ See a sample output aside.
|
||||
"failures": [
|
||||
{
|
||||
"expected": [
|
||||
"6.0",
|
||||
"6.1",
|
||||
"7.0",
|
||||
"8.0",
|
||||
"10.0",
|
||||
"11.0",
|
||||
"12.0",
|
||||
],
|
||||
"message": "Invalid value \"5.0\"",
|
||||
"reason": "TARGET:INVALID",
|
||||
@@ -135,7 +134,7 @@ Sample script
|
||||
|
||||
Here are 2 examples of database upgrade request creation using:
|
||||
|
||||
* one in the python programming language using the pycurl library
|
||||
* one in the python programming language using the requests library
|
||||
* one in the bash programming language using `curl <http://curl.haxx.se>`_ (tool
|
||||
for transfering data using http) and `jq <https://stedolan.github.io/jq>`_ (JSON processor):
|
||||
|
||||
@@ -145,15 +144,12 @@ Here are 2 examples of database upgrade request creation using:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from urllib import urlencode
|
||||
from io import BytesIO
|
||||
import pycurl
|
||||
import json
|
||||
import requests
|
||||
|
||||
CREATE_URL = "https://upgrade.odoo.com/database/v1/create"
|
||||
CONTRACT = "M123456-abcdef"
|
||||
AIM = "test"
|
||||
TARGET = "8.0"
|
||||
TARGET = "12.0"
|
||||
EMAIL = "john.doe@example.com"
|
||||
FILENAME = "db_name.dump"
|
||||
|
||||
@@ -164,27 +160,15 @@ Here are 2 examples of database upgrade request creation using:
|
||||
('contract', CONTRACT),
|
||||
('target', TARGET),
|
||||
])
|
||||
postfields = urlencode(fields)
|
||||
|
||||
c = pycurl.Curl()
|
||||
c.setopt(pycurl.URL, CREATE_URL)
|
||||
c.setopt(c.POSTFIELDS, postfields)
|
||||
data = BytesIO()
|
||||
c.setopt(c.WRITEFUNCTION, data.write)
|
||||
c.perform()
|
||||
|
||||
# transform output into a dict:
|
||||
response = json.loads(data.getvalue())
|
||||
|
||||
# get http status:
|
||||
http_code = c.getinfo(pycurl.HTTP_CODE)
|
||||
c.close()
|
||||
r = requests.get(CREATE_URL, data=fields)
|
||||
print(r.text)
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
CONTRACT=M123456-abcdef
|
||||
AIM=test
|
||||
TARGET=8.0
|
||||
TARGET=12.0
|
||||
EMAIL=john.doe@example.com
|
||||
FILENAME=db_name.dump
|
||||
CREATE_URL="https://upgrade.odoo.com/database/v1/create"
|
||||
@@ -235,33 +219,19 @@ should be empty if everything went fine.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import os
|
||||
import pycurl
|
||||
from urllib import urlencode
|
||||
import requests
|
||||
|
||||
UPLOAD_URL = "https://upgrade.odoo.com/database/v1/upload"
|
||||
DUMPFILE = "openchs.70.cdump"
|
||||
DUMPFILE = "/tmp/dump.sql"
|
||||
|
||||
fields = dict([
|
||||
('request', '10534'),
|
||||
('key', 'Aw7pItGVKFuZ_FOR3U8VFQ=='),
|
||||
])
|
||||
headers = {"Content-Type": "application/octet-stream"}
|
||||
postfields = urlencode(fields)
|
||||
|
||||
c = pycurl.Curl()
|
||||
c.setopt(pycurl.URL, UPLOAD_URL+"?"+postfields)
|
||||
c.setopt(pycurl.POST, 1)
|
||||
filesize = os.path.getsize(DUMPFILE)
|
||||
c.setopt(pycurl.POSTFIELDSIZE, filesize)
|
||||
fp = open(DUMPFILE, 'rb')
|
||||
c.setopt(pycurl.READFUNCTION, fp.read)
|
||||
c.setopt(
|
||||
pycurl.HTTPHEADER,
|
||||
['%s: %s' % (k, headers[k]) for k in headers])
|
||||
|
||||
c.perform()
|
||||
c.close()
|
||||
with open(DUMPFILE, 'rb') as f:
|
||||
requests.post(UPLOAD_URL, data=f, params=fields, headers=headers)
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
@@ -311,29 +281,20 @@ The ``request_sftp_access`` method returns a JSON dictionary containing the foll
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import os
|
||||
import pycurl
|
||||
from urllib import urlencode
|
||||
import requests
|
||||
|
||||
UPLOAD_URL = "https://upgrade.odoo.com/database/v1/request_sftp_access"
|
||||
SSH_KEYS="/path/to/your/authorized_keys"
|
||||
SSH_KEY = "$HOME/.ssh/id_rsa.pub"
|
||||
SSH_KEY_CONTENT = open(SSH_KEY,'r').read()
|
||||
|
||||
fields = dict([
|
||||
('request', '10534'),
|
||||
('key', 'Aw7pItGVKFuZ_FOR3U8VFQ=='),
|
||||
('ssh_keys', SSH_KEY_CONTENT)
|
||||
])
|
||||
postfields = urlencode(fields)
|
||||
|
||||
c = pycurl.Curl()
|
||||
c.setopt(pycurl.URL, UPLOAD_URL+"?"+postfields)
|
||||
c.setopt(pycurl.POST, 1)
|
||||
c.setopt(c.HTTPPOST,[("ssh_keys",
|
||||
(c.FORM_FILE, SSH_KEYS,
|
||||
c.FORM_CONTENTTYPE, "text/plain"))
|
||||
])
|
||||
|
||||
c.perform()
|
||||
c.close()
|
||||
r = requests.post(UPLOAD_URL, params=fields)
|
||||
print(r.text)
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
@@ -442,10 +403,7 @@ should be empty if everything went fine.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from urllib import urlencode
|
||||
from io import BytesIO
|
||||
import pycurl
|
||||
import json
|
||||
import requests
|
||||
|
||||
PROCESS_URL = "https://upgrade.odoo.com/database/v1/process"
|
||||
|
||||
@@ -453,21 +411,9 @@ should be empty if everything went fine.
|
||||
('request', '10534'),
|
||||
('key', 'Aw7pItGVKFuZ_FOR3U8VFQ=='),
|
||||
])
|
||||
postfields = urlencode(fields)
|
||||
|
||||
c = pycurl.Curl()
|
||||
c.setopt(pycurl.URL, PROCESS_URL)
|
||||
c.setopt(c.POSTFIELDS, postfields)
|
||||
data = BytesIO()
|
||||
c.setopt(c.WRITEFUNCTION, data.write)
|
||||
c.perform()
|
||||
|
||||
# transform output into a dict:
|
||||
response = json.loads(data.getvalue())
|
||||
|
||||
# get http status:
|
||||
http_code = c.getinfo(pycurl.HTTP_CODE)
|
||||
c.close()
|
||||
r = requests.get(PROCESS_URL, data=fields)
|
||||
print(r.text)
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
@@ -479,6 +425,59 @@ should be empty if everything went fine.
|
||||
|
||||
.. _upgrade-api-status-method:
|
||||
|
||||
|
||||
Asking to skip the tests
|
||||
=========================
|
||||
|
||||
This action asks the Upgrade Platform to skip the tests for your request.
|
||||
If you don't want Odoo to test and validate the migration, you can bypass the testing stage and directly get the migrated dump.
|
||||
|
||||
The ``skip_test`` method
|
||||
----------------------
|
||||
|
||||
.. py:function:: https://upgrade.odoo.com/database/v1/skip_test
|
||||
|
||||
Skip the tests, deliver the upgraded dump, and set the state to 'delivered'
|
||||
|
||||
:param str key: (required) your private key
|
||||
:param str request: (required) your request id
|
||||
:return: request result
|
||||
:rtype: JSON dictionary
|
||||
|
||||
The request id and the private key are obtained using the :ref:`create method
|
||||
<upgrade-api-create-method>`
|
||||
|
||||
The result is a JSON dictionary containing the list of ``failures``, which
|
||||
should be empty if everything went fine.
|
||||
|
||||
.. rst-class:: setup doc-aside
|
||||
|
||||
.. switcher::
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import requests
|
||||
|
||||
PROCESS_URL = "https://upgrade.odoo.com/database/v1/skip_test"
|
||||
|
||||
fields = dict([
|
||||
('request', '10534'),
|
||||
('key', 'Aw7pItGVKFuZ_FOR3U8VFQ=='),
|
||||
])
|
||||
|
||||
r = requests.get(PROCESS_URL, data=fields)
|
||||
print(r.text)
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
PROCESS_URL="https://upgrade.odoo.com/database/v1/skip_test"
|
||||
KEY="Aw7pItGVKFuZ_FOR3U8VFQ=="
|
||||
REQUEST_ID="10534"
|
||||
URL_PARAMS="key=${KEY}&request=${REQUEST_ID}"
|
||||
curl -sS "${PROCESS_URL}?${URL_PARAMS}"
|
||||
|
||||
.. _upgrade-api-status-method:
|
||||
|
||||
Obtaining your request status
|
||||
=============================
|
||||
|
||||
@@ -508,30 +507,17 @@ database upgrade request.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from urllib import urlencode
|
||||
from io import BytesIO
|
||||
import pycurl
|
||||
import json
|
||||
import requests
|
||||
|
||||
STATUS_URL = "https://upgrade.odoo.com/database/v1/status"
|
||||
PROCESS_URL = "https://upgrade.odoo.com/database/v1/status"
|
||||
|
||||
fields = dict([
|
||||
('request', '10534'),
|
||||
('key', 'Aw7pItGVKFuZ_FOR3U8VFQ=='),
|
||||
])
|
||||
postfields = urlencode(fields)
|
||||
|
||||
c = pycurl.Curl()
|
||||
c.setopt(pycurl.URL, PROCESS_URL)
|
||||
c.setopt(c.POSTFIELDS, postfields)
|
||||
data = BytesIO()
|
||||
c.setopt(c.WRITEFUNCTION, data.write)
|
||||
c.perform()
|
||||
|
||||
# transform output into a dict:
|
||||
response = json.loads(data.getvalue())
|
||||
|
||||
c.close()
|
||||
r = requests.get(PROCESS_URL, data=fields)
|
||||
print(r.text)
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
@@ -615,7 +601,7 @@ The ``request`` key contains various useful information about your request:
|
||||
"id": 10534,
|
||||
"key": "Aw7pItGVKFuZ_FOR3U8VFQ==",
|
||||
"email": "john.doe@example.com",
|
||||
"target": "8.0",
|
||||
"target": "12.0",
|
||||
"aim": "test",
|
||||
"filename": "db_name.dump",
|
||||
"timezone": null,
|
||||
@@ -631,7 +617,7 @@ The ``request`` key contains various useful information about your request:
|
||||
"modules_url": "https://upgrade.odoo.com/database/eu1/10534/Aw7pItGVKFuZ_FOR3U8VFQ==/modules/archive",
|
||||
"filesize": "912.99 Kb",
|
||||
"database_uuid": null,
|
||||
"created_at": "2015-09-09 07:13:49",
|
||||
"created_at": "2018-09-09 07:13:49",
|
||||
"estimated_time": null,
|
||||
"processed_at": null,
|
||||
"elapsed": "00:00",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from odoo import fields, models
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class IrLogging(models.Model):
|
||||
@@ -7,8 +7,22 @@ class IrLogging(models.Model):
|
||||
_description = 'Logging'
|
||||
_order = 'id DESC'
|
||||
|
||||
create_date = fields.Datetime(readonly=True)
|
||||
create_uid = fields.Integer(string='Uid', readonly=True) # Integer not m2o is intentionnal
|
||||
# The _log_access fields are defined manually for the following reasons:
|
||||
#
|
||||
# - The entries in ir_logging are filled in with sql queries bypassing the orm. As the --log-db
|
||||
# cli option allows to insert ir_logging entries into a remote database, the one2many *_uid
|
||||
# fields make no sense in the first place but we will keep it for backward compatibility.
|
||||
#
|
||||
# - Also, when an ir_logging entry is triggered by the orm (when using --log-db) at the moment
|
||||
# it is making changes to the res.users model, the ALTER TABLE will aquire an exclusive lock
|
||||
# on res_users, preventing the ir_logging INSERT to be processed, hence the ongoing module
|
||||
# install/update will hang forever as the orm is blocked by the ir_logging query that will
|
||||
# never occur.
|
||||
create_uid = fields.Integer(string='Created by', readonly=True)
|
||||
create_date = fields.Datetime(string='Created on', readonly=True)
|
||||
write_uid = fields.Integer(string='Last Updated by', readonly=True)
|
||||
write_date = fields.Datetime(string='Last Updated on', readonly=True)
|
||||
|
||||
name = fields.Char(required=True)
|
||||
type = fields.Selection([('client', 'Client'), ('server', 'Server')], required=True, index=True)
|
||||
dbname = fields.Char(string='Database Name', index=True)
|
||||
@@ -17,3 +31,8 @@ class IrLogging(models.Model):
|
||||
path = fields.Char(required=True)
|
||||
func = fields.Char(string='Function', required=True)
|
||||
line = fields.Char(required=True)
|
||||
|
||||
@api.model_cr
|
||||
def init(self):
|
||||
super(IrLogging, self).init()
|
||||
self._cr.execute("ALTER TABLE ir_logging DROP CONSTRAINT IF EXISTS ir_logging_write_uid_fkey")
|
||||
|
||||
@@ -542,6 +542,8 @@ class Partner(models.Model):
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
if self.env.context.get('import_file'):
|
||||
self._check_import_consistency(vals_list)
|
||||
for vals in vals_list:
|
||||
if vals.get('website'):
|
||||
vals['website'] = self._clean_website(vals['website'])
|
||||
@@ -863,6 +865,26 @@ class Partner(models.Model):
|
||||
'template': '/base/static/xls/res_partner.xls'
|
||||
}]
|
||||
|
||||
@api.model
|
||||
def _check_import_consistency(self, vals_list):
|
||||
"""
|
||||
The values created by an import are generated by a name search, field by field.
|
||||
As a result there is no check that the field values are consistent with each others.
|
||||
We check that if the state is given a value, it does belong to the given country, or we remove it.
|
||||
"""
|
||||
States = self.env['res.country.state']
|
||||
states_ids = {vals['state_id'] for vals in vals_list if vals.get('state_id')}
|
||||
state_to_country = States.search([('id', 'in', list(states_ids))]).read(['country_id'])
|
||||
for vals in vals_list:
|
||||
if vals.get('state_id'):
|
||||
country_id = next(c['country_id'][0] for c in state_to_country if c['id'] == vals.get('state_id'))
|
||||
state = States.browse(vals['state_id'])
|
||||
if state.country_id.id != country_id:
|
||||
state_domain = [('code', '=', state.code),
|
||||
('country_id', '=', country_id)]
|
||||
state = States.search(state_domain, limit=1)
|
||||
vals['state_id'] = state.id # replace state or remove it if not found
|
||||
|
||||
@api.multi
|
||||
def _get_country_name(self):
|
||||
return self.country_id.name or ''
|
||||
|
||||
@@ -130,6 +130,19 @@ class TranslationToolsTestCase(unittest.TestCase):
|
||||
self.assertItemsEqual(terms,
|
||||
['Form stuff', '<span class="fa fa-globe" title="Title stuff"/>'])
|
||||
|
||||
def test_translate_xml_inline5(self):
|
||||
""" Test xml_translate() with inline elements with empty translated attrs only. """
|
||||
terms = []
|
||||
source = """<form string="Form stuff">
|
||||
<div>
|
||||
<label for="stuff"/>
|
||||
<span class="fa fa-globe" title=""/>
|
||||
</div>
|
||||
</form>"""
|
||||
result = xml_translate(terms.append, source)
|
||||
self.assertEquals(result, source)
|
||||
self.assertItemsEqual(terms, ['Form stuff'])
|
||||
|
||||
def test_translate_xml_t(self):
|
||||
""" Test xml_translate() with t-* attributes. """
|
||||
terms = []
|
||||
|
||||
+95
-27
@@ -25,9 +25,25 @@ if os.name == 'posix':
|
||||
# Unix only for workers
|
||||
import fcntl
|
||||
import resource
|
||||
try:
|
||||
import inotify
|
||||
from inotify.adapters import InotifyTrees
|
||||
from inotify.constants import IN_MODIFY, IN_CREATE, IN_MOVED_TO
|
||||
INOTIFY_LISTEN_EVENTS = IN_MODIFY | IN_CREATE | IN_MOVED_TO
|
||||
except ImportError:
|
||||
inotify = None
|
||||
else:
|
||||
# Windows shim
|
||||
signal.SIGHUP = -1
|
||||
inotify = None
|
||||
|
||||
if not inotify:
|
||||
try:
|
||||
import watchdog
|
||||
from watchdog.observers import Observer
|
||||
from watchdog.events import FileCreatedEvent, FileModifiedEvent, FileMovedEvent
|
||||
except ImportError:
|
||||
watchdog = None
|
||||
|
||||
# Optional process names for workers
|
||||
try:
|
||||
@@ -45,13 +61,6 @@ from odoo.tools import stripped_sys_argv, dumpstacks, log_ormcache_stats
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
import watchdog
|
||||
from watchdog.observers import Observer
|
||||
from watchdog.events import FileCreatedEvent, FileModifiedEvent, FileMovedEvent
|
||||
except ImportError:
|
||||
watchdog = None
|
||||
|
||||
SLEEP_INTERVAL = 60 # 1 min
|
||||
|
||||
def memory_info(process):
|
||||
@@ -176,7 +185,24 @@ class ThreadedWSGIServerReloadable(LoggingBaseWSGIServerMixIn, werkzeug.serving.
|
||||
#----------------------------------------------------------
|
||||
# FileSystem Watcher for autoreload and cache invalidation
|
||||
#----------------------------------------------------------
|
||||
class FSWatcher(object):
|
||||
class FSWatcherBase(object):
|
||||
def handle_file(self, path):
|
||||
if path.endswith('.py') and not os.path.basename(path).startswith('.~'):
|
||||
try:
|
||||
source = open(path, 'rb').read() + b'\n'
|
||||
compile(source, path, 'exec')
|
||||
except IOError:
|
||||
_logger.error('autoreload: python code change detected, IOError for %s', path)
|
||||
except SyntaxError:
|
||||
_logger.error('autoreload: python code change detected, SyntaxError in %s', path)
|
||||
else:
|
||||
if not getattr(odoo, 'phoenix', False):
|
||||
_logger.info('autoreload: python code updated, autoreload activated')
|
||||
restart()
|
||||
return True
|
||||
|
||||
|
||||
class FSWatcherWatchdog(FSWatcherBase):
|
||||
def __init__(self):
|
||||
self.observer = Observer()
|
||||
for path in odoo.modules.module.ad_paths:
|
||||
@@ -187,27 +213,60 @@ class FSWatcher(object):
|
||||
if isinstance(event, (FileCreatedEvent, FileModifiedEvent, FileMovedEvent)):
|
||||
if not event.is_directory:
|
||||
path = getattr(event, 'dest_path', event.src_path)
|
||||
if path.endswith('.py') and not os.path.basename(path).startswith('.~'):
|
||||
try:
|
||||
source = open(path, 'rb').read() + b'\n'
|
||||
compile(source, path, 'exec')
|
||||
except FileNotFoundError:
|
||||
_logger.error('autoreload: python code change detected, FileNotFound for %s', path)
|
||||
except SyntaxError:
|
||||
_logger.error('autoreload: python code change detected, SyntaxError in %s', path)
|
||||
else:
|
||||
if not getattr(odoo, 'phoenix', False):
|
||||
_logger.info('autoreload: python code updated, autoreload activated')
|
||||
restart()
|
||||
self.handle_file(path)
|
||||
|
||||
def start(self):
|
||||
self.observer.start()
|
||||
_logger.info('AutoReload watcher running')
|
||||
_logger.info('AutoReload watcher running with watchdog')
|
||||
|
||||
def stop(self):
|
||||
self.observer.stop()
|
||||
self.observer.join()
|
||||
|
||||
|
||||
class FSWatcherInotify(FSWatcherBase):
|
||||
def __init__(self):
|
||||
self.started = False
|
||||
# ignore warnings from inotify in case we have duplicate addons paths.
|
||||
inotify.adapters._LOGGER.setLevel(logging.ERROR)
|
||||
# recreate a list as InotifyTrees' __init__ deletes the list's items
|
||||
paths_to_watch = []
|
||||
for path in odoo.modules.module.ad_paths:
|
||||
paths_to_watch.append(path)
|
||||
_logger.info('Watching addons folder %s', path)
|
||||
self.watcher = InotifyTrees(paths_to_watch, mask=INOTIFY_LISTEN_EVENTS, block_duration_s=.5)
|
||||
|
||||
def run(self):
|
||||
_logger.info('AutoReload watcher running with inotify')
|
||||
dir_creation_events = set(('IN_MOVED_TO', 'IN_CREATE'))
|
||||
while self.started:
|
||||
for event in self.watcher.event_gen(timeout_s=0, yield_nones=False):
|
||||
(_, type_names, path, filename) = event
|
||||
if 'IN_ISDIR' not in type_names:
|
||||
# despite not having IN_DELETE in the watcher's mask, the
|
||||
# watcher sends these events when a directory is deleted.
|
||||
if 'IN_DELETE' not in type_names:
|
||||
full_path = os.path.join(path, filename)
|
||||
if self.handle_file(full_path):
|
||||
return
|
||||
elif dir_creation_events.intersection(type_names):
|
||||
full_path = os.path.join(path, filename)
|
||||
for root, _, files in os.walk(full_path):
|
||||
for file in files:
|
||||
if self.handle_file(os.path.join(root, file)):
|
||||
return
|
||||
|
||||
def start(self):
|
||||
self.started = True
|
||||
self.thread = threading.Thread(target=self.run, name="odoo.service.autoreload.watcher")
|
||||
self.thread.setDaemon(True)
|
||||
self.thread.start()
|
||||
|
||||
def stop(self):
|
||||
self.started = False
|
||||
self.thread.join()
|
||||
|
||||
|
||||
#----------------------------------------------------------
|
||||
# Servers: Threaded, Gevented and Prefork
|
||||
#----------------------------------------------------------
|
||||
@@ -878,6 +937,8 @@ class Worker(object):
|
||||
while self.alive:
|
||||
self.multi.pipe_ping(self.watchdog_pipe)
|
||||
self.sleep()
|
||||
if not self.alive:
|
||||
break
|
||||
self.process_work()
|
||||
self.check_limits()
|
||||
except:
|
||||
@@ -1108,21 +1169,28 @@ def start(preload=None, stop=False):
|
||||
server = ThreadedServer(odoo.service.wsgi_server.application)
|
||||
|
||||
watcher = None
|
||||
if 'reload' in config['dev_mode']:
|
||||
if watchdog:
|
||||
watcher = FSWatcher()
|
||||
if 'reload' in config['dev_mode'] and not odoo.evented:
|
||||
if inotify:
|
||||
watcher = FSWatcherInotify()
|
||||
watcher.start()
|
||||
elif watchdog:
|
||||
watcher = FSWatcherWatchdog()
|
||||
watcher.start()
|
||||
else:
|
||||
_logger.warning("'watchdog' module not installed. Code autoreload feature is disabled")
|
||||
if os.name == 'posix' and platform.system() != 'Darwin':
|
||||
module = 'inotify'
|
||||
else:
|
||||
module = 'watchdog'
|
||||
_logger.warning("'%s' module not installed. Code autoreload feature is disabled", module)
|
||||
if 'werkzeug' in config['dev_mode']:
|
||||
server.app = DebuggedApplication(server.app, evalex=True)
|
||||
|
||||
rc = server.run(preload, stop)
|
||||
|
||||
if watcher:
|
||||
watcher.stop()
|
||||
# like the legend of the phoenix, all ends with beginnings
|
||||
if getattr(odoo, 'phoenix', False):
|
||||
if watcher:
|
||||
watcher.stop()
|
||||
_reexec()
|
||||
|
||||
return rc if rc else 0
|
||||
|
||||
@@ -257,7 +257,7 @@ def translate_xml_node(node, callback, parse, serialize):
|
||||
result.tail = node.tail
|
||||
has_text = (
|
||||
todo_has_text or nonspace(result.text) or nonspace(result.tail)
|
||||
or any(name in TRANSLATED_ATTRS for name in result.attrib)
|
||||
or any((key in TRANSLATED_ATTRS and val) for key, val in result.attrib.items())
|
||||
)
|
||||
return (has_text, result)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user