[MERGE] forward port branch 11.0 up to 0196a6feb5

This commit is contained in:
Christophe Simonis
2019-03-25 16:47:05 +01:00
18 changed files with 315 additions and 70 deletions
+9 -4
View File
@@ -50,13 +50,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', 'probability', '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'] * datum['probability'] / 100) for datum in opportunity_data}
]).read(['planned_revenue', 'probability', '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) * datum.get('probability', 0) / 100.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
@@ -270,7 +270,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())])
+3 -2
View File
@@ -13,9 +13,9 @@ 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',
'author': 'Ahmet Altınışık, Can Tecim',
'maintainer':'https://launchpad.net/~openerp-turkey',
'website':'https://launchpad.net/openerp-turkey',
'website':'https://launchpad.net/openerp-turkey, http://www.cantecim.com',
'depends': [
'account',
],
@@ -24,5 +24,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
+22 -9
View File
@@ -668,15 +668,28 @@ class MassMailing(models.Model):
"""Returns a set of emails already targeted by current mailing/campaign (no duplicates)"""
self.ensure_one()
target = self.env[self.mailing_model_real]
mail_field = 'email' if 'email' in target._fields else 'email_from'
# avoid loading a large number of records in memory
# + use a basic heuristic for extracting emails
query = """
SELECT lower(substring(%(mail_field)s, '([^ ,;<@]+@[^> ,;]+)'))
FROM mail_mail_statistics s
JOIN %(target)s t ON (s.res_id = t.id)
WHERE substring(%(mail_field)s, '([^ ,;<@]+@[^> ,;]+)') IS NOT NULL
"""
if set(['email', 'email_from']) & set(target._fields):
mail_field = 'email' if 'email' in target._fields else 'email_from'
# avoid loading a large number of records in memory
# + use a basic heuristic for extracting emails
query = """
SELECT lower(substring(%(mail_field)s, '([^ ,;<@]+@[^> ,;]+)'))
FROM mail_mail_statistics s
JOIN %(target)s t ON (s.res_id = t.id)
WHERE substring(%(mail_field)s, '([^ ,;<@]+@[^> ,;]+)') IS NOT NULL
"""
elif 'partner_id' in target._fields:
mail_field = 'email'
query = """
SELECT lower(substring(p.%(mail_field)s, '([^ ,;<@]+@[^> ,;]+)'))
FROM mail_mail_statistics s
JOIN %(target)s t ON (s.res_id = t.id)
JOIN res_partner p ON (t.partner_id = p.id)
WHERE substring(p.%(mail_field)s, '([^ ,;<@]+@[^> ,;]+)') IS NOT NULL
"""
else:
raise UserError(_("Unsupported mass mailing model %s") % self.mailing_model_id.name)
if self.mass_mailing_campaign_id.unique_ab_testing:
query +="""
AND s.mass_mailing_campaign_id = %%(mailing_campaign_id)s;
+1 -1
View File
@@ -246,7 +246,7 @@ class MrpWorkorder(models.Model):
@api.multi
def write(self, values):
if ('date_planned_start' in values or 'date_planned_finished' in values) and any(workorder.state == 'done' for workorder in self):
if list(values.keys()) != ['time_ids'] and any(workorder.state == 'done' for workorder in self):
raise UserError(_('You can not change the finished work order.'))
return super(MrpWorkorder, self).write(values)
+38 -2
View File
@@ -18,6 +18,31 @@ from odoo.tools.pycompat import to_native
_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">
+1 -1
View File
@@ -147,7 +147,7 @@ class StockMove(models.Model):
restrict_partner_id = fields.Many2one('res.partner', 'Owner ', help="Technical field used to depict a restriction on the ownership of quants to consider when marking this move as 'done'")
route_ids = fields.Many2many('stock.location.route', 'stock_location_route_move', 'move_id', 'route_id', 'Destination route', help="Preferred route")
warehouse_id = fields.Many2one('stock.warehouse', 'Warehouse', help="Technical field depicting the warehouse to consider for the route selection on the next procurement (if any).")
has_tracking = fields.Selection(related='product_id.tracking', string='Product with Tracking')
has_tracking = fields.Selection(related='product_id.tracking', string='Product with Tracking', readonly=True)
quantity_done = fields.Float('Quantity Done', compute='_quantity_done_compute', digits=dp.get_precision('Product Unit of Measure'), inverse='_quantity_done_set')
show_operations = fields.Boolean(related='picking_id.picking_type_id.show_operations')
show_details_visible = fields.Boolean('Details Visible', compute='_compute_show_details_visible')
+1 -1
View File
@@ -353,7 +353,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/>
+4 -4
View File
@@ -499,7 +499,7 @@ class TestMailgateway(BaseFunctionalTest, MockEmails):
self.assertEqual(msg.model, False,
'message_post: private discussion: context key "thread_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(
@@ -527,9 +527,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
@@ -28,7 +28,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')
@@ -36,7 +36,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))
@@ -47,7 +47,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))
@@ -58,7 +58,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')
self.assertEqual(msg.reply_to, '%s <%s@%s>' % (self.env.user.company_id.name, alias_catchall, alias_domain))
self.assertEqual(msg.email_from, '%s <%s>' % (self.user_employee.name, self.user_employee.email))
@@ -69,7 +69,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))
@@ -83,7 +83,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 <%s@%s>' % (self.env.user.company_id.name, self.alias_record.name, self.alias_record.alias_name, alias_domain))
self.assertEqual(msg.email_from, '%s <%s>' % (self.user_employee.name, self.user_employee.email))
@@ -97,7 +97,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 <%s@%s>' % (self.env.user.company_id.name, self.alias_record.name, self.alias_record.alias_name, alias_domain))
self.assertEqual(msg.email_from, '%s <%s>' % (self.user_employee.name, self.user_employee.email))
@@ -107,9 +107,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({
@@ -374,6 +374,7 @@ var SnippetEditor = Widget.extend({
*/
_onDragAndDropStart: function () {
var self = this;
this.dropped = false;
self.size = {
width: self.$target.width(),
height: self.$target.height()
@@ -419,10 +420,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;
+1 -1
View File
@@ -272,7 +272,7 @@ class SaleOrderOption(models.Model):
order_line = order.order_line.filtered(lambda line: line.product_id == self.product_id)
if order_line:
order_line = order_line[0]
order_line.product_uom_qty += 1
order_line.product_uom_qty += self.quantity
else:
vals = {
'price_unit': self.price_unit,
@@ -243,7 +243,7 @@
</div>
<div class="text-center mb16" t-if="quotation.amount_undiscounted &gt; quotation.amount_untaxed">
<div class="text-center mb16" t-if="quotation.amount_undiscounted - quotation.amount_untaxed &gt; 0.01">
<p class="text-muted mb8">Your advantage:</p>
<t t-if="quotation.amount_untaxed == quotation.amount_total">
<strong t-field="quotation.amount_total"
+22 -3
View File
@@ -1,13 +1,27 @@
# 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):
_name = 'ir.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)
@@ -16,3 +30,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")
+95 -27
View File
@@ -25,9 +25,25 @@ if os.name == 'posix':
import fcntl
import resource
import psutil
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:
@@ -44,13 +60,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):
@@ -131,7 +140,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:
@@ -142,27 +168,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
#----------------------------------------------------------
@@ -770,6 +829,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:
@@ -995,21 +1056,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