summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristophe Simonis <chs@odoo.com>2019-03-25 19:45:26 +0100
committerChristophe Simonis <chs@odoo.com>2019-03-25 19:45:26 +0100
commitf00c490be8688d80f211f6755b5ada541eefd602 (patch)
tree39fad5136327db80653801f8a40f3c5c807c65d0
parent28a1371035fae9892e1ceeb6c9edd7ddda806ed5 (diff)
parent11b1e12cde37cc002e475d11793be61d9d108290 (diff)
[MERGE] forward port branch 12.0 up to 11b1e12cde3
-rw-r--r--addons/account/models/account.py3
-rw-r--r--addons/account/models/account_invoice.py12
-rw-r--r--addons/account/report/account_invoice_report.py3
-rw-r--r--addons/auth_signup/data/auth_signup_data.xml2
-rw-r--r--addons/base_iban/models/res_partner_bank.py12
-rw-r--r--addons/crm/models/crm_lead.py7
-rw-r--r--addons/crm/models/crm_team.py13
-rw-r--r--addons/fleet/models/fleet_vehicle_cost.py2
-rw-r--r--addons/hr_holidays/__manifest__.py1
-rw-r--r--addons/hr_holidays/static/scss/hr_leave_mobile.scss6
-rw-r--r--addons/hr_holidays/views/hr_leave_template.xml8
-rw-r--r--addons/hr_holidays/views/hr_leave_views.xml2
-rw-r--r--addons/hr_timesheet/models/hr_timesheet.py2
-rw-r--r--addons/hr_timesheet/tests/test_timesheet.py32
-rw-r--r--addons/l10n_tr/__manifest__.py5
-rw-r--r--addons/l10n_tr/data/res.country.state.csv82
-rw-r--r--addons/mail/models/mail_activity.py11
-rw-r--r--addons/payment_adyen/models/payment.py40
-rw-r--r--addons/payment_stripe/static/src/js/stripe.js6
-rw-r--r--addons/payment_stripe/views/payment_stripe_templates.xml7
-rw-r--r--addons/point_of_sale/views/pos_config_view.xml6
-rw-r--r--addons/product/views/product_template_views.xml1
-rw-r--r--addons/purchase_requisition/models/purchase.py2
-rw-r--r--addons/sale/models/account_invoice.py3
-rw-r--r--addons/sale/models/sale.py35
-rw-r--r--addons/sale/report/sale_report_templates.xml6
-rw-r--r--addons/sale/views/sale_portal_templates.xml2
-rw-r--r--addons/stock/static/src/js/stock_traceability_report_widgets.js2
-rw-r--r--addons/stock/views/stock_picking_views.xml2
-rw-r--r--addons/stock_dropshipping/models/sale.py2
-rw-r--r--addons/stock_picking_batch/report/report_picking_batch.xml4
-rw-r--r--addons/test_mail/tests/test_mail_gateway.py8
-rw-r--r--addons/test_mail/tests/test_mail_message.py20
-rw-r--r--addons/web/static/src/js/views/calendar/calendar_controller.js6
-rw-r--r--addons/web/static/src/js/views/calendar/calendar_renderer.js4
-rw-r--r--addons/web/static/src/xml/base.xml2
-rw-r--r--addons/web_editor/static/src/js/editor/snippets.editor.js14
-rw-r--r--addons/web_editor/static/src/js/editor/transcoder.js2
-rw-r--r--addons/web_editor/static/src/scss/web_editor.common.scss2
-rw-r--r--addons/website_event/controllers/main.py5
-rw-r--r--addons/website_event_sale/controllers/main.py4
-rw-r--r--addons/website_forum/static/src/scss/website_forum.scss4
-rw-r--r--doc/cla/corporate/inspur.md1
-rw-r--r--doc/cla/individual/iledarn.md11
-rw-r--r--doc/webservices/upgrade.rst178
-rw-r--r--odoo/addons/base/models/ir_logging.py25
-rw-r--r--odoo/addons/base/models/res_partner.py22
-rw-r--r--odoo/addons/base/tests/test_translate.py13
-rw-r--r--odoo/service/server.py122
-rw-r--r--odoo/tools/translate.py2
50 files changed, 568 insertions, 198 deletions
diff --git a/addons/account/models/account.py b/addons/account/models/account.py
index 00bdabb7f9e..c4569e91886 100644
--- a/addons/account/models/account.py
+++ b/addons/account/models/account.py
@@ -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)
}
diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py
index 3143827c903..aee35e460a6 100644
--- a/addons/account/models/account_invoice.py
+++ b/addons/account/models/account_invoice.py
@@ -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):
diff --git a/addons/account/report/account_invoice_report.py b/addons/account/report/account_invoice_report.py
index aa37b7bec3c..7861644525a 100644
--- a/addons/account/report/account_invoice_report.py
+++ b/addons/account/report/account_invoice_report.py
@@ -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)
diff --git a/addons/auth_signup/data/auth_signup_data.xml b/addons/auth_signup/data/auth_signup_data.xml
index 658ea04d251..6db81fd0dc3 100644
--- a/addons/auth_signup/data/auth_signup_data.xml
+++ b/addons/auth_signup/data/auth_signup_data.xml
@@ -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&amp;utm_medium=auth" style="color: #875A7B;">Odoo Tour</a> to discover the tool.
<br /><br />
diff --git a/addons/base_iban/models/res_partner_bank.py b/addons/base_iban/models/res_partner_bank.py
index 62b414eae00..f457a912011 100644
--- a/addons/base_iban/models/res_partner_bank.py
+++ b/addons/base_iban/models/res_partner_bank.py
@@ -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
diff --git a/addons/crm/models/crm_lead.py b/addons/crm/models/crm_lead.py
index 2858ac644cc..36710856502 100644
--- a/addons/crm/models/crm_lead.py
+++ b/addons/crm/models/crm_lead.py
@@ -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, ...)
# ----------------------------------------
diff --git a/addons/crm/models/crm_team.py b/addons/crm/models/crm_team.py
index a101fb26848..d7e66bfa3ee 100644
--- a/addons/crm/models/crm_team.py
+++ b/addons/crm/models/crm_team.py
@@ -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)
diff --git a/addons/fleet/models/fleet_vehicle_cost.py b/addons/fleet/models/fleet_vehicle_cost.py
index 422645912de..c66403290e5 100644
--- a/addons/fleet/models/fleet_vehicle_cost.py
+++ b/addons/fleet/models/fleet_vehicle_cost.py
@@ -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())])
diff --git a/addons/hr_holidays/__manifest__.py b/addons/hr_holidays/__manifest__.py
index 9c765ea2596..c57a2cfbca4 100644
--- a/addons/hr_holidays/__manifest__.py
+++ b/addons/hr_holidays/__manifest__.py
@@ -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',
diff --git a/addons/hr_holidays/static/scss/hr_leave_mobile.scss b/addons/hr_holidays/static/scss/hr_leave_mobile.scss
new file mode 100644
index 00000000000..7dabf58db05
--- /dev/null
+++ b/addons/hr_holidays/static/scss/hr_leave_mobile.scss
@@ -0,0 +1,6 @@
+@include media-breakpoint-down(sm) {
+ .o_hr_holidays_dates {
+ display: flex;
+ flex-flow: column;
+ }
+}
diff --git a/addons/hr_holidays/views/hr_leave_template.xml b/addons/hr_holidays/views/hr_leave_template.xml
new file mode 100644
index 00000000000..e178a7d5f41
--- /dev/null
+++ b/addons/hr_holidays/views/hr_leave_template.xml
@@ -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>
diff --git a/addons/hr_holidays/views/hr_leave_views.xml b/addons/hr_holidays/views/hr_leave_views.xml
index 5356e36a5b1..8614cb5eecf 100644
--- a/addons/hr_holidays/views/hr_leave_views.xml
+++ b/addons/hr_holidays/views/hr_leave_views.xml
@@ -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"
diff --git a/addons/hr_timesheet/models/hr_timesheet.py b/addons/hr_timesheet/models/hr_timesheet.py
index da83e0fd0db..10898588532 100644
--- a/addons/hr_timesheet/models/hr_timesheet.py
+++ b/addons/hr_timesheet/models/hr_timesheet.py
@@ -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']]):
diff --git a/addons/hr_timesheet/tests/test_timesheet.py b/addons/hr_timesheet/tests/test_timesheet.py
index b984beaed0b..a67fb5f79dc 100644
--- a/addons/hr_timesheet/tests/test_timesheet.py
+++ b/addons/hr_timesheet/tests/test_timesheet.py
@@ -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,
+ }])
diff --git a/addons/l10n_tr/__manifest__.py b/addons/l10n_tr/__manifest__.py
index f33a46de98b..4b5d2ac9033 100644
--- a/addons/l10n_tr/__manifest__.py
+++ b/addons/l10n_tr/__manifest__.py
@@ -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'
],
}
diff --git a/addons/l10n_tr/data/res.country.state.csv b/addons/l10n_tr/data/res.country.state.csv
new file mode 100644
index 00000000000..7a749f056b1
--- /dev/null
+++ b/addons/l10n_tr/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" \ No newline at end of file
diff --git a/addons/mail/models/mail_activity.py b/addons/mail/models/mail_activity.py
index 1efffbf061f..b31b906d23f 100644
--- a/addons/mail/models/mail_activity.py
+++ b/addons/mail/models/mail_activity.py
@@ -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 = []
diff --git a/addons/payment_adyen/models/payment.py b/addons/payment_adyen/models/payment.py
index a18bd385f50..818de84dc20 100644
--- a/addons/payment_adyen/models/payment.py
+++ b/addons/payment_adyen/models/payment.py
@@ -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'
@@ -28,6 +53,16 @@ class AcquirerAdyen(models.Model):
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 """
return {
@@ -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,
diff --git a/addons/payment_stripe/static/src/js/stripe.js b/addons/payment_stripe/static/src/js/stripe.js
index 262068fd12d..7d8d20a0590 100644
--- a/addons/payment_stripe/static/src/js/stripe.js
+++ b/addons/payment_stripe/static/src/js/stripe.js
@@ -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',
diff --git a/addons/payment_stripe/views/payment_stripe_templates.xml b/addons/payment_stripe/views/payment_stripe_templates.xml
index 2e3101e5de0..46da1615802 100644
--- a/addons/payment_stripe/views/payment_stripe_templates.xml
+++ b/addons/payment_stripe/views/payment_stripe_templates.xml
@@ -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">
diff --git a/addons/point_of_sale/views/pos_config_view.xml b/addons/point_of_sale/views/pos_config_view.xml
index 8dd29cb7bfe..1350585b135 100644
--- a/addons/point_of_sale/views/pos_config_view.xml
+++ b/addons/point_of_sale/views/pos_config_view.xml
@@ -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">
diff --git a/addons/product/views/product_template_views.xml b/addons/product/views/product_template_views.xml
index 8aa451f6f55..7a6d11d214d 100644
--- a/addons/product/views/product_template_views.xml
+++ b/addons/product/views/product_template_views.xml
@@ -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">
diff --git a/addons/purchase_requisition/models/purchase.py b/addons/purchase_requisition/models/purchase.py
index 7e4d5359e2c..996c7f06fc8 100644
--- a/addons/purchase_requisition/models/purchase.py
+++ b/addons/purchase_requisition/models/purchase.py
@@ -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':
diff --git a/addons/sale/models/account_invoice.py b/addons/sale/models/account_invoice.py
index b0df38a6a53..e995dab5745 100644
--- a/addons/sale/models/account_invoice.py
+++ b/addons/sale/models/account_invoice.py
@@ -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
diff --git a/addons/sale/models/sale.py b/addons/sale/models/sale.py
index 562da160396..260de427823 100644
--- a/addons/sale/models/sale.py
+++ b/addons/sale/models/sale.py
@@ -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):
diff --git a/addons/sale/report/sale_report_templates.xml b/addons/sale/report/sale_report_templates.xml
index b438cbd3db2..edce182dcfc 100644
--- a/addons/sale/report/sale_report_templates.xml
+++ b/addons/sale/report/sale_report_templates.xml
@@ -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"
diff --git a/addons/sale/views/sale_portal_templates.xml b/addons/sale/views/sale_portal_templates.xml
index a5e17031263..66fd9f388fa 100644
--- a/addons/sale/views/sale_portal_templates.xml
+++ b/addons/sale/views/sale_portal_templates.xml
@@ -161,7 +161,7 @@
<t t-if="sale_order.remaining_validity_days &gt; 1">days</t><t t-else="">day</t>
</div>
</li>
- <li t-if="sale_order.amount_undiscounted &gt; sale_order.amount_untaxed" class="list-group-item flex-grow-1">
+ <li t-if="sale_order.amount_undiscounted - sale_order.amount_untaxed &gt; 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"
diff --git a/addons/stock/static/src/js/stock_traceability_report_widgets.js b/addons/stock/static/src/js/stock_traceability_report_widgets.js
index 7a52ea100c1..97468a48135 100644
--- a/addons/stock/static/src/js/stock_traceability_report_widgets.js
+++ b/addons/stock/static/src/js/stock_traceability_report_widgets.js
@@ -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',
diff --git a/addons/stock/views/stock_picking_views.xml b/addons/stock/views/stock_picking_views.xml
index aeafa64444e..c87a786e5e3 100644
--- a/addons/stock/views/stock_picking_views.xml
+++ b/addons/stock/views/stock_picking_views.xml
@@ -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','&lt;', time.strftime('%%Y-%%m-%%d %%H:%%M:%%S'))]" help="Pickings that are late on scheduled time"/>
+ <filter name="late" string="Late" domain="[('scheduled_date','&lt;', 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/>
diff --git a/addons/stock_dropshipping/models/sale.py b/addons/stock_dropshipping/models/sale.py
index b527701a049..c942e5cdca1 100644
--- a/addons/stock_dropshipping/models/sale.py
+++ b/addons/stock_dropshipping/models/sale.py
@@ -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')
diff --git a/addons/stock_picking_batch/report/report_picking_batch.xml b/addons/stock_picking_batch/report/report_picking_batch.xml
index 079ccdf1ed4..4eb4cf2ec62 100644
--- a/addons/stock_picking_batch/report/report_picking_batch.xml
+++ b/addons/stock_picking_batch/report/report_picking_batch.xml
@@ -20,6 +20,7 @@
<thead>
<tr>
<th>Picking Reference</th>
+ <th>Barcode</th>
<th>Status</th>
<th>Commitment Date</th>
<th>Scheduled Date</th>
@@ -31,6 +32,9 @@
<span t-field="pick.name"/>
</td>
<td>
+ <img t-att-src="'/report/barcode/?type=%s&amp;value=%s&amp;width=%s&amp;height=%s' % ('Code128', pick.name, 300, 50)" alt="Barcode"/>
+ </td>
+ <td>
<span t-field="pick.state"/>
</td>
<td>
diff --git a/addons/test_mail/tests/test_mail_gateway.py b/addons/test_mail/tests/test_mail_gateway.py
index 1a796b07501..dea415d6765 100644
--- a/addons/test_mail/tests/test_mail_gateway.py
+++ b/addons/test_mail/tests/test_mail_gateway.py
@@ -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>'
diff --git a/addons/test_mail/tests/test_mail_message.py b/addons/test_mail/tests/test_mail_message.py
index 3b7cf25da46..c5962339f68 100644
--- a/addons/test_mail/tests/test_mail_message.py
+++ b/addons/test_mail/tests/test_mail_message.py
@@ -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({
diff --git a/addons/web/static/src/js/views/calendar/calendar_controller.js b/addons/web/static/src/js/views/calendar/calendar_controller.js
index 0934ae462b3..af31c252898 100644
--- a/addons/web/static/src/js/views/calendar/calendar_controller.js
+++ b/addons/web/static/src/js/views/calendar/calendar_controller.js
@@ -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]);
}
}
diff --git a/addons/web/static/src/js/views/calendar/calendar_renderer.js b/addons/web/static/src/js/views/calendar/calendar_renderer.js
index 03c59885fbe..0ea7d60b1ce 100644
--- a/addons/web/static/src/js/views/calendar/calendar_renderer.js
+++ b/addons/web/static/src/js/views/calendar/calendar_renderer.js
@@ -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', {
diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml
index 131c897bd8c..49b03ce6a3a 100644
--- a/addons/web/static/src/xml/base.xml
+++ b/addons/web/static/src/xml/base.xml
@@ -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>
diff --git a/addons/web_editor/static/src/js/editor/snippets.editor.js b/addons/web_editor/static/src/js/editor/snippets.editor.js
index 6a7b9397206..651e71f63a5 100644
--- a/addons/web_editor/static/src/js/editor/snippets.editor.js
+++ b/addons/web_editor/static/src/js/editor/snippets.editor.js
@@ -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;
diff --git a/addons/web_editor/static/src/js/editor/transcoder.js b/addons/web_editor/static/src/js/editor/transcoder.js
index 85903f6b4eb..745e8492b11 100644
--- a/addons/web_editor/static/src/js/editor/transcoder.js
+++ b/addons/web_editor/static/src/js/editor/transcoder.js
@@ -174,7 +174,7 @@ function getMatchedCSSRules(a) {
break;
}
$el = $el.parent();
- } while (!$el.is('html'));
+ } while ($el.length && !$el.is('html'));
}
return style;
diff --git a/addons/web_editor/static/src/scss/web_editor.common.scss b/addons/web_editor/static/src/scss/web_editor.common.scss
index c082e823cc0..a1de126b4e4 100644
--- a/addons/web_editor/static/src/scss/web_editor.common.scss
+++ b/addons/web_editor/static/src/scss/web_editor.common.scss
@@ -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);
diff --git a/addons/website_event/controllers/main.py b/addons/website_event/controllers/main.py
index 9e86e2ddeba..acff56f9321 100644
--- a/addons/website_event/controllers/main.py
+++ b/addons/website_event/controllers/main.py
@@ -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,
diff --git a/addons/website_event_sale/controllers/main.py b/addons/website_event_sale/controllers/main.py
index 689fe5cdc81..acf5f58be2d 100644
--- a/addons/website_event_sale/controllers/main.py
+++ b/addons/website_event_sale/controllers/main.py
@@ -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):
diff --git a/addons/website_forum/static/src/scss/website_forum.scss b/addons/website_forum/static/src/scss/website_forum.scss
index 36b3c72e2ff..e5953d91679 100644
--- a/addons/website_forum/static/src/scss/website_forum.scss
+++ b/addons/website_forum/static/src/scss/website_forum.scss
@@ -243,3 +243,7 @@ img.o_forum_avatar_big {
min-height: 75px;
}
}
+
+.website_forum {
+ margin-bottom: $spacer;
+}
diff --git a/doc/cla/corporate/inspur.md b/doc/cla/corporate/inspur.md
index 88999bdf10b..0a6ad6352c1 100644
--- a/doc/cla/corporate/inspur.md
+++ b/doc/cla/corporate/inspur.md
@@ -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 \ No newline at end of file
diff --git a/doc/cla/individual/iledarn.md b/doc/cla/individual/iledarn.md
new file mode 100644
index 00000000000..0f174ec0322
--- /dev/null
+++ b/doc/cla/individual/iledarn.md
@@ -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
diff --git a/doc/webservices/upgrade.rst b/doc/webservices/upgrade.rst
index fa168935b23..92984b4b29d 100644
--- a/doc/webservices/upgrade.rst
+++ b/doc/webservices/upgrade.rst
@@ -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,25 +411,66 @@ 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()
+ r = requests.get(PROCESS_URL, data=fields)
+ print(r.text)
+
+ .. code-block:: bash
+
+ PROCESS_URL="https://upgrade.odoo.com/database/v1/process"
+ KEY="Aw7pItGVKFuZ_FOR3U8VFQ=="
+ REQUEST_ID="10534"
+ URL_PARAMS="key=${KEY}&request=${REQUEST_ID}"
+ curl -sS "${PROCESS_URL}?${URL_PARAMS}"
+
+.. _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
- # transform output into a dict:
- response = json.loads(data.getvalue())
+ Skip the tests, deliver the upgraded dump, and set the state to 'delivered'
- # get http status:
- http_code = c.getinfo(pycurl.HTTP_CODE)
- c.close()
+ :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/process"
+ PROCESS_URL="https://upgrade.odoo.com/database/v1/skip_test"
KEY="Aw7pItGVKFuZ_FOR3U8VFQ=="
REQUEST_ID="10534"
URL_PARAMS="key=${KEY}&request=${REQUEST_ID}"
@@ -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",
diff --git a/odoo/addons/base/models/ir_logging.py b/odoo/addons/base/models/ir_logging.py
index 8d8a21f6d6c..04acbe33873 100644
--- a/odoo/addons/base/models/ir_logging.py
+++ b/odoo/addons/base/models/ir_logging.py
@@ -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")
diff --git a/odoo/addons/base/models/res_partner.py b/odoo/addons/base/models/res_partner.py
index 916981838fa..15f16efdebb 100644
--- a/odoo/addons/base/models/res_partner.py
+++ b/odoo/addons/base/models/res_partner.py
@@ -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 ''
diff --git a/odoo/addons/base/tests/test_translate.py b/odoo/addons/base/tests/test_translate.py
index 0ae6ab1007f..f6a556be937 100644
--- a/odoo/addons/base/tests/test_translate.py
+++ b/odoo/addons/base/tests/test_translate.py
@@ -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 = []
diff --git a/odoo/service/server.py b/odoo/service/server.py
index abfbe595cb3..4b05d197a09 100644
--- a/odoo/service/server.py
+++ b/odoo/service/server.py
@@ -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
diff --git a/odoo/tools/translate.py b/odoo/tools/translate.py
index e20ebf9fed7..5760a39f61c 100644
--- a/odoo/tools/translate.py
+++ b/odoo/tools/translate.py
@@ -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)