[MERGE] forward port branch saas-11.3 up to e9068c290b
This commit is contained in:
@@ -51,13 +51,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)
|
||||
|
||||
@@ -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())])
|
||||
|
||||
@@ -13,8 +13,8 @@ Bu modül kurulduktan sonra, Muhasebe yapılandırma sihirbazı çalışır
|
||||
* Sihirbaz sizden hesap planı şablonu, planın kurulacağı şirket, banka hesap
|
||||
bilgileriniz, ilgili para birimi gibi bilgiler isteyecek.
|
||||
""",
|
||||
'author': 'Ahmet Altınışık',
|
||||
'maintainer':'https://launchpad.net/~openerp-turkey',
|
||||
'author': 'Ahmet Altınışık, Can Tecim',
|
||||
'maintainer':'https://launchpad.net/~openerp-turkey, http://www.cantecim.com',
|
||||
'depends': [
|
||||
'account',
|
||||
],
|
||||
@@ -25,5 +25,6 @@ Bu modül kurulduktan sonra, Muhasebe yapılandırma sihirbazı çalışır
|
||||
'data/account_data.xml',
|
||||
'data/account_tax_template_data.xml',
|
||||
'data/account_chart_template_data.xml',
|
||||
'data/res.country.state.csv'
|
||||
],
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
"id","country_id:id","name","code"
|
||||
state_tr_01,base.tr,"Adana","01"
|
||||
state_tr_02,base.tr,"Adıyaman","02"
|
||||
state_tr_03,base.tr,"Afyon","03"
|
||||
state_tr_04,base.tr,"Ağrı","04"
|
||||
state_tr_05,base.tr,"Amasya","05"
|
||||
state_tr_06,base.tr,"Ankara","06"
|
||||
state_tr_07,base.tr,"Antalya","07"
|
||||
state_tr_08,base.tr,"Artvin","08"
|
||||
state_tr_09,base.tr,"Aydın","09"
|
||||
state_tr_10,base.tr,"Balıkesir","10"
|
||||
state_tr_11,base.tr,"Bilecik","11"
|
||||
state_tr_12,base.tr,"Bingöl","12"
|
||||
state_tr_13,base.tr,"Bitlis","13"
|
||||
state_tr_14,base.tr,"Bolu","14"
|
||||
state_tr_15,base.tr,"Burdur","15"
|
||||
state_tr_16,base.tr,"Bursa","16"
|
||||
state_tr_17,base.tr,"Çanakkale","17"
|
||||
state_tr_18,base.tr,"Çankırı","18"
|
||||
state_tr_19,base.tr,"Çorum","19"
|
||||
state_tr_20,base.tr,"Denizli","20"
|
||||
state_tr_21,base.tr,"Diyarbakır","21"
|
||||
state_tr_22,base.tr,"Edirne","22"
|
||||
state_tr_23,base.tr,"Elazığ","23"
|
||||
state_tr_24,base.tr,"Erzincan","24"
|
||||
state_tr_25,base.tr,"Erzurum","25"
|
||||
state_tr_26,base.tr,"Eskişehir","26"
|
||||
state_tr_27,base.tr,"Gaziantep","27"
|
||||
state_tr_28,base.tr,"Giresun","28"
|
||||
state_tr_29,base.tr,"Gümüşhane","29"
|
||||
state_tr_30,base.tr,"Hakkari","30"
|
||||
state_tr_31,base.tr,"Hatay","31"
|
||||
state_tr_32,base.tr,"Isparta","32"
|
||||
state_tr_33,base.tr,"İçel","33"
|
||||
state_tr_34,base.tr,"İstanbul","34"
|
||||
state_tr_35,base.tr,"İzmir","35"
|
||||
state_tr_36,base.tr,"Kars","36"
|
||||
state_tr_37,base.tr,"Kastamonu","37"
|
||||
state_tr_38,base.tr,"Kayseri","38"
|
||||
state_tr_39,base.tr,"Kırklareli","39"
|
||||
state_tr_40,base.tr,"Kırşehir","40"
|
||||
state_tr_41,base.tr,"Kocaeli","41"
|
||||
state_tr_42,base.tr,"Konya","42"
|
||||
state_tr_43,base.tr,"Kütahya","43"
|
||||
state_tr_44,base.tr,"Malatya","44"
|
||||
state_tr_45,base.tr,"Manisa","45"
|
||||
state_tr_46,base.tr,"K.maraş","46"
|
||||
state_tr_47,base.tr,"Mardin","47"
|
||||
state_tr_48,base.tr,"Muğla","48"
|
||||
state_tr_49,base.tr,"Muş","49"
|
||||
state_tr_50,base.tr,"Nevşehir","50"
|
||||
state_tr_51,base.tr,"Niğde","51"
|
||||
state_tr_52,base.tr,"Ordu","52"
|
||||
state_tr_53,base.tr,"Rize","53"
|
||||
state_tr_54,base.tr,"Sakarya","54"
|
||||
state_tr_55,base.tr,"Samsun","55"
|
||||
state_tr_56,base.tr,"Siirt","56"
|
||||
state_tr_57,base.tr,"Sinop","57"
|
||||
state_tr_58,base.tr,"Sivas","58"
|
||||
state_tr_59,base.tr,"Tekirdağ","59"
|
||||
state_tr_60,base.tr,"Tokat","60"
|
||||
state_tr_61,base.tr,"Trabzon","61"
|
||||
state_tr_62,base.tr,"Tunceli","62"
|
||||
state_tr_63,base.tr,"Şanlıurfa","63"
|
||||
state_tr_64,base.tr,"Uşak","64"
|
||||
state_tr_65,base.tr,"Van","65"
|
||||
state_tr_66,base.tr,"Yozgat","66"
|
||||
state_tr_67,base.tr,"Zonguldak","67"
|
||||
state_tr_68,base.tr,"Aksaray","68"
|
||||
state_tr_69,base.tr,"Bayburt","69"
|
||||
state_tr_70,base.tr,"Karaman","70"
|
||||
state_tr_71,base.tr,"Kırıkkale","71"
|
||||
state_tr_72,base.tr,"Batman","72"
|
||||
state_tr_73,base.tr,"Şırnak","73"
|
||||
state_tr_74,base.tr,"Bartın","74"
|
||||
state_tr_75,base.tr,"Ardahan","75"
|
||||
state_tr_76,base.tr,"Iğdır","76"
|
||||
state_tr_77,base.tr,"Yalova","77"
|
||||
state_tr_78,base.tr,"Karabük","78"
|
||||
state_tr_79,base.tr,"Kilis","79"
|
||||
state_tr_80,base.tr,"Osmaniye","80"
|
||||
state_tr_81,base.tr,"Düzce","81"
|
||||
|
@@ -230,7 +230,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)
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -161,7 +161,7 @@
|
||||
<t t-if="sale_order.remaining_validity_days > 1">days</t><t t-else="">day</t>
|
||||
</div>
|
||||
</li>
|
||||
<li t-if="sale_order.amount_undiscounted > sale_order.amount_untaxed" class="list-group-item flex-grow-1">
|
||||
<li t-if="sale_order.amount_undiscounted - sale_order.amount_untaxed > 0.01" class="list-group-item flex-grow-1">
|
||||
<small><b class="text-muted">Your advantage</b></small>
|
||||
<small>
|
||||
<b t-field="sale_order.amount_undiscounted"
|
||||
|
||||
@@ -145,7 +145,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', readonly=False)
|
||||
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', readonly=False)
|
||||
show_details_visible = fields.Boolean('Details Visible', compute='_compute_show_details_visible')
|
||||
|
||||
@@ -360,7 +360,7 @@
|
||||
<filter name="done" string="Done" domain="[('state','=','done')]" help="Pickings already processed"/>
|
||||
<filter name="cancel" string="Cancelled" domain="[('state','=', 'cancel')]" help="Cancelled Moves"/>
|
||||
<separator/>
|
||||
<filter name="late" string="Late" domain="[('scheduled_date','<', time.strftime('%%Y-%%m-%%d %%H:%%M:%%S'))]" help="Pickings that are late on scheduled time"/>
|
||||
<filter name="late" string="Late" domain="[('scheduled_date','<', time.strftime('%%Y-%%m-%%d %%H:%%M:%%S')), ('state', 'in', ('assigned', 'waiting', 'confirmed'))]" help="Pickings that are late on scheduled time"/>
|
||||
<separator/>
|
||||
<filter name="backorder" string="Backorders" domain="[('backorder_id','!=', False)]" help="Remaining parts of picking partially processed"/>
|
||||
<separator/>
|
||||
|
||||
@@ -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>'
|
||||
|
||||
@@ -30,7 +30,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')
|
||||
|
||||
@@ -38,7 +38,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))
|
||||
|
||||
@@ -49,7 +49,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))
|
||||
|
||||
@@ -60,7 +60,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))
|
||||
|
||||
@@ -71,7 +71,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))
|
||||
|
||||
@@ -85,7 +85,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))
|
||||
|
||||
@@ -99,7 +99,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))
|
||||
|
||||
@@ -109,9 +109,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({
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,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")
|
||||
|
||||
+95
-27
@@ -25,9 +25,25 @@ if os.name == 'posix':
|
||||
# Unix only for workers
|
||||
import fcntl
|
||||
import resource
|
||||
try:
|
||||
import inotify
|
||||
from inotify.adapters import InotifyTrees
|
||||
from inotify.constants import IN_MODIFY, IN_CREATE, IN_MOVED_TO
|
||||
INOTIFY_LISTEN_EVENTS = IN_MODIFY | IN_CREATE | IN_MOVED_TO
|
||||
except ImportError:
|
||||
inotify = None
|
||||
else:
|
||||
# Windows shim
|
||||
signal.SIGHUP = -1
|
||||
inotify = None
|
||||
|
||||
if not inotify:
|
||||
try:
|
||||
import watchdog
|
||||
from watchdog.observers import Observer
|
||||
from watchdog.events import FileCreatedEvent, FileModifiedEvent, FileMovedEvent
|
||||
except ImportError:
|
||||
watchdog = None
|
||||
|
||||
# Optional process names for workers
|
||||
try:
|
||||
@@ -45,13 +61,6 @@ from odoo.tools import stripped_sys_argv, dumpstacks, log_ormcache_stats
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
import watchdog
|
||||
from watchdog.observers import Observer
|
||||
from watchdog.events import FileCreatedEvent, FileModifiedEvent, FileMovedEvent
|
||||
except ImportError:
|
||||
watchdog = None
|
||||
|
||||
SLEEP_INTERVAL = 60 # 1 min
|
||||
|
||||
def memory_info(process):
|
||||
@@ -176,7 +185,24 @@ class ThreadedWSGIServerReloadable(LoggingBaseWSGIServerMixIn, werkzeug.serving.
|
||||
#----------------------------------------------------------
|
||||
# FileSystem Watcher for autoreload and cache invalidation
|
||||
#----------------------------------------------------------
|
||||
class FSWatcher(object):
|
||||
class FSWatcherBase(object):
|
||||
def handle_file(self, path):
|
||||
if path.endswith('.py') and not os.path.basename(path).startswith('.~'):
|
||||
try:
|
||||
source = open(path, 'rb').read() + b'\n'
|
||||
compile(source, path, 'exec')
|
||||
except IOError:
|
||||
_logger.error('autoreload: python code change detected, IOError for %s', path)
|
||||
except SyntaxError:
|
||||
_logger.error('autoreload: python code change detected, SyntaxError in %s', path)
|
||||
else:
|
||||
if not getattr(odoo, 'phoenix', False):
|
||||
_logger.info('autoreload: python code updated, autoreload activated')
|
||||
restart()
|
||||
return True
|
||||
|
||||
|
||||
class FSWatcherWatchdog(FSWatcherBase):
|
||||
def __init__(self):
|
||||
self.observer = Observer()
|
||||
for path in odoo.modules.module.ad_paths:
|
||||
@@ -187,27 +213,60 @@ class FSWatcher(object):
|
||||
if isinstance(event, (FileCreatedEvent, FileModifiedEvent, FileMovedEvent)):
|
||||
if not event.is_directory:
|
||||
path = getattr(event, 'dest_path', event.src_path)
|
||||
if path.endswith('.py') and not os.path.basename(path).startswith('.~'):
|
||||
try:
|
||||
source = open(path, 'rb').read() + b'\n'
|
||||
compile(source, path, 'exec')
|
||||
except FileNotFoundError:
|
||||
_logger.error('autoreload: python code change detected, FileNotFound for %s', path)
|
||||
except SyntaxError:
|
||||
_logger.error('autoreload: python code change detected, SyntaxError in %s', path)
|
||||
else:
|
||||
if not getattr(odoo, 'phoenix', False):
|
||||
_logger.info('autoreload: python code updated, autoreload activated')
|
||||
restart()
|
||||
self.handle_file(path)
|
||||
|
||||
def start(self):
|
||||
self.observer.start()
|
||||
_logger.info('AutoReload watcher running')
|
||||
_logger.info('AutoReload watcher running with watchdog')
|
||||
|
||||
def stop(self):
|
||||
self.observer.stop()
|
||||
self.observer.join()
|
||||
|
||||
|
||||
class FSWatcherInotify(FSWatcherBase):
|
||||
def __init__(self):
|
||||
self.started = False
|
||||
# ignore warnings from inotify in case we have duplicate addons paths.
|
||||
inotify.adapters._LOGGER.setLevel(logging.ERROR)
|
||||
# recreate a list as InotifyTrees' __init__ deletes the list's items
|
||||
paths_to_watch = []
|
||||
for path in odoo.modules.module.ad_paths:
|
||||
paths_to_watch.append(path)
|
||||
_logger.info('Watching addons folder %s', path)
|
||||
self.watcher = InotifyTrees(paths_to_watch, mask=INOTIFY_LISTEN_EVENTS, block_duration_s=.5)
|
||||
|
||||
def run(self):
|
||||
_logger.info('AutoReload watcher running with inotify')
|
||||
dir_creation_events = set(('IN_MOVED_TO', 'IN_CREATE'))
|
||||
while self.started:
|
||||
for event in self.watcher.event_gen(timeout_s=0, yield_nones=False):
|
||||
(_, type_names, path, filename) = event
|
||||
if 'IN_ISDIR' not in type_names:
|
||||
# despite not having IN_DELETE in the watcher's mask, the
|
||||
# watcher sends these events when a directory is deleted.
|
||||
if 'IN_DELETE' not in type_names:
|
||||
full_path = os.path.join(path, filename)
|
||||
if self.handle_file(full_path):
|
||||
return
|
||||
elif dir_creation_events.intersection(type_names):
|
||||
full_path = os.path.join(path, filename)
|
||||
for root, _, files in os.walk(full_path):
|
||||
for file in files:
|
||||
if self.handle_file(os.path.join(root, file)):
|
||||
return
|
||||
|
||||
def start(self):
|
||||
self.started = True
|
||||
self.thread = threading.Thread(target=self.run, name="odoo.service.autoreload.watcher")
|
||||
self.thread.setDaemon(True)
|
||||
self.thread.start()
|
||||
|
||||
def stop(self):
|
||||
self.started = False
|
||||
self.thread.join()
|
||||
|
||||
|
||||
#----------------------------------------------------------
|
||||
# Servers: Threaded, Gevented and Prefork
|
||||
#----------------------------------------------------------
|
||||
@@ -878,6 +937,8 @@ class Worker(object):
|
||||
while self.alive:
|
||||
self.multi.pipe_ping(self.watchdog_pipe)
|
||||
self.sleep()
|
||||
if not self.alive:
|
||||
break
|
||||
self.process_work()
|
||||
self.check_limits()
|
||||
except:
|
||||
@@ -1108,21 +1169,28 @@ def start(preload=None, stop=False):
|
||||
server = ThreadedServer(odoo.service.wsgi_server.application)
|
||||
|
||||
watcher = None
|
||||
if 'reload' in config['dev_mode']:
|
||||
if watchdog:
|
||||
watcher = FSWatcher()
|
||||
if 'reload' in config['dev_mode'] and not odoo.evented:
|
||||
if inotify:
|
||||
watcher = FSWatcherInotify()
|
||||
watcher.start()
|
||||
elif watchdog:
|
||||
watcher = FSWatcherWatchdog()
|
||||
watcher.start()
|
||||
else:
|
||||
_logger.warning("'watchdog' module not installed. Code autoreload feature is disabled")
|
||||
if os.name == 'posix' and platform.system() != 'Darwin':
|
||||
module = 'inotify'
|
||||
else:
|
||||
module = 'watchdog'
|
||||
_logger.warning("'%s' module not installed. Code autoreload feature is disabled", module)
|
||||
if 'werkzeug' in config['dev_mode']:
|
||||
server.app = DebuggedApplication(server.app, evalex=True)
|
||||
|
||||
rc = server.run(preload, stop)
|
||||
|
||||
if watcher:
|
||||
watcher.stop()
|
||||
# like the legend of the phoenix, all ends with beginnings
|
||||
if getattr(odoo, 'phoenix', False):
|
||||
if watcher:
|
||||
watcher.stop()
|
||||
_reexec()
|
||||
|
||||
return rc if rc else 0
|
||||
|
||||
Reference in New Issue
Block a user