[MERGE] forward port branch 12.0 up to 11b1e12cde

This commit is contained in:
Christophe Simonis
2019-03-25 19:45:26 +01:00
50 changed files with 568 additions and 198 deletions
+1 -2
View File
@@ -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)
}
+6 -6
View File
@@ -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)
+1 -1
View File
@@ -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 />
+10 -2
View File
@@ -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
+7
View File
@@ -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, ...)
# ----------------------------------------
+9 -4
View File
@@ -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)
+1 -1
View File
@@ -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())])
+1
View File
@@ -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>
+1 -1
View File
@@ -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"
+1 -1
View File
@@ -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,
}])
+3 -2
View File
@@ -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'
],
}
+82
View File
@@ -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"
1 id country_id:id name code
2 state_tr_01 base.tr Adana 01
3 state_tr_02 base.tr Adıyaman 02
4 state_tr_03 base.tr Afyon 03
5 state_tr_04 base.tr Ağrı 04
6 state_tr_05 base.tr Amasya 05
7 state_tr_06 base.tr Ankara 06
8 state_tr_07 base.tr Antalya 07
9 state_tr_08 base.tr Artvin 08
10 state_tr_09 base.tr Aydın 09
11 state_tr_10 base.tr Balıkesir 10
12 state_tr_11 base.tr Bilecik 11
13 state_tr_12 base.tr Bingöl 12
14 state_tr_13 base.tr Bitlis 13
15 state_tr_14 base.tr Bolu 14
16 state_tr_15 base.tr Burdur 15
17 state_tr_16 base.tr Bursa 16
18 state_tr_17 base.tr Çanakkale 17
19 state_tr_18 base.tr Çankırı 18
20 state_tr_19 base.tr Çorum 19
21 state_tr_20 base.tr Denizli 20
22 state_tr_21 base.tr Diyarbakır 21
23 state_tr_22 base.tr Edirne 22
24 state_tr_23 base.tr Elazığ 23
25 state_tr_24 base.tr Erzincan 24
26 state_tr_25 base.tr Erzurum 25
27 state_tr_26 base.tr Eskişehir 26
28 state_tr_27 base.tr Gaziantep 27
29 state_tr_28 base.tr Giresun 28
30 state_tr_29 base.tr Gümüşhane 29
31 state_tr_30 base.tr Hakkari 30
32 state_tr_31 base.tr Hatay 31
33 state_tr_32 base.tr Isparta 32
34 state_tr_33 base.tr İçel 33
35 state_tr_34 base.tr İstanbul 34
36 state_tr_35 base.tr İzmir 35
37 state_tr_36 base.tr Kars 36
38 state_tr_37 base.tr Kastamonu 37
39 state_tr_38 base.tr Kayseri 38
40 state_tr_39 base.tr Kırklareli 39
41 state_tr_40 base.tr Kırşehir 40
42 state_tr_41 base.tr Kocaeli 41
43 state_tr_42 base.tr Konya 42
44 state_tr_43 base.tr Kütahya 43
45 state_tr_44 base.tr Malatya 44
46 state_tr_45 base.tr Manisa 45
47 state_tr_46 base.tr K.maraş 46
48 state_tr_47 base.tr Mardin 47
49 state_tr_48 base.tr Muğla 48
50 state_tr_49 base.tr Muş 49
51 state_tr_50 base.tr Nevşehir 50
52 state_tr_51 base.tr Niğde 51
53 state_tr_52 base.tr Ordu 52
54 state_tr_53 base.tr Rize 53
55 state_tr_54 base.tr Sakarya 54
56 state_tr_55 base.tr Samsun 55
57 state_tr_56 base.tr Siirt 56
58 state_tr_57 base.tr Sinop 57
59 state_tr_58 base.tr Sivas 58
60 state_tr_59 base.tr Tekirdağ 59
61 state_tr_60 base.tr Tokat 60
62 state_tr_61 base.tr Trabzon 61
63 state_tr_62 base.tr Tunceli 62
64 state_tr_63 base.tr Şanlıurfa 63
65 state_tr_64 base.tr Uşak 64
66 state_tr_65 base.tr Van 65
67 state_tr_66 base.tr Yozgat 66
68 state_tr_67 base.tr Zonguldak 67
69 state_tr_68 base.tr Aksaray 68
70 state_tr_69 base.tr Bayburt 69
71 state_tr_70 base.tr Karaman 70
72 state_tr_71 base.tr Kırıkkale 71
73 state_tr_72 base.tr Batman 72
74 state_tr_73 base.tr Şırnak 73
75 state_tr_74 base.tr Bartın 74
76 state_tr_75 base.tr Ardahan 75
77 state_tr_76 base.tr Iğdır 76
78 state_tr_77 base.tr Yalova 77
79 state_tr_78 base.tr Karabük 78
80 state_tr_79 base.tr Kilis 79
81 state_tr_80 base.tr Osmaniye 80
82 state_tr_81 base.tr Düzce 81
+7 -4
View File
@@ -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 = []
+38 -2
View File
@@ -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':
+2 -1
View File
@@ -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
+30 -5
View File
@@ -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):
+3 -3
View File
@@ -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"
+1 -1
View File
@@ -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"
@@ -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',
+1 -1
View File
@@ -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/>
+1 -1
View File
@@ -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&amp;value=%s&amp;width=%s&amp;height=%s' % ('Code128', pick.name, 300, 50)" alt="Barcode"/>
</td>
<td>
<span t-field="pick.state"/>
</td>
+4 -4
View File
@@ -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>'
+10 -10
View File
@@ -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', {
+1 -1
View File
@@ -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);
+1 -4
View File
@@ -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;
}
+1
View File
@@ -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
+11
View File
@@ -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
View File
@@ -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",
+22 -3
View File
@@ -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")
+22
View File
@@ -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 ''
+13
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -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)