[MERGE] forward port of branch saas-6 up to c2b4766
This commit is contained in:
+16
-14
@@ -123,22 +123,24 @@ class account_fiscal_position(osv.osv):
|
||||
else:
|
||||
delivery = partner
|
||||
|
||||
domain = [
|
||||
('auto_apply', '=', True),
|
||||
'|', ('vat_required', '=', False), ('vat_required', '=', partner.vat_subjected),
|
||||
]
|
||||
if delivery.country_id.id:
|
||||
fiscal_position_ids = self.search(cr, uid, domain + [('country_id', '=', delivery.country_id.id)], context=context, limit=1)
|
||||
domains = [[('auto_apply', '=', True), ('vat_required', '=', partner.vat_subjected)]]
|
||||
if partner.vat_subjected:
|
||||
# Possibly allow fallback to non-VAT positions, if no VAT-required position matches
|
||||
domains += [[('auto_apply', '=', True), ('vat_required', '=', False)]]
|
||||
|
||||
for domain in domains:
|
||||
if delivery.country_id.id:
|
||||
fiscal_position_ids = self.search(cr, uid, domain + [('country_id', '=', delivery.country_id.id)], context=context, limit=1)
|
||||
if fiscal_position_ids:
|
||||
return fiscal_position_ids[0]
|
||||
|
||||
fiscal_position_ids = self.search(cr, uid, domain + [('country_group_id.country_ids', '=', delivery.country_id.id)], context=context, limit=1)
|
||||
if fiscal_position_ids:
|
||||
return fiscal_position_ids[0]
|
||||
|
||||
fiscal_position_ids = self.search(cr, uid, domain + [('country_id', '=', None), ('country_group_id', '=', None)], context=context, limit=1)
|
||||
if fiscal_position_ids:
|
||||
return fiscal_position_ids[0]
|
||||
|
||||
fiscal_position_ids = self.search(cr, uid, domain + [('country_group_id.country_ids', '=', delivery.country_id.id)], context=context, limit=1)
|
||||
if fiscal_position_ids:
|
||||
return fiscal_position_ids[0]
|
||||
|
||||
fiscal_position_ids = self.search(cr, uid, domain + [('country_id', '=', None), ('country_group_id', '=', None)], context=context, limit=1)
|
||||
if fiscal_position_ids:
|
||||
return fiscal_position_ids[0]
|
||||
return False
|
||||
|
||||
class account_fiscal_position_tax(osv.osv):
|
||||
|
||||
@@ -135,16 +135,33 @@ class account_config_settings(osv.osv_memory):
|
||||
'company_id', 'income_currency_exchange_account_id',
|
||||
type='many2one',
|
||||
relation='account.account',
|
||||
string="Gain Exchange Rate Account",
|
||||
domain="[('type', '=', 'other')]"),
|
||||
string="Gain Exchange Rate Account",
|
||||
domain="[('type', '=', 'other'), ('company_id', '=', company_id)]]"),
|
||||
'expense_currency_exchange_account_id': fields.related(
|
||||
'company_id', 'expense_currency_exchange_account_id',
|
||||
type="many2one",
|
||||
relation='account.account',
|
||||
string="Loss Exchange Rate Account",
|
||||
domain="[('type', '=', 'other')]"),
|
||||
domain="[('type', '=', 'other'), ('company_id', '=', company_id)]]"),
|
||||
}
|
||||
|
||||
def _check_account_gain(self, cr, uid, ids, context=None):
|
||||
for obj in self.browse(cr, uid, ids, context=context):
|
||||
if obj.income_currency_exchange_account_id.company_id and obj.company_id != obj.income_currency_exchange_account_id.company_id:
|
||||
return False
|
||||
return True
|
||||
|
||||
def _check_account_loss(self, cr, uid, ids, context=None):
|
||||
for obj in self.browse(cr, uid, ids, context=context):
|
||||
if obj.expense_currency_exchange_account_id.company_id and obj.company_id != obj.expense_currency_exchange_account_id.company_id:
|
||||
return False
|
||||
return True
|
||||
|
||||
_constraints = [
|
||||
(_check_account_gain, 'The company of the gain exchange rate account must be the same than the company selected.', ['income_currency_exchange_account_id']),
|
||||
(_check_account_loss, 'The company of the loss exchange rate account must be the same than the company selected.', ['expense_currency_exchange_account_id']),
|
||||
]
|
||||
|
||||
def _default_company(self, cr, uid, context=None):
|
||||
user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
|
||||
return user.company_id.id
|
||||
|
||||
@@ -2,3 +2,4 @@ from . import test_tax
|
||||
from . import test_search
|
||||
from . import test_reconciliation
|
||||
from . import test_account_move_closed_period
|
||||
from . import test_fiscal_position
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
from openerp.tests.common import TransactionCase
|
||||
|
||||
class TestFiscalPosition(TransactionCase):
|
||||
"""Tests for fiscal positions in auto apply (account.fiscal.position).
|
||||
If a partner has a vat number, the fiscal positions with "vat_required=True"
|
||||
are prefered.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(TestFiscalPosition, self).setUp()
|
||||
self.fiscal_position_model = self.registry('account.fiscal.position')
|
||||
self.res_partner_model = self.registry('res.partner')
|
||||
self.res_country_model = self.registry('res.country')
|
||||
|
||||
def test_fiscal_position(self):
|
||||
cr, uid = self.cr, self.uid
|
||||
company_id = 1
|
||||
country_id = self.res_country_model.search(cr, uid, [('name', '=', 'France')])[0]
|
||||
partner_id = self.res_partner_model.create(cr, uid, dict(
|
||||
name="George",
|
||||
vat_subjected=True,
|
||||
notify_email="always",
|
||||
country_id=country_id))
|
||||
fp_b2c_id = self.fiscal_position_model.create(cr, uid, dict(name="EU-VAT-FR-B2C",
|
||||
auto_apply=True,
|
||||
country_id=country_id,
|
||||
vat_required=False,
|
||||
sequence=1))
|
||||
fp_b2b_id = self.fiscal_position_model.create(cr, uid, dict(name="EU-VAT-FR-B2B",
|
||||
auto_apply=True,
|
||||
country_id=country_id,
|
||||
vat_required=True,
|
||||
sequence=2))
|
||||
res = self.fiscal_position_model.get_fiscal_position(cr, uid, company_id, partner_id)
|
||||
self.assertEquals(fp_b2b_id, res,
|
||||
"Fiscal position detection should pick B2B position as 1rst match")
|
||||
|
||||
self.fiscal_position_model.write(cr, uid, [fp_b2b_id], {'auto_apply': False})
|
||||
res = self.fiscal_position_model.get_fiscal_position(cr, uid, company_id, partner_id)
|
||||
self.assertEquals(fp_b2c_id, res,
|
||||
"Fiscal position detection should pick B2C position as 1rst match")
|
||||
@@ -9,6 +9,7 @@ import operator
|
||||
import psycopg2
|
||||
import re
|
||||
from ast import literal_eval
|
||||
from openerp.exceptions import ValidationError
|
||||
from openerp.tools import mute_logger
|
||||
|
||||
# Validation Library https://pypi.python.org/pypi/validate_email/1.1
|
||||
@@ -246,12 +247,12 @@ class MergePartnerAutomatic(osv.TransientModel):
|
||||
for record in proxy.browse(cr, openerp.SUPERUSER_ID, record_ids, context=context):
|
||||
try:
|
||||
proxy_model = self.pool[record.model]
|
||||
field_type = proxy_model._columns[record.name].__class__._type
|
||||
column = proxy_model._columns[record.name]
|
||||
except KeyError:
|
||||
# unknown model or field => skip
|
||||
continue
|
||||
|
||||
if field_type == 'function':
|
||||
if isinstance(column, fields.function):
|
||||
continue
|
||||
|
||||
for partner in src_partners:
|
||||
|
||||
@@ -23,6 +23,6 @@ access_crm_lead_tag_manager,crm_lead_tag manager,model_crm_lead_tag,base.group_s
|
||||
access_crm_phonecall_category,crm_phonecall_category,model_crm_phonecall_category,base.group_user,1,0,0,0
|
||||
access_crm_phonecall_category_salesman,crm_phonecall_category salesman,model_crm_phonecall_category,base.group_sale_salesman,1,1,1,0
|
||||
access_crm_phonecall_category_manager,crm_phonecall_category manager,model_crm_phonecall_category,base.group_sale_manager,1,1,1,1
|
||||
access_crm_lost_reason,crm.lost.reason,model_crm_lost_reason,base.group_sale_manager,1,1,1,1
|
||||
access_crm_lost_reason,crm.lost.reason,model_crm_lost_reason,base.group_sale_salesman,1,1,1,0
|
||||
access_crm_lost_reason,crm.lost.reason,model_crm_lost_reason,base.group_user,1,0,0,0
|
||||
access_crm_lost_reason_manager,crm.lost.reason.manager,model_crm_lost_reason,base.group_sale_manager,1,1,1,1
|
||||
access_crm_lost_reason_salesman,crm.lost.reason.salesman,model_crm_lost_reason,base.group_sale_salesman,1,1,1,0
|
||||
access_crm_lost_reason_user,crm.lost.reason.user,model_crm_lost_reason,base.group_user,1,0,0,0
|
||||
|
||||
|
@@ -48,11 +48,6 @@ class decimal_precision(orm.Model):
|
||||
def clear_cache(self, cr):
|
||||
"""clear cache and update models. Notify other workers to restart their registry."""
|
||||
self.precision_get.clear_cache(self)
|
||||
env = openerp.api.Environment(cr, SUPERUSER_ID, {})
|
||||
for model in self.pool.values():
|
||||
for field in model._fields.values():
|
||||
if field.type == 'float':
|
||||
field._setup_digits(env)
|
||||
RegistryManager.signal_registry_change(cr.dbname)
|
||||
|
||||
def create(self, cr, uid, data, context=None):
|
||||
|
||||
@@ -287,6 +287,9 @@
|
||||
<field name="event_id" />
|
||||
<field name="state"/>
|
||||
<field name="message_unread" invisible="1"/>
|
||||
<button name="confirm_registration" string="Confirm Registration" states="draft" type="object" icon="gtk-apply"/>
|
||||
<button name="button_reg_close" string="Attended the Event" states="open" type="object" icon="gtk-jump-to"/>
|
||||
<button name="button_reg_cancel" string="Cancel Registration" states="draft,open" type="object" icon="gtk-cancel"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
+1
-1
@@ -86,7 +86,7 @@ class hr_job(osv.Model):
|
||||
_description = "Job Position"
|
||||
_inherit = ['mail.thread']
|
||||
_columns = {
|
||||
'name': fields.char('Job Name', required=True, select=True),
|
||||
'name': fields.char('Job Name', required=True, select=True, translate=True),
|
||||
'expected_employees': fields.function(_get_nbr_employees, string='Total Forecasted Employees',
|
||||
help='Expected number of employees for this job position after new recruitment.',
|
||||
store = {
|
||||
|
||||
@@ -528,12 +528,13 @@ class hr_applicant(osv.Model):
|
||||
'post': True,
|
||||
'notify': True,
|
||||
}, context=compose_ctx)
|
||||
values = self.pool['mail.compose.message'].onchange_template_id(
|
||||
cr, uid, [compose_id], stage.template_id.id, 'mass_mail', self._name, False, context=compose_ctx)['value']
|
||||
if values.get('attachment_ids'):
|
||||
values['attachment_ids'] = [(6, 0, values['attachment_ids'])]
|
||||
self.pool['mail.compose.message'].write(
|
||||
cr, uid, [compose_id],
|
||||
self.pool['mail.compose.message'].onchange_template_id(
|
||||
cr, uid, [compose_id],
|
||||
stage.template_id.id, 'mass_mail', self._name, False,
|
||||
context=compose_ctx)['value'],
|
||||
values,
|
||||
context=compose_ctx)
|
||||
self.pool['mail.compose.message'].send_mail(cr, uid, [compose_id], context=compose_ctx)
|
||||
return res
|
||||
|
||||
@@ -584,7 +584,7 @@ class hr_timesheet_sheet_sheet_day(osv.osv):
|
||||
((
|
||||
select
|
||||
min(hrt.id) as id,
|
||||
'UTC' as timezone,
|
||||
p.tz as timezone,
|
||||
l.date::date as name,
|
||||
s.id as sheet_id,
|
||||
sum(l.unit_amount) as total_timesheet,
|
||||
@@ -594,6 +594,10 @@ class hr_timesheet_sheet_sheet_day(osv.osv):
|
||||
hr_analytic_timesheet hrt
|
||||
JOIN account_analytic_line l ON l.id = hrt.line_id
|
||||
LEFT JOIN hr_timesheet_sheet_sheet s ON s.id = hrt.sheet_id
|
||||
JOIN hr_employee e ON s.employee_id = e.id
|
||||
JOIN resource_resource r ON e.resource_id = r.id
|
||||
LEFT JOIN res_users u ON r.user_id = u.id
|
||||
LEFT JOIN res_partner p ON u.partner_id = p.id
|
||||
group by l.date::date, s.id, timezone
|
||||
) union (
|
||||
select
|
||||
|
||||
@@ -296,13 +296,26 @@ class im_chat_shortcode(osv.Model):
|
||||
'description' : fields.char('Description'),
|
||||
}
|
||||
|
||||
def _sanitize_shorcode(self, substitution):
|
||||
""" Sanitize the shortcode substitution :
|
||||
- HTML substitution : only allow the img tag (smiley)
|
||||
- escape other substitutions to avoid XSS
|
||||
"""
|
||||
is_img_tag = re.match(r'''^<img\s+src=('|")([^'"]*)\1\s*/?>$''', substitution, re.M|re.I)
|
||||
if is_img_tag:
|
||||
return substitution
|
||||
return cgi.escape(substitution)
|
||||
|
||||
def create(self, cr, uid, values, context=None):
|
||||
if values.get('substitution'):
|
||||
values['substitution'] = tools.html_sanitize(values['substitution']).strip()
|
||||
if not values.get('substitution'):
|
||||
raise UserError(_("Substitution contains only forbidden characters. Please fill it with something else."))
|
||||
values['substitution'] = self._sanitize_shorcode(values['substitution'])
|
||||
return super(im_chat_shortcode, self).create(cr, uid, values, context=context)
|
||||
|
||||
def write(self, cr, uid, ids, values, context=None):
|
||||
if values.get('substitution'):
|
||||
values['substitution'] = self._sanitize_shorcode(values['substitution'])
|
||||
return super(im_chat_shortcode, self).write(cr, uid, ids, values, context=context)
|
||||
|
||||
def replace_shortcode(self, cr, uid, message, context=None):
|
||||
ids = self.search(cr, uid, [], context=context)
|
||||
for shortcode in self.browse(cr, uid, ids, context=context):
|
||||
|
||||
@@ -27,7 +27,7 @@ class LivechatController(http.Controller):
|
||||
info["username"] = kwargs.get("username", "Visitor")
|
||||
# find the country from the request
|
||||
country_id = False
|
||||
country_code = request.session.geoip and request.session.geoip.get('country_name') or False
|
||||
country_code = request.session.geoip and request.session.geoip.get('country_code') or False
|
||||
if country_code:
|
||||
country_ids = registry.get('res.country').search(cr, uid, [('code', '=', country_code)], context=context)
|
||||
if country_ids:
|
||||
|
||||
@@ -56,5 +56,25 @@
|
||||
</record>
|
||||
|
||||
|
||||
<!-- Smiley for rating to be displayed in the conversation -->
|
||||
<record id="im_livechat_rating_0" model="im_chat.shortcode">
|
||||
<field name="source">:rating_0</field>
|
||||
<field name="substitution"><img src='/rating/static/src/img/rating_0.png'/></field>
|
||||
<field name="description">Rating Smiley Bad</field>
|
||||
</record>
|
||||
|
||||
<record id="im_livechat_rating_5" model="im_chat.shortcode">
|
||||
<field name="source">:rating_5</field>
|
||||
<field name="substitution"><img src='/rating/static/src/img/rating_5.png'/></field>
|
||||
<field name="description">Rating Smiley Okay</field>
|
||||
</record>
|
||||
|
||||
<record id="im_livechat_rating_10" model="im_chat.shortcode">
|
||||
<field name="source">:rating_10</field>
|
||||
<field name="substitution"><img src='/rating/static/src/img/rating_10.png'/></field>
|
||||
<field name="description">Rating Smiley Good</field>
|
||||
</record>
|
||||
|
||||
|
||||
</data>
|
||||
</openerp>
|
||||
@@ -260,6 +260,7 @@ var Feedback = Widget.extend({
|
||||
return user_session.rpc("/rating/livechat/feedback", {uuid: uuid, rate: this.rating, reason : this.reason}).then(function(res) {
|
||||
if(close){
|
||||
self.trigger("feedback_sent"); // will close the conversation
|
||||
self.conversation.send_message(_.str.sprintf(_t("I rated you with :rating_%d"), self.rating), "message");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -145,6 +145,7 @@
|
||||
|
||||
<!-- the js code to initialize the LiveSupport object -->
|
||||
<template id="loader" name="Javascript initializing the LiveSupport">
|
||||
<t t-translate="off">
|
||||
$(function() {
|
||||
odoo.define('im_livechat.livesupport', function (require) {
|
||||
var im_livechat = require('im_livechat.im_livechat');
|
||||
@@ -164,6 +165,7 @@
|
||||
});
|
||||
odoo.init();
|
||||
});
|
||||
</t>
|
||||
</template>
|
||||
|
||||
</data>
|
||||
|
||||
@@ -189,13 +189,10 @@ class account_invoice(osv.osv):
|
||||
reference_type = vals['reference_type']
|
||||
else:
|
||||
reference_type = inv.reference_type or ''
|
||||
if reference_type == 'bba':
|
||||
if vals.has_key('reference'):
|
||||
bbacomm = vals['reference']
|
||||
else:
|
||||
bbacomm = inv.reference or ''
|
||||
if self.check_bbacomm(bbacomm):
|
||||
reference = re.sub('\D', '', bbacomm)
|
||||
|
||||
if reference_type == 'bba' and 'reference' in vals:
|
||||
if self.check_bbacomm(vals['reference']):
|
||||
reference = re.sub('\D', '', vals['reference'])
|
||||
vals['reference'] = '+++' + reference[0:3] + '/' + reference[3:7] + '/' + reference[7:] + '+++'
|
||||
same_ids = self.search(cr, uid,
|
||||
[('id', '!=', inv.id), ('type', '=', 'out_invoice'),
|
||||
|
||||
@@ -151,7 +151,8 @@ class l10n_eu_service(models.TransientModel):
|
||||
'ref_tax_code_id': tx_code.id,
|
||||
'ref_base_sign': -1,
|
||||
'ref_tax_sign': -1,
|
||||
'description': "EU-VAT-%s-S" % country.code
|
||||
'description': "EU-VAT-%s-S" % country.code,
|
||||
'sequence': 1000,
|
||||
}
|
||||
tax = account_tax.create(data_tax)
|
||||
if self.fiscal_position_id:
|
||||
|
||||
@@ -1820,7 +1820,17 @@ class mail_thread(osv.AbstractModel):
|
||||
('model', '=', self._name),
|
||||
('res_id', '=', record_id)], limit=1, context=context)
|
||||
if msg_ids:
|
||||
self.pool.get('mail.notification')._notify(cr, uid, msg_ids[0], partners_to_notify=partner_ids, context=context)
|
||||
notification_obj = self.pool.get('mail.notification')
|
||||
notification_obj._notify(cr, uid, msg_ids[0], partners_to_notify=partner_ids, context=context)
|
||||
message = message_obj.browse(cr, uid, msg_ids[0], context=context)
|
||||
if message.parent_id:
|
||||
partner_ids_to_parent_notify = set(partner_ids).difference(partner.id for partner in message.parent_id.notified_partner_ids)
|
||||
for partner_id in partner_ids_to_parent_notify:
|
||||
notification_obj.create(cr, uid, {
|
||||
'message_id': message.parent_id.id,
|
||||
'partner_id': partner_id,
|
||||
'is_read': True,
|
||||
}, context=context)
|
||||
|
||||
def message_auto_subscribe(self, cr, uid, ids, updated_fields, context=None, values=None):
|
||||
""" Handle auto subscription. Two methods for auto subscription exist:
|
||||
@@ -1902,6 +1912,7 @@ class mail_thread(osv.AbstractModel):
|
||||
|
||||
self._message_auto_subscribe_notify(cr, uid, ids, user_pids, context=context)
|
||||
|
||||
|
||||
return True
|
||||
|
||||
#------------------------------------------------------
|
||||
|
||||
@@ -99,6 +99,16 @@ class mrp_workcenter(osv.osv):
|
||||
value = {'costs_hour': cost.standard_price}
|
||||
return {'value': value}
|
||||
|
||||
def _check_capacity_per_cycle(self, cr, uid, ids, context=None):
|
||||
for obj in self.browse(cr, uid, ids, context=context):
|
||||
if obj.capacity_per_cycle <= 0.0:
|
||||
return False
|
||||
return True
|
||||
|
||||
_constraints = [
|
||||
(_check_capacity_per_cycle, 'The capacity per cycle must be strictly positive.', ['capacity_per_cycle']),
|
||||
]
|
||||
|
||||
class mrp_routing(osv.osv):
|
||||
"""
|
||||
For specifying the routings of Work Centers.
|
||||
|
||||
@@ -417,10 +417,10 @@
|
||||
<field name="product_efficiency"/>
|
||||
</group>
|
||||
</group>
|
||||
<lable string="Properties"/>
|
||||
<separator string="Properties" groups="product.group_mrp_properties"/>
|
||||
<group>
|
||||
<group>
|
||||
<field name="property_ids" widget="many2many_tags"/>
|
||||
<field name="property_ids" widget="many2many_tags" groups="product.group_mrp_properties"/>
|
||||
</group>
|
||||
<group/>
|
||||
</group>
|
||||
|
||||
@@ -381,7 +381,7 @@ class product_pricelist_version(osv.osv):
|
||||
'active': fields.boolean('Active',
|
||||
help="When a version is duplicated it is set to non active, so that the " \
|
||||
"dates do not overlaps with original version. You should change the dates " \
|
||||
"and reactivate the pricelist", copy=False),
|
||||
"and reactivate the pricelist"),
|
||||
'items_id': fields.one2many('product.pricelist.item',
|
||||
'price_version_id', 'Price List Items', required=True, copy=True),
|
||||
'date_start': fields.date('Start Date', help="First valid date for the version."),
|
||||
@@ -420,6 +420,13 @@ class product_pricelist_version(osv.osv):
|
||||
['date_start', 'date_end'])
|
||||
]
|
||||
|
||||
def copy(self, cr, uid, id, default=None, context=None):
|
||||
# set active False to prevent overlapping active pricelist
|
||||
# versions
|
||||
if not default:
|
||||
default = {}
|
||||
default['active'] = False
|
||||
return super(product_pricelist_version, self).copy(cr, uid, id, default, context=context)
|
||||
|
||||
class product_pricelist_item(osv.osv):
|
||||
def _price_field_get(self, cr, uid, context=None):
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
odoo.define('rating.kanban', function(require) {
|
||||
"use strict";
|
||||
|
||||
var common = require('web_kanban.common');
|
||||
|
||||
common.KanbanRecord.include({
|
||||
on_card_clicked: function() {
|
||||
if (this.view.dataset.model === 'rating.rating') {
|
||||
this.$('a').first().click();
|
||||
} else {
|
||||
this._super.apply(this, arguments);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,6 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
<!-- backend assets -->
|
||||
<template id="assets_backend" name="rating assets" inherit_id="web.assets_backend">
|
||||
<xpath expr="." position="inside">
|
||||
<script type="text/javascript" src="/rating/static/src/js/rating.js"></script>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<!-- Thanks page -->
|
||||
<template id="rating_external_page_view">
|
||||
<head>
|
||||
<link rel='stylesheet' href='/web/static/lib/bootstrap/css/bootstrap.css'/>
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
<field name="arch" type="xml">
|
||||
<kanban create="false" class="oe_background_grey">
|
||||
<field name="rating"/>
|
||||
<field name="res_id"/>
|
||||
<field name="res_model"/>
|
||||
<field name="res_name"/>
|
||||
<field name="create_date"/>
|
||||
<field name="partner_id"/>
|
||||
@@ -14,15 +16,16 @@
|
||||
<field name="feedback"/>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div style="min-height:100px;width:100px">
|
||||
<div t-attf-title="Name : #{ record.res_name.raw_value } <br>
|
||||
#{ record.partner_id.value ? 'Customer : ' + record.partner_id.value + '<br>' : '' }
|
||||
#{ record.rated_partner_id.value ? 'Responsible : ' + record.rated_partner_id.value+'<br>' : '' }
|
||||
Date : #{ moment(record.create_date.raw_value).format('L') }
|
||||
#{ record.feedback.raw_value ? '<br>'+'Reason : ' + record.feedback.raw_value : '' }"
|
||||
data-html="true" data-placement="bottom">
|
||||
<img t-attf-src="rating/static/src/img/rating_#{record.rating.raw_value}.png" class="oe_kanban_image"/>
|
||||
</div>
|
||||
<div class="oe_kanban_card oe_kanban_global_click">
|
||||
<a t-att-href="_.str.sprintf('/web?#id=%s&view_type=form&model=%s', record.res_id.raw_value, record.res_model.raw_value)">
|
||||
<img t-attf-src="rating/static/src/img/rating_#{record.rating.raw_value}.png"
|
||||
t-attf-title="Name : #{ record.res_name.raw_value } <br>
|
||||
#{ record.partner_id.value ? 'Customer : ' + record.partner_id.value + '<br>' : '' }
|
||||
#{ record.rated_partner_id.value ? 'Responsible : ' + record.rated_partner_id.value+'<br>' : '' }
|
||||
Date : #{ moment(record.create_date.raw_value).format('L') }
|
||||
#{ record.feedback.raw_value ? '<br>'+'Reason : ' + record.feedback.raw_value : '' }"
|
||||
data-html="true" data-placement="bottom"/>
|
||||
</a>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
|
||||
@@ -101,7 +101,7 @@ class sale_advance_payment_inv(osv.osv_memory):
|
||||
if wizard.amount <= 0.00:
|
||||
raise UserError(_('The value of Advance Amount must be positive.'))
|
||||
if wizard.advance_payment_method == 'percentage':
|
||||
inv_amount = sale.amount_total * wizard.amount / 100
|
||||
inv_amount = sale.amount_untaxed * wizard.amount / 100
|
||||
if not res.get('name'):
|
||||
res['name'] = self._translate_advance(cr, uid, percentage=True, context=dict(context, lang=sale.partner_id.lang)) % (wizard.amount)
|
||||
else:
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<td class="text-right">
|
||||
<span t-field="l.price_unit"/>
|
||||
</td>
|
||||
<td groups="sale.group_discount_per_so_line"><span t-field="l.discount"/></td>
|
||||
<td class="text-right" groups="sale.group_discount_per_so_line"><span t-field="l.discount"/></td>
|
||||
<td>
|
||||
<span t-esc="', '.join(map(lambda x: x.name, l.invoice_line_tax_id))"/>
|
||||
</td>
|
||||
|
||||
@@ -309,7 +309,7 @@ class stock_quant(osv.osv):
|
||||
'create_date': fields.datetime('Creation Date', readonly=True),
|
||||
'in_date': fields.datetime('Incoming Date', readonly=True, select=True),
|
||||
|
||||
'history_ids': fields.many2many('stock.move', 'stock_quant_move_rel', 'quant_id', 'move_id', 'Moves', help='Moves that operate(d) on this quant'),
|
||||
'history_ids': fields.many2many('stock.move', 'stock_quant_move_rel', 'quant_id', 'move_id', 'Moves', help='Moves that operate(d) on this quant', copy=False),
|
||||
'company_id': fields.many2one('res.company', 'Company', help="The company to which the quants belong", required=True, readonly=True, select=True),
|
||||
'inventory_value': fields.function(_calc_inventory_value, string="Inventory Value", type='float', readonly=True),
|
||||
|
||||
@@ -1805,7 +1805,7 @@ class stock_move(osv.osv):
|
||||
# used for colors in tree views:
|
||||
'scrapped': fields.related('location_dest_id', 'scrap_location', type='boolean', relation='stock.location', string='Scrapped', readonly=True),
|
||||
|
||||
'quant_ids': fields.many2many('stock.quant', 'stock_quant_move_rel', 'move_id', 'quant_id', 'Moved Quants'),
|
||||
'quant_ids': fields.many2many('stock.quant', 'stock_quant_move_rel', 'move_id', 'quant_id', 'Moved Quants', copy=False),
|
||||
'reserved_quant_ids': fields.one2many('stock.quant', 'reservation_id', 'Reserved quants'),
|
||||
'linked_move_operation_ids': fields.one2many('stock.move.operation.link', 'move_id', string='Linked Operations', readonly=True, help='Operations that impact this move for the computation of the remaining quantities'),
|
||||
'remaining_qty': fields.function(_get_remaining_qty, type='float', string='Remaining Quantity', digits=0,
|
||||
|
||||
@@ -88,13 +88,13 @@ class stock_move_scrap(osv.osv_memory):
|
||||
context=context)
|
||||
if context.get('active_id'):
|
||||
move = self.pool.get('stock.move').browse(cr, uid, context['active_id'], context=context)
|
||||
return {
|
||||
'view_type': 'form',
|
||||
'view_mode': 'form',
|
||||
'res_model': 'stock.picking',
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_id': move.picking_id.id,
|
||||
'context': context
|
||||
}
|
||||
else:
|
||||
return {'type': 'ir.actions.act_window_close'}
|
||||
if move.picking_id:
|
||||
return {
|
||||
'view_type': 'form',
|
||||
'view_mode': 'form',
|
||||
'res_model': 'stock.picking',
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_id': move.picking_id.id,
|
||||
'context': context
|
||||
}
|
||||
return {'type': 'ir.actions.act_window_close'}
|
||||
|
||||
@@ -79,13 +79,22 @@ class stock_quant(osv.osv):
|
||||
'''
|
||||
if context is None:
|
||||
context = {}
|
||||
account_period = self.pool['account.period']
|
||||
super(stock_quant, self)._price_update(cr, uid, quant_ids, newprice, context=context)
|
||||
ctx = context.copy()
|
||||
for quant in self.browse(cr, uid, quant_ids, context=context):
|
||||
move = self._get_latest_move(cr, uid, quant, context=context)
|
||||
# this is where we post accounting entries for adjustment
|
||||
ctx['force_valuation_amount'] = newprice - quant.cost
|
||||
self._account_entry_move(cr, uid, [quant], move, context=ctx)
|
||||
valuation_update = newprice - quant.cost
|
||||
# this is where we post accounting entries for adjustment, if needed
|
||||
if not quant.company_id.currency_id.is_zero(valuation_update):
|
||||
# adjustment journal entry needed, cost has been updated
|
||||
period_id = (context.get('force_period') or
|
||||
account_period.find(cr, uid, move.date, context=context)[0])
|
||||
period = account_period.browse(cr, uid, period_id, context=context)
|
||||
# If neg quant period already closed (likely with manual valuation), skip update
|
||||
if period.state != 'done':
|
||||
ctx = dict(context, force_valuation_amount=valuation_update)
|
||||
self._account_entry_move(cr, uid, [quant], move, context=ctx)
|
||||
|
||||
#update the standard price of the product, only if we would have done it if we'd have had enough stock at first, which means
|
||||
#1) the product cost's method is 'real'
|
||||
#2) we just fixed a negative quant caused by an outgoing shipment
|
||||
|
||||
@@ -845,6 +845,16 @@ var FormView = View.extend(common.FieldManagerMixin, {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Heuristic to assign a proper sequence number for new records that
|
||||
// are added in a dataset containing other lines with existing sequence numbers
|
||||
if (!self.datarecord.id && self.fields.sequence &&
|
||||
!_.has(values, 'sequence') && !_.isEmpty(self.dataset.cache)) {
|
||||
// Find current max or min sequence (editable top/bottom)
|
||||
var current = _[prepend_on_create ? "min" : "max"](
|
||||
_.map(self.dataset.cache, function(o){return o.values.sequence})
|
||||
);
|
||||
values['sequence'] = prepend_on_create ? current - 1 : current + 1;
|
||||
}
|
||||
if (form_invalid) {
|
||||
self.set({'display_invalid_fields': true});
|
||||
first_invalid_field.focus();
|
||||
|
||||
@@ -474,6 +474,11 @@ define([
|
||||
return this.normalize();
|
||||
}
|
||||
|
||||
// if apply the editor to a P, LI...
|
||||
if (dom.isText(sc) && /^P|^LI|^H[1-7]/.test(sc.parentNode.nodeName.toUpperCase())) {
|
||||
return this.normalize();
|
||||
}
|
||||
|
||||
// find inline top ancestor
|
||||
var topAncestor;
|
||||
if (dom.isInline(sc)) {
|
||||
|
||||
@@ -502,7 +502,7 @@ dom.removeBetween = function (sc, so, ec, eo, towrite) {
|
||||
if (sc.tagName) {
|
||||
sc = sc.childNodes[so] || dom.firstChild(ec);
|
||||
so = 0;
|
||||
if (!dom.hasContentBefore(sc)) {
|
||||
if (!dom.hasContentBefore(sc) && towrite) {
|
||||
sc.parentNode.insertBefore(document.createTextNode('\u00A0'), sc);
|
||||
}
|
||||
}
|
||||
@@ -557,18 +557,19 @@ dom.removeBetween = function (sc, so, ec, eo, towrite) {
|
||||
sc = before ? dom.lastChild(before) : dom.firstChild(after);
|
||||
so = sc.textContent.length;
|
||||
|
||||
if (before) {
|
||||
if (towrite && !node.firstChild && node.parentNode && !dom.isNotBreakable(node)) {
|
||||
sc = $("<br/>")[0];
|
||||
so = 0;
|
||||
node.appendChild(br);
|
||||
} else if (!ancestor.children.length && !ancestor.textContent.match(/\S|\u00A0/)) {
|
||||
sc = $("<br/>")[0];
|
||||
so = 0;
|
||||
$(ancestor).prepend(sc);
|
||||
} else if (before) {
|
||||
var text = sc.textContent.replace(/[ \t\n\r]+$/, '\u00A0');
|
||||
so -= sc.textContent.length - text.length;
|
||||
sc.textContent = text;
|
||||
}
|
||||
if (towrite && !node.firstChild && node.parentNode && !dom.isNotBreakable(node)) {
|
||||
var br = $("<br/>")[0];
|
||||
node.appendChild(br);
|
||||
sc = br;
|
||||
so = 0;
|
||||
}
|
||||
dom.autoMerge(sc, false);
|
||||
|
||||
} else {
|
||||
|
||||
@@ -704,7 +705,7 @@ dom.isFont = function (node) {
|
||||
(nodeName === "SPAN" && (
|
||||
node.className.match(/(^|\s)fa(\s|$)/i) ||
|
||||
node.className.match(/(^|\s)(text|bg)-/i) ||
|
||||
(node.attributes.style && node.attributes.style.value.match(/(^|\s)(color|background-color):/i)))) );
|
||||
(node.attributes.style && node.attributes.style.value.match(/(^|\s)(color|background-color|font-size):/i)))) );
|
||||
};
|
||||
dom.isVisibleText = function (textNode) {
|
||||
return !!textNode.textContent.match(/\S|\u00A0/);
|
||||
@@ -1024,7 +1025,7 @@ $.summernote.pluginEvents.enter = function (event, editor, layoutInfo) {
|
||||
|
||||
var node = dom.node(r.sc);
|
||||
var last = node;
|
||||
while (node && dom.isSplitable(node) && !dom.isList(node)) {
|
||||
while (node && dom.isSplitable(node) && !dom.isList(node) && !dom.isEditable(node)) {
|
||||
last = node;
|
||||
node = node.parentNode;
|
||||
}
|
||||
@@ -1158,8 +1159,6 @@ $.summernote.pluginEvents.delete = function (event, editor, layoutInfo) {
|
||||
}
|
||||
r = r.deleteContents();
|
||||
r.select();
|
||||
event.preventDefault();
|
||||
return false;
|
||||
}
|
||||
|
||||
var target = r.ec;
|
||||
@@ -1284,8 +1283,6 @@ $.summernote.pluginEvents.backspace = function (event, editor, layoutInfo) {
|
||||
}
|
||||
r = r.deleteContents();
|
||||
r.select();
|
||||
event.preventDefault();
|
||||
return false;
|
||||
}
|
||||
|
||||
var target = r.sc;
|
||||
@@ -1729,12 +1726,19 @@ $.summernote.pluginEvents.formatBlock = function (event, editor, layoutInfo, sTa
|
||||
$.summernote.pluginEvents.removeFormat = function (event, editor, layoutInfo, value) {
|
||||
var $editable = layoutInfo.editable();
|
||||
$editable.data('NoteHistory').recordUndo($editable);
|
||||
|
||||
var node = range.create().sc.parentNode;
|
||||
document.execCommand('removeFormat');
|
||||
document.execCommand('removeFormat');
|
||||
document.execCommand('removeFormat'); // 2 times because some browser don't clean all in one time
|
||||
var r = range.create();
|
||||
r = dom.merge(node, r.sc, r.so, r.ec, r.eo, null, true);
|
||||
range.create(r.sc, r.so, r.ec, r.eo).select();
|
||||
r = range.create(r.sc, r.so, r.ec, r.eo);
|
||||
r.select();
|
||||
|
||||
$.summernote.pluginEvents.justify(event, editor, layoutInfo, "");
|
||||
|
||||
r.clean().select();
|
||||
|
||||
event.preventDefault();
|
||||
return false;
|
||||
};
|
||||
@@ -1766,6 +1770,9 @@ eventHandler.toolbar.button.updateRecentColor = function (elBtn, sEvent, sValue)
|
||||
|
||||
options.fontSizes = [_t('Default'), 8, 9, 10, 11, 12, 14, 18, 24, 36, 48, 62];
|
||||
$.summernote.pluginEvents.applyFont = function (event, editor, layoutInfo, color, bgcolor, size) {
|
||||
var $editable = layoutInfo.editable();
|
||||
$editable.data('NoteHistory').recordUndo($editable);
|
||||
|
||||
var rng = range.create();
|
||||
var startPoint = rng.getStartPoint();
|
||||
var endPoint = rng.getEndPoint();
|
||||
@@ -1955,7 +1962,7 @@ $.summernote.pluginEvents.applyFont = function (event, editor, layoutInfo, color
|
||||
if (i>0 && (font = dom.ancestor(nodes[i-1], dom.isFont))) {
|
||||
className2 = font.getAttribute('class');
|
||||
style2 = font.getAttribute('style');
|
||||
if (node !== font && className == className2 && style == style2) {
|
||||
if (node !== font && className == className2 && style == style2 && (node.parentNode === font.parentNode || node.parentNode === font)) {
|
||||
remove(node, font);
|
||||
nodes.splice(i,1);
|
||||
i--;
|
||||
@@ -1968,6 +1975,8 @@ $.summernote.pluginEvents.applyFont = function (event, editor, layoutInfo, color
|
||||
};
|
||||
$.summernote.pluginEvents.fontSize = function (event, editor, layoutInfo, value) {
|
||||
var $editable = layoutInfo.editable();
|
||||
$editable.data('NoteHistory').recordUndo($editable);
|
||||
|
||||
event.preventDefault();
|
||||
$.summernote.pluginEvents.applyFont(event, editor, layoutInfo, null, null, value);
|
||||
editor.afterCommand($editable);
|
||||
@@ -2001,6 +2010,60 @@ $.summernote.pluginEvents.backColor = function (event, editor, layoutInfo, backC
|
||||
}
|
||||
$.summernote.pluginEvents.applyFont(event, editor, layoutInfo, null, backColor, null);
|
||||
editor.afterCommand($editable);
|
||||
};
|
||||
|
||||
$.summernote.pluginEvents.justify = function (event, editor, layoutInfo, align) {
|
||||
var rng = range.create();
|
||||
var startPoint = rng.getStartPoint();
|
||||
var endPoint = rng.getEndPoint();
|
||||
|
||||
// get list of nodes to change
|
||||
var nodes = [];
|
||||
nodes.push(dom.ancestor(startPoint.node, dom.isPara) || dom.ancestor(startPoint.node, dom.isCell));
|
||||
nodes.push(dom.ancestor(endPoint.node, dom.isPara) || dom.ancestor(endPoint.node, dom.isCell));
|
||||
dom.walkPoint(startPoint, endPoint, function (point) {
|
||||
var node = point.node;
|
||||
if (dom.isPara(node) || dom.isCell(node)) {
|
||||
nodes.push(point.node);
|
||||
}
|
||||
});
|
||||
nodes = list.unique(nodes);
|
||||
|
||||
$(nodes).not(":has(div,p,li,h1,h2,h3,h4,h5,h6,h7,td,th)").each(function () {
|
||||
var $node = $(this).css('text-align', '');
|
||||
var css = window.getComputedStyle(this);
|
||||
var textAlign = css.textAlign;
|
||||
if (textAlign === "start") {
|
||||
textAlign = css.direction === "ltr" ? "left" : "right";
|
||||
}
|
||||
if(textAlign !== align) {
|
||||
$node.css('text-align', align);
|
||||
}
|
||||
}).find("*").css('text-align', "");
|
||||
};
|
||||
$.summernote.pluginEvents.justifyLeft = function (event, editor, layoutInfo) {
|
||||
var $editable = layoutInfo.editable();
|
||||
$editable.data('NoteHistory').recordUndo($editable);
|
||||
$.summernote.pluginEvents.justify(event, editor, layoutInfo, 'left');
|
||||
editor.afterCommand($editable);
|
||||
};
|
||||
$.summernote.pluginEvents.justifyCenter = function (event, editor, layoutInfo) {
|
||||
var $editable = layoutInfo.editable();
|
||||
$editable.data('NoteHistory').recordUndo($editable);
|
||||
$.summernote.pluginEvents.justify(event, editor, layoutInfo, 'center');
|
||||
editor.afterCommand($editable);
|
||||
};
|
||||
$.summernote.pluginEvents.justifyRight = function (event, editor, layoutInfo) {
|
||||
var $editable = layoutInfo.editable();
|
||||
$editable.data('NoteHistory').recordUndo($editable);
|
||||
$.summernote.pluginEvents.justify(event, editor, layoutInfo, 'right');
|
||||
editor.afterCommand($editable);
|
||||
};
|
||||
$.summernote.pluginEvents.justifyFull = function (event, editor, layoutInfo) {
|
||||
var $editable = layoutInfo.editable();
|
||||
$editable.data('NoteHistory').recordUndo($editable);
|
||||
$.summernote.pluginEvents.justify(event, editor, layoutInfo, 'justify');
|
||||
editor.afterCommand($editable);
|
||||
};
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@@ -213,16 +213,6 @@ var BuildingBlock = Widget.extend({
|
||||
throw new Error("Wrong snippets xml definition");
|
||||
}
|
||||
|
||||
$ul.children().tooltip({
|
||||
delay: { "show": 500, "hide": 100 },
|
||||
container: 'body',
|
||||
title: function () {
|
||||
return (navigator.appVersion.indexOf('Mac') > -1 ? 'CMD' : 'CTRL')+'+SHIFT+'+($(this).index()+1);
|
||||
},
|
||||
trigger: 'hover',
|
||||
placement: 'top'
|
||||
}).on('click', function () {$(this).tooltip('hide');});
|
||||
|
||||
// t-snippet
|
||||
$html.find('[data-oe-type="snippet"][data-oe-name]').each(function () {
|
||||
$('<div/>').insertAfter(this).append(this).attr('name', $(this).data('oe-name'));
|
||||
|
||||
@@ -140,7 +140,7 @@ Tour.register({
|
||||
},
|
||||
{
|
||||
waitFor: '#wrapwrap > main > div > section .row > div:first:has( font:last:containsExact(Bat) )',
|
||||
element: '.note-air-popover button[data-event="undo"]',
|
||||
element: '.note-image-popover button[data-event="undo"]',
|
||||
title: "undo",
|
||||
},
|
||||
{
|
||||
|
||||
@@ -143,8 +143,7 @@ website.if_dom_contains('.website_forum', function () {
|
||||
return true;
|
||||
});
|
||||
|
||||
|
||||
$('.link_url').on('change', function (ev) {
|
||||
$('.link_url, .o_forum_post_link').on('change', function (ev) { // keep .link_url for compat
|
||||
ev.preventDefault();
|
||||
var $link = $(ev.currentTarget);
|
||||
var display_error = function(){
|
||||
@@ -153,7 +152,7 @@ website.if_dom_contains('.website_forum', function () {
|
||||
'Please enter valid URL. Example: http://www.odoo.com'+
|
||||
'</div>');
|
||||
$link.parent().append($warning);
|
||||
$("button#btn_post_your_article")[0].disabled = true;
|
||||
$link.parents('form').find('button')[0].disabled = true;
|
||||
|
||||
};
|
||||
var url = $link.val();
|
||||
@@ -163,10 +162,10 @@ website.if_dom_contains('.website_forum', function () {
|
||||
var regex = new RegExp("(http(s)?://.)?(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)");
|
||||
if(regex.test(url)){
|
||||
ajax.jsonRpc("/forum/get_url_title", 'call', {'url': url}).then(function (data) {
|
||||
if(data){
|
||||
if (data) {
|
||||
$("input[name='post_name']")[0].value = data;
|
||||
$('button#btn_post_your_article').prop('disabled', false);
|
||||
}else{
|
||||
$link.parents('form').find('button')[0].disabled = false;
|
||||
} else {
|
||||
display_error();
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ var ForumShare = SocialShare.extend({
|
||||
website.ready().done(function() {
|
||||
|
||||
// Store social share data to display modal on next page
|
||||
$(document.body).on('click', '.oe_social_share_call', function() {
|
||||
$(document.body).on('click', ':not(.karma_required).oe_social_share_call', function() {
|
||||
var social_data = {};
|
||||
social_data['target_type'] = $(this).data('social-target-type');
|
||||
sessionStorage.setItem('social_share', JSON.stringify(social_data));
|
||||
|
||||
@@ -435,7 +435,7 @@
|
||||
<label class="col-sm-2 control-label" for="content_link">URL to Share</label>
|
||||
<div class="col-sm-8">
|
||||
<input type="text" name="content_link" required="True" t-attf-value="#{post_name}"
|
||||
class="form-control mb16 link_url" placeholder="e.g. https://www.odoo.com"/>
|
||||
class="form-control mb16 o_forum_post_link" placeholder="e.g. https://www.odoo.com"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
@@ -454,8 +454,9 @@
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-offset-2 col-sm-8">
|
||||
<button t-attf-class="btn btn-primary #{forum.allow_share and 'oe_social_share_call' or ''}" disabled="True" id="btn_post_your_article"
|
||||
data-hashtags="#link" data-social-target-type="other">Post</button>
|
||||
<button t-attf-class="btn btn-primary #{forum.allow_share and 'oe_social_share_call' or ''} #{(user.karma < forum.karma_ask) and 'karma_required' or ''}"
|
||||
t-att-data-karma="forum.karma_ask"
|
||||
disabled="True" data-hashtags="#link" data-social-target-type="other">Post</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -481,7 +482,8 @@
|
||||
<input type="hidden" name="karma_retag" t-attf-value="#{forum.karma_retag}" id="karma_retag"/>
|
||||
<input type="text" name="post_tags" placeholder="Tags" class="form-control js_select2"/>
|
||||
<br/><br/>
|
||||
<button t-attf-class="btn btn-primary #{forum.allow_share and 'oe_social_share_call' or ''}"
|
||||
<button t-attf-class="btn btn-primary #{forum.allow_share and 'oe_social_share_call' or ''} #{(user.karma < forum.karma_ask) and 'karma_required' or ''}"
|
||||
t-att-data-karma="forum.karma_ask"
|
||||
data-hashtags="#discussion" data-social-target-type="default">Post Your Topic</button>
|
||||
</form>
|
||||
</t>
|
||||
|
||||
@@ -18,5 +18,5 @@ class hr_job(osv.osv):
|
||||
return super(hr_job, self).job_open(cr, uid, ids, context)
|
||||
|
||||
_columns = {
|
||||
'website_description': fields.html('Website description'),
|
||||
'website_description': fields.html('Website description', translate=True),
|
||||
}
|
||||
|
||||
@@ -342,7 +342,7 @@
|
||||
<div class="snippet_row bg-color" style="padding:0;width:600px;max-width:600px;margin:0 auto;background: #fff repeat top /100%;color:#777777">
|
||||
<table style="width:100%;text-align:justify;margin:0 auto;background:inherit;color:inherit;border-collapse:collapse;color:inherit">
|
||||
<tr>
|
||||
<td style="padding:10px 0;font-size:14px">
|
||||
<td style="padding:10px 30px;font-size:14px">
|
||||
<p style="margin:0">Dear John</p>
|
||||
<p style="margin:0">Great stories have personality. Consider telling a great story that provides personality. Writing a story with personality for potential clients will assist with making a relationship connection. This shows up in small quirks like word choices or phrases. Write from your point of view, not from someone else's experience.<br/></p>
|
||||
<p style="margin:0">Great stories are for everyone even when only written for just one person. If you try to write with a wide general audience in mind, your story will ring false and be bland. No one will be interested. Write for one person. If it’s genuine for the one, it’s genuine for the rest.</p>
|
||||
|
||||
@@ -481,7 +481,7 @@
|
||||
<div class="snippet_row bg-color" style="padding:0;width:600px;max-width:600px;margin:0 auto;background:#fff repeat top /100%;color:#464646">
|
||||
<table style="width:100%;text-align:justify;margin:0 auto;border-collapse:collapse;color:inherit">
|
||||
<tr>
|
||||
<td style="padding:10px 0;font-size:14px">
|
||||
<td style="padding:10px 30px;font-size:14px">
|
||||
<p style="margin:0">Dear John</p>
|
||||
<p style="margin:0">Great stories have personality. Consider telling a great story that provides personality. Writing a story with personality for potential clients will assist with making a relationship connection. This shows up in small quirks like word choices or phrases. Write from your point of view, not from someone else's experience.<br/></p>
|
||||
<p style="margin:0">Great stories are for everyone even when only written for just one person. If you try to write with a wide general audience in mind, your story will ring false and be bland. No one will be interested. Write for one person. If it’s genuine for the one, it’s genuine for the rest.</p>
|
||||
|
||||
@@ -465,7 +465,7 @@
|
||||
<div class="snippet_row bg-color" style="padding:0;width:600px;max-width:600px;margin:0 auto;background: #fff repeat top /100%;color:#89898d">
|
||||
<table style="width:100%;text-align:justify;margin:0 auto;border-collapse:collapse;color:inherit">
|
||||
<tr>
|
||||
<td style="padding:10px 0;font-size:14px">
|
||||
<td style="padding:10px 30px;font-size:14px">
|
||||
<p style="margin:0">Dear John</p>
|
||||
<p style="margin:0">Great stories have personality. Consider telling a great story that provides personality. Writing a story with personality for potential clients will assist with making a relationship connection. This shows up in small quirks like word choices or phrases. Write from your point of view, not from someone else's experience.<br/></p>
|
||||
<p style="margin:0">Great stories are for everyone even when only written for just one person. If you try to write with a wide general audience in mind, your story will ring false and be bland. No one will be interested. Write for one person. If it’s genuine for the one, it’s genuine for the rest.</p>
|
||||
|
||||
@@ -510,7 +510,7 @@
|
||||
<div class="snippet_row bg-color" style="padding:0;width:600px;max-width:600px;margin:0 auto;background: #EAEAEA repeat top /100%;color:#282D29">
|
||||
<table style="width:100%;text-align:justify;margin:0 auto;border-collapse:collapse;color:inherit">
|
||||
<tr>
|
||||
<td style="padding:10px 0;font-size:14px">
|
||||
<td style="padding:10px 30px;font-size:14px">
|
||||
<p style="margin:0">Dear John</p>
|
||||
<p style="margin:0">Great stories have personality. Consider telling a great story that provides personality. Writing a story with personality for potential clients will assist with making a relationship connection. This shows up in small quirks like word choices or phrases. Write from your point of view, not from someone else's experience.<br/></p>
|
||||
<p style="margin:0">Great stories are for everyone even when only written for just one person. If you try to write with a wide general audience in mind, your story will ring false and be bland. No one will be interested. Write for one person. If it’s genuine for the one, it’s genuine for the rest.</p>
|
||||
|
||||
@@ -482,7 +482,7 @@
|
||||
<div class="snippet_row bg-color" style="padding:0;width:600px;max-width:600px;margin:0 auto;background: #FFFFFF repeat top /100%;color:#37302d">
|
||||
<table style="width:100%;text-align:justify;margin:0 auto;border-collapse:collapse;color:inherit">
|
||||
<tr>
|
||||
<td style="padding:10px 0;font-size:14px">
|
||||
<td style="padding:10px 30px;font-size:14px">
|
||||
<p style="margin:0">Dear John</p>
|
||||
<p style="margin:0">Great stories have personality. Consider telling a great story that provides personality. Writing a story with personality for potential clients will assist with making a relationship connection. This shows up in small quirks like word choices or phrases. Write from your point of view, not from someone else's experience.<br/></p>
|
||||
<p style="margin:0">Great stories are for everyone even when only written for just one person. If you try to write with a wide general audience in mind, your story will ring false and be bland. No one will be interested. Write for one person. If it’s genuine for the one, it’s genuine for the rest.</p>
|
||||
|
||||
@@ -449,7 +449,7 @@
|
||||
<div class="snippet_row bg-color" style="padding:0;width:600px;max-width:600px;margin:0 auto;background: #fff repeat top /100%;color:#168484">
|
||||
<table style="width:100%;text-align:justify;margin:0 auto;inherit;border-collapse:collapse;color:inherit">
|
||||
<tr>
|
||||
<td style="padding:10px 0;font-size:14px;line-height:20px">
|
||||
<td style="padding:10px 30px;font-size:14px;line-height:20px">
|
||||
<p style="margin:0">Dear John</p>
|
||||
<p style="margin:0">Great stories have personality. Consider telling a great story that provides personality. Writing a story with personality for potential clients will assist with making a relationship connection. This shows up in small quirks like word choices or phrases. Write from your point of view, not from someone else's experience.<br/></p>
|
||||
<p style="margin:0">Great stories are for everyone even when only written for just one person. If you try to write with a wide general audience in mind, your story will ring false and be bland. No one will be interested. Write for one person. If it’s genuine for the one, it’s genuine for the rest.</p>
|
||||
|
||||
@@ -464,7 +464,7 @@
|
||||
<div class="snippet_row bg-color" style="padding:0;width:600px;max-width:600px;margin:0 auto;background: #fff repeat top /100%;color:#37302d">
|
||||
<table style="width:100%;text-align:justify;margin:0 auto;border-collapse:collapse;color:inherit">
|
||||
<tr>
|
||||
<td style="padding:10px 0;font-size:14px;line-height:20px">
|
||||
<td style="padding:10px 30px;font-size:14px;line-height:20px">
|
||||
<p style="margin:0">Dear John</p>
|
||||
<p style="margin:0">Great stories have personality. Consider telling a great story that provides personality. Writing a story with personality for potential clients will assist with making a relationship connection. This shows up in small quirks like word choices or phrases. Write from your point of view, not from someone else's experience.<br/></p>
|
||||
<p style="margin:0">Great stories are for everyone even when only written for just one person. If you try to write with a wide general audience in mind, your story will ring false and be bland. No one will be interested. Write for one person. If it’s genuine for the one, it’s genuine for the rest.</p>
|
||||
|
||||
@@ -465,7 +465,7 @@
|
||||
<div class="snippet_row bg-color" style="padding:0;width:600px;max-width:600px;margin:0 auto;background: #fff repeat top /100%;color:#414141">
|
||||
<table style="width:100%;text-align:justify;margin:0 auto;color:inherit;border-collapse:collapse">
|
||||
<tr>
|
||||
<td style="padding:10px 0;font-size:14px">
|
||||
<td style="padding:10px 30px;font-size:14px">
|
||||
<p style="margin:0">Dear John</p>
|
||||
<p style="margin:0">Great stories have personality. Consider telling a great story that provides personality. Writing a story with personality for potential clients will assist with making a relationship connection. This shows up in small quirks like word choices or phrases. Write from your point of view, not from someone else's experience.<br/></p>
|
||||
<p style="margin:0">Great stories are for everyone even when only written for just one person. If you try to write with a wide general audience in mind, your story will ring false and be bland. No one will be interested. Write for one person. If it’s genuine for the one, it’s genuine for the rest.</p>
|
||||
|
||||
@@ -485,7 +485,7 @@
|
||||
<div class="snippet_row bg-color" style="padding:0;width:600px;max-width:600px;margin:0 auto;background: #fff repeat top /100%;color:#414141">
|
||||
<table style="width:100%;text-align:justify;margin:0 auto;color:#414141;border-collapse:collapse">
|
||||
<tr>
|
||||
<td style="padding:10px 0;font-size:14px">
|
||||
<td style="padding:10px 30px;font-size:14px">
|
||||
<p style="margin:0">Dear John</p>
|
||||
<p style="margin:0">Great stories have personality. Consider telling a great story that provides personality. Writing a story with personality for potential clients will assist with making a relationship connection. This shows up in small quirks like word choices or phrases. Write from your point of view, not from someone else's experience.<br/></p>
|
||||
<p style="margin:0">Great stories are for everyone even when only written for just one person. If you try to write with a wide general audience in mind, your story will ring false and be bland. No one will be interested. Write for one person. If it’s genuine for the one, it’s genuine for the rest.</p>
|
||||
|
||||
@@ -440,7 +440,7 @@
|
||||
<ul class="list-unstyled">
|
||||
<t t-set="inc" t-value="0"/>
|
||||
<t t-foreach="variant_id.value_ids" t-as="value_id">
|
||||
<li t-if="value_id.product_ids" class="form-group js_attribute_value" style="margin: 0;">
|
||||
<li class="form-group js_attribute_value" style="margin: 0;">
|
||||
<label class="control-label" style="margin: 0 20px;">
|
||||
<input type="radio" class="js_variant_change" t-att-checked="'checked' if not inc else ''" t-att-name="'attribute-%s-%s' % (product.id, variant_id.attribute_id.id)" t-att-value="value_id.id" style="vertical-align: top; margin-right: 10px;"/>
|
||||
<span t-field="value_id.name"/>
|
||||
|
||||
@@ -23,8 +23,11 @@ class website_sale_options(website_sale):
|
||||
return r
|
||||
|
||||
@http.route(['/shop/cart/update_option'], type='http', auth="public", methods=['POST'], website=True)
|
||||
def cart_options_update_json(self, product_id, add_qty=1, set_qty=0, goto_shop=None, **kw):
|
||||
def cart_options_update_json(self, product_id, add_qty=1, set_qty=0, goto_shop=None, lang=None, **kw):
|
||||
cr, uid, context, pool = request.cr, request.uid, request.context, request.registry
|
||||
if lang:
|
||||
context = dict(context, lang=lang)
|
||||
request.website = request.website.with_context(context)
|
||||
|
||||
order = request.website.sale_get_order(force_create=1)
|
||||
product = pool['product.product'].browse(cr, uid, int(product_id), context=context)
|
||||
@@ -57,11 +60,13 @@ class website_sale_options(website_sale):
|
||||
if not context.get('pricelist'):
|
||||
context['pricelist'] = int(pricelist)
|
||||
|
||||
website_context = kw.get('kwargs', {}).get('context', {})
|
||||
context = dict(context or {}, **website_context)
|
||||
from_currency = pool.get('product.price.type')._get_field_currency(cr, uid, 'list_price', context)
|
||||
to_currency = pricelist.currency_id
|
||||
compute_currency = lambda price: pool['res.currency']._compute(cr, uid, from_currency, to_currency, price, context=context)
|
||||
|
||||
product = pool['product.product'].browse(cr, uid, int(product_id), context=context)
|
||||
request.website = request.website.with_context(context)
|
||||
|
||||
return request.website._render("website_sale_options.modal", {
|
||||
'product': product,
|
||||
|
||||
@@ -33,6 +33,7 @@ $('.oe_website_sale #add_to_cart, .oe_website_sale #products_grid .a-submit')
|
||||
var $a = $(this);
|
||||
$form.ajaxSubmit({
|
||||
url: '/shop/cart/update_option',
|
||||
data: {lang: openerp.website.get_context().lang},
|
||||
success: function (quantity) {
|
||||
if (!$a.hasClass('js_goto_shop')) {
|
||||
window.location.href = window.location.href.replace(/shop([\/?].*)?$/, "shop/cart");
|
||||
|
||||
+7
-3
@@ -56,11 +56,15 @@ del time
|
||||
# The hard-coded super-user id (a.k.a. administrator, or root user).
|
||||
SUPERUSER_ID = 1
|
||||
|
||||
def registry(database_name):
|
||||
def registry(database_name=None):
|
||||
"""
|
||||
Return the model registry for the given database. If the registry does not
|
||||
exist yet, it is created on the fly.
|
||||
Return the model registry for the given database, or the database mentioned
|
||||
on the current thread. If the registry does not exist yet, it is created on
|
||||
the fly.
|
||||
"""
|
||||
if database_name is None:
|
||||
import threading
|
||||
database_name = threading.currentThread().dbname
|
||||
return modules.registry.RegistryManager.get(database_name)
|
||||
|
||||
#----------------------------------------------------------
|
||||
|
||||
@@ -896,6 +896,8 @@ class users_view(osv.osv):
|
||||
def fields_get(self, cr, uid, allfields=None, context=None, write_access=True, attributes=None):
|
||||
res = super(users_view, self).fields_get(cr, uid, allfields, context, write_access, attributes)
|
||||
# add reified groups fields
|
||||
if uid != SUPERUSER_ID and not self.pool['res.users'].has_group(cr, uid, 'base.group_erp_manager'):
|
||||
return res
|
||||
for app, kind, gs in self.pool['res.groups'].get_groups_by_application(cr, uid, context):
|
||||
if kind == 'selection':
|
||||
# selection group field
|
||||
|
||||
@@ -31,12 +31,10 @@ class TestAPI(common.TransactionCase):
|
||||
self.assertTrue(ids)
|
||||
self.assertTrue(partners)
|
||||
|
||||
# partners and its contents are instance of the model, and share its ormcache
|
||||
# partners and its contents are instance of the model
|
||||
self.assertIsRecordset(partners, 'res.partner')
|
||||
self.assertIs(partners._ormcache, self.env['res.partner']._ormcache)
|
||||
for p in partners:
|
||||
self.assertIsRecord(p, 'res.partner')
|
||||
self.assertIs(p._ormcache, self.env['res.partner']._ormcache)
|
||||
|
||||
self.assertEqual([p.id for p in partners], ids)
|
||||
self.assertEqual(self.env['res.partner'].browse(ids), partners)
|
||||
|
||||
@@ -111,9 +111,6 @@ class TestPropertyField(common.TransactionCase):
|
||||
self.partner._columns.update({
|
||||
'property_country': fields.property(type='many2one', relation="res.country", string="Country by company"),
|
||||
})
|
||||
self.partner._all_columns.update({
|
||||
'property_country': fields.column_info('property_country', self.partner._columns['property_country'], None, None, None),
|
||||
})
|
||||
self.partner._field_create(cr)
|
||||
|
||||
partner_id = self.partner.create(cr, alice, {
|
||||
|
||||
+183
-114
@@ -30,12 +30,13 @@ import logging
|
||||
import pytz
|
||||
import xmlrpclib
|
||||
|
||||
from openerp.tools import float_round, ustr, html_sanitize
|
||||
from openerp.tools import float_round, frozendict, html_sanitize, ustr
|
||||
from openerp.tools import DEFAULT_SERVER_DATE_FORMAT as DATE_FORMAT
|
||||
from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT as DATETIME_FORMAT
|
||||
|
||||
DATE_LENGTH = len(date.today().strftime(DATE_FORMAT))
|
||||
DATETIME_LENGTH = len(datetime.now().strftime(DATETIME_FORMAT))
|
||||
EMPTY_DICT = frozendict()
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -72,10 +73,25 @@ class MetaField(type):
|
||||
""" Metaclass for field classes. """
|
||||
by_type = {}
|
||||
|
||||
def __new__(meta, name, bases, attrs):
|
||||
""" Combine the ``_slots`` dict from parent classes, and determine
|
||||
`__slots__` for them on the new class.
|
||||
"""
|
||||
base_slots = {}
|
||||
for base in reversed(bases):
|
||||
base_slots.update(getattr(base, '_slots', ()))
|
||||
|
||||
slots = dict(base_slots)
|
||||
slots.update(attrs.get('_slots', ()))
|
||||
|
||||
attrs['__slots__'] = set(slots) - set(base_slots)
|
||||
attrs['_slots'] = slots
|
||||
return type.__new__(meta, name, bases, attrs)
|
||||
|
||||
def __init__(cls, name, bases, attrs):
|
||||
super(MetaField, cls).__init__(name, bases, attrs)
|
||||
if cls.type:
|
||||
cls.by_type[cls.type] = cls
|
||||
if cls.type and cls.type not in MetaField.by_type:
|
||||
MetaField.by_type[cls.type] = cls
|
||||
|
||||
# compute class attributes to avoid calling dir() on fields
|
||||
cls.column_attrs = []
|
||||
@@ -253,56 +269,81 @@ class Field(object):
|
||||
"""
|
||||
__metaclass__ = MetaField
|
||||
|
||||
_attrs = None # dictionary with all field attributes
|
||||
_free_attrs = None # list of semantic-free attribute names
|
||||
type = None # type of the field (string)
|
||||
relational = False # whether the field is a relational one
|
||||
|
||||
automatic = False # whether the field is automatically created ("magic" field)
|
||||
inherited = False # whether the field is inherited (_inherits)
|
||||
column = None # the column corresponding to the field
|
||||
setup_done = False # whether the field has been set up
|
||||
_slots = {
|
||||
'_attrs': EMPTY_DICT, # dictionary of field attributes; it contains:
|
||||
# - all attributes after __init__()
|
||||
# - free attributes only after set_class_name()
|
||||
|
||||
name = None # name of the field
|
||||
type = None # type of the field (string)
|
||||
relational = False # whether the field is a relational one
|
||||
model_name = None # name of the model of this field
|
||||
comodel_name = None # name of the model of values (if relational)
|
||||
inverse_fields = None # list of inverse fields (objects)
|
||||
'automatic': False, # whether the field is automatically created ("magic" field)
|
||||
'inherited': False, # whether the field is inherited (_inherits)
|
||||
'column': None, # the column corresponding to the field
|
||||
'setup_done': False, # whether the field has been set up
|
||||
|
||||
store = True # whether the field is stored in database
|
||||
index = False # whether the field is indexed in database
|
||||
manual = False # whether the field is a custom field
|
||||
copy = True # whether the field is copied over by BaseModel.copy()
|
||||
depends = () # collection of field dependencies
|
||||
recursive = False # whether self depends on itself
|
||||
compute = None # compute(recs) computes field on recs
|
||||
compute_sudo = False # whether field should be recomputed as admin
|
||||
inverse = None # inverse(recs) inverses field on recs
|
||||
search = None # search(recs, operator, value) searches on self
|
||||
related = None # sequence of field names, for related fields
|
||||
related_sudo = True # whether related fields should be read as admin
|
||||
company_dependent = False # whether `self` is company-dependent (property field)
|
||||
default = None # default(recs) returns the default value
|
||||
'name': None, # name of the field
|
||||
'model_name': None, # name of the model of this field
|
||||
'comodel_name': None, # name of the model of values (if relational)
|
||||
|
||||
string = None # field label
|
||||
help = None # field tooltip
|
||||
readonly = False
|
||||
required = False
|
||||
states = None
|
||||
groups = False # csv list of group xml ids
|
||||
change_default = None # whether the field may trigger a "user-onchange"
|
||||
deprecated = None # whether the field is ... deprecated
|
||||
'store': True, # whether the field is stored in database
|
||||
'index': False, # whether the field is indexed in database
|
||||
'manual': False, # whether the field is a custom field
|
||||
'copy': True, # whether the field is copied over by BaseModel.copy()
|
||||
'depends': (), # collection of field dependencies
|
||||
'recursive': False, # whether self depends on itself
|
||||
'compute': None, # compute(recs) computes field on recs
|
||||
'compute_sudo': False, # whether field should be recomputed as admin
|
||||
'inverse': None, # inverse(recs) inverses field on recs
|
||||
'search': None, # search(recs, operator, value) searches on self
|
||||
'related': None, # sequence of field names, for related fields
|
||||
'related_sudo': True, # whether related fields should be read as admin
|
||||
'company_dependent': False, # whether `self` is company-dependent (property field)
|
||||
'default': None, # default(recs) returns the default value
|
||||
|
||||
'string': None, # field label
|
||||
'help': None, # field tooltip
|
||||
'readonly': False, # whether the field is readonly
|
||||
'required': False, # whether the field is required
|
||||
'states': None, # set readonly and required depending on state
|
||||
'groups': None, # csv list of group xml ids
|
||||
'change_default': False, # whether the field may trigger a "user-onchange"
|
||||
'deprecated': None, # whether the field is deprecated
|
||||
|
||||
'inverse_fields': (), # collection of inverse fields (objects)
|
||||
'computed_fields': (), # fields computed with the same method as self
|
||||
'related_field': None, # corresponding related field
|
||||
'_triggers': (), # invalidation and recomputation triggers
|
||||
}
|
||||
|
||||
def __init__(self, string=None, **kwargs):
|
||||
kwargs['string'] = string
|
||||
self._attrs = {key: val for key, val in kwargs.iteritems() if val is not None}
|
||||
self._free_attrs = []
|
||||
attrs = {key: val for key, val in kwargs.iteritems() if val is not None}
|
||||
self._attrs = attrs or EMPTY_DICT
|
||||
|
||||
# self._triggers is a set of pairs (field, path) that represents the
|
||||
# computed fields that depend on `self`. When `self` is modified, it
|
||||
# invalidates the cache of each `field`, and registers the records to
|
||||
# recompute based on `path`. See method `modified` below for details.
|
||||
self._triggers = set()
|
||||
self.inverse_fields = []
|
||||
def __getattr__(self, name):
|
||||
""" Access non-slot field attribute. """
|
||||
try:
|
||||
return self._attrs[name]
|
||||
except KeyError:
|
||||
raise AttributeError(name)
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
""" Set slot or non-slot field attribute. """
|
||||
try:
|
||||
object.__setattr__(self, name, value)
|
||||
except AttributeError:
|
||||
if self._attrs:
|
||||
self._attrs[name] = value
|
||||
else:
|
||||
self._attrs = {name: value} # replace EMPTY_DICT
|
||||
|
||||
def __delattr__(self, name):
|
||||
""" Remove non-slot field attribute. """
|
||||
try:
|
||||
del self._attrs[name]
|
||||
except KeyError:
|
||||
raise AttributeError(name)
|
||||
|
||||
def new(self, **kwargs):
|
||||
""" Return a field of the same type as `self`, with its own parameters. """
|
||||
@@ -310,6 +351,10 @@ class Field(object):
|
||||
|
||||
def set_class_name(self, cls, name):
|
||||
""" Assign the model class and field name of `self`. """
|
||||
self_attrs = self._attrs
|
||||
for attr, value in self._slots.iteritems():
|
||||
setattr(self, attr, value)
|
||||
|
||||
self.model_name = cls._name
|
||||
self.name = name
|
||||
|
||||
@@ -320,7 +365,7 @@ class Field(object):
|
||||
attrs.update(field._attrs)
|
||||
else:
|
||||
attrs.clear()
|
||||
attrs.update(self._attrs) # necessary in case self is not in cls
|
||||
attrs.update(self_attrs) # necessary in case self is not in cls
|
||||
|
||||
# initialize `self` with `attrs`
|
||||
if attrs.get('compute'):
|
||||
@@ -338,8 +383,6 @@ class Field(object):
|
||||
attrs.pop('store', None)
|
||||
|
||||
for attr, value in attrs.iteritems():
|
||||
if not hasattr(self, attr):
|
||||
self._free_attrs.append(attr)
|
||||
setattr(self, attr, value)
|
||||
|
||||
if not self.string and not self.related:
|
||||
@@ -478,10 +521,9 @@ class Field(object):
|
||||
if not getattr(self, attr):
|
||||
setattr(self, attr, getattr(field, prop))
|
||||
|
||||
for attr in field._free_attrs:
|
||||
if attr not in self._free_attrs:
|
||||
self._free_attrs.append(attr)
|
||||
setattr(self, attr, getattr(field, attr))
|
||||
for attr, value in field._attrs.iteritems():
|
||||
if attr not in self._attrs:
|
||||
setattr(self, attr, value)
|
||||
|
||||
# special case for states: copy it only for inherited fields
|
||||
if not self.states and self.inherited:
|
||||
@@ -533,6 +575,16 @@ class Field(object):
|
||||
#
|
||||
# Setup of field triggers
|
||||
#
|
||||
# The triggers is a collection of pairs (field, path) of computed fields
|
||||
# that depend on `self`. When `self` is modified, it invalidates the cache
|
||||
# of each `field`, and registers the records to recompute based on `path`.
|
||||
# See method `modified` below for details.
|
||||
#
|
||||
|
||||
def add_trigger(self, trigger):
|
||||
""" Add a recomputation trigger on `self`. """
|
||||
if trigger not in self._triggers:
|
||||
self._triggers += (trigger,)
|
||||
|
||||
def setup_triggers(self, env):
|
||||
""" Add the necessary triggers to invalidate/recompute `self`. """
|
||||
@@ -561,12 +613,12 @@ class Field(object):
|
||||
continue
|
||||
|
||||
#_logger.debug("Add trigger on %s to recompute %s", field, self)
|
||||
field._triggers.add((self, '.'.join(path0 or ['id'])))
|
||||
field.add_trigger((self, '.'.join(path0 or ['id'])))
|
||||
|
||||
# add trigger on inverse fields, too
|
||||
for invf in field.inverse_fields:
|
||||
#_logger.debug("Add trigger on %s to recompute %s", invf, self)
|
||||
invf._triggers.add((self, '.'.join(path0 + [head])))
|
||||
invf.add_trigger((self, '.'.join(path0 + [head])))
|
||||
|
||||
# recursively traverse the dependency
|
||||
if tail:
|
||||
@@ -648,8 +700,8 @@ class Field(object):
|
||||
args = {}
|
||||
for attr, prop in self.column_attrs:
|
||||
args[attr] = getattr(self, prop)
|
||||
for attr in self._free_attrs:
|
||||
args[attr] = getattr(self, attr)
|
||||
for attr, value in self._attrs.iteritems():
|
||||
args[attr] = value
|
||||
|
||||
if self.company_dependent:
|
||||
# company-dependent fields are mapped to former property fields
|
||||
@@ -959,10 +1011,11 @@ class Boolean(Field):
|
||||
|
||||
class Integer(Field):
|
||||
type = 'integer'
|
||||
group_operator = None # operator for aggregating values
|
||||
_slots = {
|
||||
'group_operator': None, # operator for aggregating values
|
||||
}
|
||||
|
||||
_related_group_operator = property(attrgetter('group_operator'))
|
||||
|
||||
_column_group_operator = property(attrgetter('group_operator'))
|
||||
|
||||
def convert_to_cache(self, value, record, validate=True):
|
||||
@@ -990,27 +1043,31 @@ class Float(Field):
|
||||
cursor and returning a pair (total, decimal)
|
||||
"""
|
||||
type = 'float'
|
||||
_digits = None # digits argument passed to class initializer
|
||||
digits = None # digits as computed by setup()
|
||||
group_operator = None # operator for aggregating values
|
||||
_slots = {
|
||||
'_digits': None, # digits argument passed to class initializer
|
||||
'group_operator': None, # operator for aggregating values
|
||||
}
|
||||
|
||||
def __init__(self, string=None, digits=None, **kwargs):
|
||||
super(Float, self).__init__(string=string, _digits=digits, **kwargs)
|
||||
|
||||
@property
|
||||
def digits(self):
|
||||
if callable(self._digits):
|
||||
with registry().cursor() as cr:
|
||||
return self._digits(cr)
|
||||
else:
|
||||
return self._digits
|
||||
|
||||
def _setup_digits(self, env):
|
||||
""" Setup the digits for `self` and its corresponding column """
|
||||
self.digits = self._digits(env.cr) if callable(self._digits) else self._digits
|
||||
if self.digits:
|
||||
assert isinstance(self.digits, (tuple, list)) and len(self.digits) >= 2, \
|
||||
"Float field %s with digits %r, expecting (total, decimal)" % (self, self.digits)
|
||||
if self.column:
|
||||
self.column.digits_change(env.cr)
|
||||
pass
|
||||
|
||||
def _setup_regular(self, env):
|
||||
super(Float, self)._setup_regular(env)
|
||||
self._setup_digits(env)
|
||||
|
||||
_related_digits = property(attrgetter('digits'))
|
||||
_related__digits = property(attrgetter('_digits'))
|
||||
_related_group_operator = property(attrgetter('group_operator'))
|
||||
|
||||
_description_digits = property(attrgetter('digits'))
|
||||
@@ -1021,15 +1078,16 @@ class Float(Field):
|
||||
|
||||
def convert_to_cache(self, value, record, validate=True):
|
||||
# apply rounding here, otherwise value in cache may be wrong!
|
||||
if self.digits:
|
||||
return float_round(float(value or 0.0), precision_digits=self.digits[1])
|
||||
else:
|
||||
return float(value or 0.0)
|
||||
value = float(value or 0.0)
|
||||
digits = self.digits
|
||||
return float_round(value, precision_digits=digits[1]) if digits else value
|
||||
|
||||
|
||||
class _String(Field):
|
||||
""" Abstract class for string fields. """
|
||||
translate = False
|
||||
_slots = {
|
||||
'translate': False, # whether the field is translated
|
||||
}
|
||||
|
||||
_column_translate = property(attrgetter('translate'))
|
||||
_related_translate = property(attrgetter('translate'))
|
||||
@@ -1044,17 +1102,19 @@ class Char(_String):
|
||||
:param bool translate: whether the values of this field can be translated
|
||||
"""
|
||||
type = 'char'
|
||||
size = None
|
||||
_slots = {
|
||||
'size': None, # maximum size of values (deprecated)
|
||||
}
|
||||
|
||||
_column_size = property(attrgetter('size'))
|
||||
_related_size = property(attrgetter('size'))
|
||||
_description_size = property(attrgetter('size'))
|
||||
|
||||
def _setup_regular(self, env):
|
||||
super(Char, self)._setup_regular(env)
|
||||
assert isinstance(self.size, (NoneType, int)), \
|
||||
"Char field %s with non-integer size %r" % (self, self.size)
|
||||
|
||||
_column_size = property(attrgetter('size'))
|
||||
_related_size = property(attrgetter('size'))
|
||||
_description_size = property(attrgetter('size'))
|
||||
|
||||
def convert_to_cache(self, value, record, validate=True):
|
||||
if value is None or value is False:
|
||||
return False
|
||||
@@ -1075,8 +1135,10 @@ class Text(_String):
|
||||
|
||||
class Html(_String):
|
||||
type = 'html'
|
||||
sanitize = True # whether value must be sanitized
|
||||
strip_style = False # whether to strip style attributes
|
||||
_slots = {
|
||||
'sanitize': True, # whether value must be sanitized
|
||||
'strip_style': False, # whether to strip style attributes
|
||||
}
|
||||
|
||||
_column_sanitize = property(attrgetter('sanitize'))
|
||||
_related_sanitize = property(attrgetter('sanitize'))
|
||||
@@ -1245,8 +1307,9 @@ class Selection(Field):
|
||||
<field-incremental-definition>`.
|
||||
"""
|
||||
type = 'selection'
|
||||
selection = None # [(value, string), ...], function or method name
|
||||
selection_add = None # [(value, string), ...]
|
||||
_slots = {
|
||||
'selection': None, # [(value, string), ...], function or method name
|
||||
}
|
||||
|
||||
def __init__(self, selection=None, string=None, **kwargs):
|
||||
if callable(selection):
|
||||
@@ -1337,20 +1400,18 @@ class Selection(Field):
|
||||
|
||||
class Reference(Selection):
|
||||
type = 'reference'
|
||||
size = None
|
||||
_slots = {
|
||||
'size': None, # maximum size of values (deprecated)
|
||||
}
|
||||
|
||||
def __init__(self, selection=None, string=None, **kwargs):
|
||||
super(Reference, self).__init__(selection=selection, string=string, **kwargs)
|
||||
_related_size = property(attrgetter('size'))
|
||||
_column_size = property(attrgetter('size'))
|
||||
|
||||
def _setup_regular(self, env):
|
||||
super(Reference, self)._setup_regular(env)
|
||||
assert isinstance(self.size, (NoneType, int)), \
|
||||
"Reference field %s with non-integer size %r" % (self, self.size)
|
||||
|
||||
_related_size = property(attrgetter('size'))
|
||||
|
||||
_column_size = property(attrgetter('size'))
|
||||
|
||||
def convert_to_cache(self, value, record, validate=True):
|
||||
if isinstance(value, BaseModel):
|
||||
if ((not validate or value._name in self.get_values(record.env))
|
||||
@@ -1376,8 +1437,10 @@ class Reference(Selection):
|
||||
class _Relational(Field):
|
||||
""" Abstract class for relational fields. """
|
||||
relational = True
|
||||
domain = None # domain for searching values
|
||||
context = None # context for searching values
|
||||
_slots = {
|
||||
'domain': [], # domain for searching values
|
||||
'context': {}, # context for searching values
|
||||
}
|
||||
|
||||
def _setup_regular(self, env):
|
||||
super(_Relational, self)._setup_regular(env)
|
||||
@@ -1445,9 +1508,11 @@ class Many2one(_Relational):
|
||||
fields or field extensions.
|
||||
"""
|
||||
type = 'many2one'
|
||||
ondelete = 'set null' # what to do when value is deleted
|
||||
auto_join = False # whether joins are generated upon search
|
||||
delegate = False # whether self implements delegation
|
||||
_slots = {
|
||||
'ondelete': 'set null', # what to do when value is deleted
|
||||
'auto_join': False, # whether joins are generated upon search
|
||||
'delegate': False, # whether self implements delegation
|
||||
}
|
||||
|
||||
def __init__(self, comodel_name=None, string=None, **kwargs):
|
||||
super(Many2one, self).__init__(comodel_name=comodel_name, string=string, **kwargs)
|
||||
@@ -1650,10 +1715,12 @@ class One2many(_RelationalMulti):
|
||||
the case of related fields or field extensions.
|
||||
"""
|
||||
type = 'one2many'
|
||||
inverse_name = None # name of the inverse field
|
||||
auto_join = False # whether joins are generated upon search
|
||||
limit = None # optional limit to use upon read
|
||||
copy = False # o2m are not copied by default
|
||||
_slots = {
|
||||
'inverse_name': None, # name of the inverse field
|
||||
'auto_join': False, # whether joins are generated upon search
|
||||
'limit': None, # optional limit to use upon read
|
||||
'copy': False, # o2m are not copied by default
|
||||
}
|
||||
|
||||
def __init__(self, comodel_name=None, inverse_name=None, string=None, **kwargs):
|
||||
super(One2many, self).__init__(
|
||||
@@ -1674,8 +1741,8 @@ class One2many(_RelationalMulti):
|
||||
# (res_model/res_id pattern). Only inverse the field if this is
|
||||
# a `Many2one` field.
|
||||
if isinstance(invf, Many2one):
|
||||
self.inverse_fields.append(invf)
|
||||
invf.inverse_fields.append(self)
|
||||
self.inverse_fields += (invf,)
|
||||
invf.inverse_fields += (self,)
|
||||
|
||||
_description_relation_field = property(attrgetter('inverse_name'))
|
||||
|
||||
@@ -1715,10 +1782,12 @@ class Many2many(_RelationalMulti):
|
||||
|
||||
"""
|
||||
type = 'many2many'
|
||||
relation = None # name of table
|
||||
column1 = None # column of table referring to model
|
||||
column2 = None # column of table referring to comodel
|
||||
limit = None # optional limit to use upon read
|
||||
_slots = {
|
||||
'relation': None, # name of table
|
||||
'column1': None, # column of table referring to model
|
||||
'column2': None, # column of table referring to comodel
|
||||
'limit': None, # optional limit to use upon read
|
||||
}
|
||||
|
||||
def __init__(self, comodel_name=None, relation=None, column1=None, column2=None,
|
||||
string=None, **kwargs):
|
||||
@@ -1746,8 +1815,8 @@ class Many2many(_RelationalMulti):
|
||||
# if inverse field has already been setup, it is present in m2m
|
||||
invf = m2m.get((self.relation, self.column2, self.column1))
|
||||
if invf:
|
||||
self.inverse_fields.append(invf)
|
||||
invf.inverse_fields.append(self)
|
||||
self.inverse_fields += (invf,)
|
||||
invf.inverse_fields += (self,)
|
||||
else:
|
||||
# add self in m2m, so that its inverse field can find it
|
||||
m2m[(self.relation, self.column1, self.column2)] = self
|
||||
@@ -1768,15 +1837,15 @@ class Serialized(Field):
|
||||
|
||||
class Id(Field):
|
||||
""" Special case for field 'id'. """
|
||||
store = True
|
||||
#: Can't write this!
|
||||
readonly = True
|
||||
|
||||
def __init__(self, string=None, **kwargs):
|
||||
super(Id, self).__init__(type='integer', string=string, **kwargs)
|
||||
type = 'integer'
|
||||
_slots = {
|
||||
'string': 'ID',
|
||||
'store': True,
|
||||
'readonly': True,
|
||||
}
|
||||
|
||||
def to_column(self):
|
||||
self.column = fields.integer('ID')
|
||||
self.column = fields.integer(self.string)
|
||||
return self.column
|
||||
|
||||
def __get__(self, record, owner):
|
||||
@@ -1790,7 +1859,7 @@ class Id(Field):
|
||||
raise TypeError("field 'id' cannot be assigned")
|
||||
|
||||
# imported here to avoid dependency cycle issues
|
||||
from openerp import SUPERUSER_ID
|
||||
from openerp import SUPERUSER_ID, registry
|
||||
from .exceptions import Warning, AccessError, MissingError
|
||||
from .models import BaseModel, MAGIC_COLUMNS
|
||||
from .osv import fields
|
||||
|
||||
+65
-78
@@ -334,13 +334,6 @@ class BaseModel(object):
|
||||
# field_column_obj, origina_parent_model), ... }
|
||||
_inherit_fields = {}
|
||||
|
||||
# Mapping field name/column_info object
|
||||
# This is similar to _inherit_fields but:
|
||||
# 1. includes self fields,
|
||||
# 2. uses column_info instead of a triple.
|
||||
# Warning: _all_columns is deprecated, use _fields instead
|
||||
_all_columns = {}
|
||||
|
||||
_table = None
|
||||
_log_create = False
|
||||
_sql_constraints = []
|
||||
@@ -486,7 +479,6 @@ class BaseModel(object):
|
||||
"""
|
||||
field = cls._fields.pop(name)
|
||||
cls._columns.pop(name, None)
|
||||
cls._all_columns.pop(name, None)
|
||||
if hasattr(cls, name):
|
||||
delattr(cls, name)
|
||||
return field
|
||||
@@ -565,11 +557,22 @@ class BaseModel(object):
|
||||
|
||||
"""
|
||||
|
||||
# IMPORTANT: the registry contains an instance for each model. The class
|
||||
# of each model carries inferred metadata that is shared among the
|
||||
# model's instances for this registry, but not among registries. Hence
|
||||
# we cannot use that "registry class" for combining model classes by
|
||||
# inheritance, since it confuses the metadata inference process.
|
||||
# The model's class inherits from cls and the classes of the inherited
|
||||
# models. All those classes are combined in a flat hierarchy:
|
||||
#
|
||||
# Model the base class of all models
|
||||
# / | \
|
||||
# cls c2 c1 the classes defined in modules
|
||||
# \ | /
|
||||
# ModelClass the final class of the model
|
||||
# / | \
|
||||
# model recordset ... the class' instances
|
||||
#
|
||||
# The registry contains the instance `model`. Its class, `ModelClass`,
|
||||
# carries inferred metadata that is shared between all the model's
|
||||
# instances for this registry only. When we '_inherit' from another
|
||||
# model, we do not inherit its `ModelClass`, but this class' parents.
|
||||
# This is a limitation of the inheritance mechanism.
|
||||
|
||||
# Keep links to non-inherited constraints in cls; this is useful for
|
||||
# instance when exporting translations
|
||||
@@ -586,65 +589,54 @@ class BaseModel(object):
|
||||
# determine the module that introduced the model
|
||||
original_module = pool[name]._original_module if name in parents else cls._module
|
||||
|
||||
# build the class hierarchy for the model
|
||||
# determine all the classes the model should inherit from
|
||||
bases = [cls]
|
||||
hierarchy = cls
|
||||
for parent in parents:
|
||||
if parent not in pool:
|
||||
raise TypeError('The model "%s" specifies an unexisting parent class "%s"\n'
|
||||
'You may need to add a dependency on the parent class\' module.' % (name, parent))
|
||||
parent_model = pool[parent]
|
||||
parent_class = type(pool[parent])
|
||||
bases += parent_class.__bases__
|
||||
hierarchy = type(name, (hierarchy, parent_class), {'_register': False})
|
||||
|
||||
# do no use the class of parent_model, since that class contains
|
||||
# inferred metadata; use its ancestor instead
|
||||
parent_class = type(parent_model).__base__
|
||||
# order bases following the mro of class hierarchy
|
||||
bases = [base for base in hierarchy.mro() if base in bases]
|
||||
|
||||
inherits = dict(parent_class._inherits)
|
||||
inherits.update(cls._inherits)
|
||||
# determine the attributes of the model's class
|
||||
inherits = {}
|
||||
depends = {}
|
||||
constraints = {}
|
||||
sql_constraints = []
|
||||
|
||||
depends = dict(parent_class._depends)
|
||||
for m, fs in cls._depends.iteritems():
|
||||
depends[m] = depends.get(m, []) + fs
|
||||
for base in reversed(bases):
|
||||
inherits.update(base._inherits)
|
||||
|
||||
old_constraints = parent_class._constraints
|
||||
new_constraints = cls._constraints
|
||||
# filter out from old_constraints the ones overridden by a
|
||||
# constraint with the same function name in new_constraints
|
||||
constraints = new_constraints + [oldc
|
||||
for oldc in old_constraints
|
||||
if not any(newc[2] == oldc[2] and same_name(newc[0], oldc[0])
|
||||
for newc in new_constraints)
|
||||
]
|
||||
for mname, fnames in base._depends.iteritems():
|
||||
depends[mname] = depends.get(mname, []) + fnames
|
||||
|
||||
sql_constraints = cls._sql_constraints + \
|
||||
parent_class._sql_constraints
|
||||
for cons in base._constraints:
|
||||
# cons may override a constraint with the same function name
|
||||
constraints[getattr(cons[0], '__name__', id(cons[0]))] = cons
|
||||
|
||||
attrs = {
|
||||
'_name': name,
|
||||
'_register': False,
|
||||
'_inherits': inherits,
|
||||
'_depends': depends,
|
||||
'_constraints': constraints,
|
||||
'_sql_constraints': sql_constraints,
|
||||
}
|
||||
cls = type(name, (cls, parent_class), attrs)
|
||||
sql_constraints += base._sql_constraints
|
||||
|
||||
# introduce the "registry class" of the model;
|
||||
# duplicate some attributes so that the ORM can modify them
|
||||
attrs = {
|
||||
# build the actual class of the model
|
||||
ModelClass = type(name, tuple(bases), {
|
||||
'_name': name,
|
||||
'_register': False,
|
||||
'_columns': None, # recomputed in _setup_fields()
|
||||
'_defaults': None, # recomputed in _setup_base()
|
||||
'_fields': frozendict(), # idem
|
||||
'_inherits': dict(cls._inherits),
|
||||
'_depends': dict(cls._depends),
|
||||
'_constraints': list(cls._constraints),
|
||||
'_sql_constraints': list(cls._sql_constraints),
|
||||
'_inherits': inherits,
|
||||
'_depends': depends,
|
||||
'_constraints': constraints.values(),
|
||||
'_sql_constraints': sql_constraints,
|
||||
'_original_module': original_module,
|
||||
}
|
||||
cls = type(cls._name, (cls,), attrs)
|
||||
})
|
||||
|
||||
# instantiate the model, and initialize it
|
||||
model = object.__new__(cls)
|
||||
model = object.__new__(ModelClass)
|
||||
model.__init__(pool, cr)
|
||||
return model
|
||||
|
||||
@@ -655,8 +647,6 @@ class BaseModel(object):
|
||||
|
||||
# process store of low-level function fields
|
||||
for fname, column in cls._columns.iteritems():
|
||||
if hasattr(column, 'digits_change'):
|
||||
column.digits_change(cr)
|
||||
# filter out existing store about this field
|
||||
pool._store_function[cls._name] = [
|
||||
stored
|
||||
@@ -821,9 +811,6 @@ class BaseModel(object):
|
||||
"TransientModels must have log_access turned on, " \
|
||||
"in order to implement their access rights policy"
|
||||
|
||||
# prepare ormcache, which must be shared by all instances of the model
|
||||
cls._ormcache = {}
|
||||
|
||||
@api.model
|
||||
@ormcache()
|
||||
def _is_an_ordinary_table(self):
|
||||
@@ -1820,7 +1807,7 @@ class BaseModel(object):
|
||||
``tools.ormcache`` or ``tools.ormcache_multi``.
|
||||
"""
|
||||
try:
|
||||
self._ormcache.clear()
|
||||
self.pool.cache.clear_prefix((self.pool.db_name, self._name))
|
||||
self.pool._any_cache_cleared = True
|
||||
except AttributeError:
|
||||
pass
|
||||
@@ -2911,7 +2898,7 @@ class BaseModel(object):
|
||||
|
||||
@classmethod
|
||||
def _inherits_reload(cls):
|
||||
""" Recompute the _inherit_fields and _all_columns mappings. """
|
||||
""" Recompute the _inherit_fields mapping. """
|
||||
cls._inherit_fields = struct = {}
|
||||
for parent_model, parent_field in cls._inherits.iteritems():
|
||||
parent = cls.pool[parent_model]
|
||||
@@ -2921,19 +2908,17 @@ class BaseModel(object):
|
||||
for name, source in parent._inherit_fields.iteritems():
|
||||
struct[name] = (parent_model, parent_field, source[2], source[3])
|
||||
|
||||
# old-api stuff
|
||||
cls._all_columns = cls._get_column_infos()
|
||||
|
||||
@classmethod
|
||||
def _get_column_infos(cls):
|
||||
"""Returns a dict mapping all fields names (direct fields and
|
||||
inherited field via _inherits) to a ``column_info`` struct
|
||||
giving detailed columns """
|
||||
@property
|
||||
def _all_columns(self):
|
||||
""" Returns a dict mapping all fields names (self fields and inherited
|
||||
field via _inherits) to a ``column_info`` object giving detailed column
|
||||
information. This property is deprecated, use ``_fields`` instead.
|
||||
"""
|
||||
result = {}
|
||||
# do not inverse for loops, since local fields may hide inherited ones!
|
||||
for k, (parent, m2o, col, original_parent) in cls._inherit_fields.iteritems():
|
||||
for k, (parent, m2o, col, original_parent) in self._inherit_fields.iteritems():
|
||||
result[k] = fields.column_info(k, col, parent, m2o, original_parent)
|
||||
for k, col in cls._columns.iteritems():
|
||||
for k, col in self._columns.iteritems():
|
||||
result[k] = fields.column_info(k, col)
|
||||
return result
|
||||
|
||||
@@ -3010,14 +2995,15 @@ class BaseModel(object):
|
||||
if column:
|
||||
cls._columns[name] = column
|
||||
|
||||
# group fields by compute to determine field.computed_fields
|
||||
fields_by_compute = defaultdict(list)
|
||||
# determine field.computed_fields
|
||||
computed_fields = defaultdict(list)
|
||||
for field in cls._fields.itervalues():
|
||||
if field.compute:
|
||||
field.computed_fields = fields_by_compute[field.compute]
|
||||
field.computed_fields.append(field)
|
||||
else:
|
||||
field.computed_fields = []
|
||||
computed_fields[field.compute].append(field)
|
||||
|
||||
for fields in computed_fields.itervalues():
|
||||
for field in fields:
|
||||
field.computed_fields = fields
|
||||
|
||||
@api.model
|
||||
def _setup_complete(self):
|
||||
@@ -3035,9 +3021,10 @@ class BaseModel(object):
|
||||
model = self.env[model_name]
|
||||
for field_name in field_names:
|
||||
field = model._fields[field_name]
|
||||
field._triggers.update(triggers)
|
||||
for trigger in triggers:
|
||||
field.add_trigger(trigger)
|
||||
|
||||
# determine old-api cls._inherit_fields and cls._all_columns
|
||||
# determine old-api structures about inherited fields
|
||||
cls._inherits_reload()
|
||||
|
||||
# register stuff about low-level function fields
|
||||
|
||||
@@ -104,6 +104,10 @@ class Registry(Mapping):
|
||||
""" Same as ``self[model_name]``. """
|
||||
return self.models[model_name]
|
||||
|
||||
@lazy_property
|
||||
def cache(self):
|
||||
return RegistryManager.cache
|
||||
|
||||
@lazy_property
|
||||
def pure_function_fields(self):
|
||||
""" Return the list of pure function fields (field objects) """
|
||||
@@ -287,6 +291,7 @@ class RegistryManager(object):
|
||||
|
||||
"""
|
||||
_registries = None
|
||||
_cache = None
|
||||
_lock = threading.RLock()
|
||||
_saved_lock = None
|
||||
|
||||
@@ -300,13 +305,26 @@ class RegistryManager(object):
|
||||
# cannot specify the memory limit soft on windows...
|
||||
size = 42
|
||||
else:
|
||||
# On average, a clean registry take 25MB of memory + cache
|
||||
avgsz = 30 * 1024 * 1024
|
||||
# A registry takes 10MB of memory on average, so we reserve
|
||||
# 10Mb (registry) + 5Mb (working memory) per registry
|
||||
avgsz = 15 * 1024 * 1024
|
||||
size = int(config['limit_memory_soft'] / avgsz)
|
||||
|
||||
cls._registries = LRU(size)
|
||||
return cls._registries
|
||||
|
||||
@classproperty
|
||||
def cache(cls):
|
||||
""" Return the global LRU ormcache. Its keys are tuples with the
|
||||
following structure: (db_name, model_name, method, args...).
|
||||
"""
|
||||
with cls.lock():
|
||||
if cls._cache is None:
|
||||
# we allocate 8192 cache entries per registry
|
||||
size = 8192 * cls.registries.count
|
||||
cls._cache = LRU(size)
|
||||
return cls._cache
|
||||
|
||||
@classmethod
|
||||
def lock(cls):
|
||||
""" Return the current registry lock. """
|
||||
|
||||
+222
-133
@@ -48,10 +48,11 @@ from psycopg2 import Binary
|
||||
import openerp
|
||||
import openerp.tools as tools
|
||||
from openerp.tools.translate import _
|
||||
from openerp.tools import float_round, float_repr
|
||||
from openerp.tools import html_sanitize
|
||||
from openerp.tools import float_repr, float_round, frozendict, html_sanitize
|
||||
import simplejson
|
||||
from openerp import SUPERUSER_ID
|
||||
from openerp import SUPERUSER_ID, registry
|
||||
|
||||
EMPTY_DICT = frozendict()
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -73,7 +74,6 @@ class _column(object):
|
||||
_classic_read = True
|
||||
_classic_write = True
|
||||
_auto_join = False
|
||||
_prefetch = True
|
||||
_properties = False
|
||||
_type = 'unknown'
|
||||
_obj = None
|
||||
@@ -84,59 +84,64 @@ class _column(object):
|
||||
_symbol_get = None
|
||||
_deprecated = False
|
||||
|
||||
copy = True # whether value is copied by BaseModel.copy()
|
||||
string = None
|
||||
help = ""
|
||||
required = False
|
||||
readonly = False
|
||||
_domain = []
|
||||
_context = {}
|
||||
states = None
|
||||
priority = 0
|
||||
change_default = False
|
||||
size = None
|
||||
ondelete = None
|
||||
translate = False
|
||||
select = False
|
||||
manual = False
|
||||
write = False
|
||||
read = False
|
||||
selectable = True
|
||||
group_operator = False
|
||||
groups = False # CSV list of ext IDs of groups
|
||||
deprecated = False # Optional deprecation warning
|
||||
__slots__ = [
|
||||
'copy', # whether value is copied by BaseModel.copy()
|
||||
'string',
|
||||
'help',
|
||||
'required',
|
||||
'readonly',
|
||||
'_domain',
|
||||
'_context',
|
||||
'states',
|
||||
'priority',
|
||||
'change_default',
|
||||
'size',
|
||||
'ondelete',
|
||||
'translate',
|
||||
'select',
|
||||
'manual',
|
||||
'write',
|
||||
'read',
|
||||
'selectable',
|
||||
'group_operator',
|
||||
'groups', # CSV list of ext IDs of groups
|
||||
'deprecated', # Optional deprecation warning
|
||||
'_args',
|
||||
'_prefetch',
|
||||
]
|
||||
|
||||
def __init__(self, string='unknown', required=False, readonly=False, domain=None, context=None, states=None, priority=0, change_default=False, size=None, ondelete=None, translate=False, select=False, manual=False, **args):
|
||||
def __init__(self, string='unknown', required=False, readonly=False, domain=[], context={}, states=None, priority=0, change_default=False, size=None, ondelete=None, translate=False, select=False, manual=False, **args):
|
||||
"""
|
||||
|
||||
The 'manual' keyword argument specifies if the field is a custom one.
|
||||
It corresponds to the 'state' column in ir_model_fields.
|
||||
|
||||
"""
|
||||
args0 = {
|
||||
'string': string,
|
||||
'help': args.pop('help', None),
|
||||
'required': required,
|
||||
'readonly': readonly,
|
||||
'_domain': domain,
|
||||
'_context': context,
|
||||
'states': states,
|
||||
'priority': priority,
|
||||
'change_default': change_default,
|
||||
'size': size,
|
||||
'ondelete': ondelete.lower() if ondelete else None,
|
||||
'translate': translate,
|
||||
'select': select,
|
||||
'manual': manual,
|
||||
'group_operator': args.pop('group_operator', None),
|
||||
'groups': args.pop('groups', None),
|
||||
'deprecated': args.pop('deprecated', None),
|
||||
}
|
||||
for key, val in args0.iteritems():
|
||||
if val:
|
||||
setattr(self, key, val)
|
||||
# add parameters and default values
|
||||
args['copy'] = args.get('copy', True)
|
||||
args['string'] = string
|
||||
args['help'] = args.get('help', '')
|
||||
args['required'] = required
|
||||
args['readonly'] = readonly
|
||||
args['_domain'] = domain
|
||||
args['_context'] = context
|
||||
args['states'] = states
|
||||
args['priority'] = priority
|
||||
args['change_default'] = change_default
|
||||
args['size'] = size
|
||||
args['ondelete'] = ondelete.lower() if ondelete else None
|
||||
args['translate'] = translate
|
||||
args['select'] = select
|
||||
args['manual'] = manual
|
||||
args['write'] = args.get('write', False)
|
||||
args['read'] = args.get('read', False)
|
||||
args['selectable'] = args.get('selectable', True)
|
||||
args['group_operator'] = args.get('group_operator', None)
|
||||
args['groups'] = args.get('groups', None)
|
||||
args['deprecated'] = args.get('deprecated', None)
|
||||
args['_prefetch'] = args.get('_prefetch', True)
|
||||
|
||||
self._args = args
|
||||
self._args = EMPTY_DICT
|
||||
for key, val in args.iteritems():
|
||||
setattr(self, key, val)
|
||||
|
||||
@@ -144,6 +149,30 @@ class _column(object):
|
||||
if not self._classic_write or self.deprecated or self.manual:
|
||||
self._prefetch = False
|
||||
|
||||
def __getattr__(self, name):
|
||||
""" Access a non-slot attribute. """
|
||||
try:
|
||||
return self._args[name]
|
||||
except KeyError:
|
||||
raise AttributeError(name)
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
""" Set a slot or non-slot attribute. """
|
||||
try:
|
||||
object.__setattr__(self, name, value)
|
||||
except AttributeError:
|
||||
if self._args:
|
||||
self._args[name] = value
|
||||
else:
|
||||
self._args = {name: value} # replace EMPTY_DICT
|
||||
|
||||
def __delattr__(self, name):
|
||||
""" Remove a non-slot attribute. """
|
||||
try:
|
||||
del self._args[name]
|
||||
except KeyError:
|
||||
raise AttributeError(name)
|
||||
|
||||
def new(self, _computed_field=False, **args):
|
||||
""" Return a column like `self` with the given parameters; the parameter
|
||||
`_computed_field` tells whether the corresponding field is computed.
|
||||
@@ -223,6 +252,7 @@ class boolean(_column):
|
||||
_symbol_c = '%s'
|
||||
_symbol_f = bool
|
||||
_symbol_set = (_symbol_c, _symbol_f)
|
||||
__slots__ = []
|
||||
|
||||
def __init__(self, string='unknown', required=False, **args):
|
||||
super(boolean, self).__init__(string=string, required=required, **args)
|
||||
@@ -238,6 +268,7 @@ class integer(_column):
|
||||
_symbol_f = lambda x: int(x or 0)
|
||||
_symbol_set = (_symbol_c, _symbol_f)
|
||||
_symbol_get = lambda self,x: x or 0
|
||||
__slots__ = []
|
||||
|
||||
def __init__(self, string='unknown', required=False, **args):
|
||||
super(integer, self).__init__(string=string, required=required, **args)
|
||||
@@ -245,6 +276,7 @@ class integer(_column):
|
||||
class reference(_column):
|
||||
_type = 'reference'
|
||||
_classic_read = False # post-process to handle missing target
|
||||
__slots__ = ['selection']
|
||||
|
||||
def __init__(self, string, selection, size=None, **args):
|
||||
if callable(selection):
|
||||
@@ -297,6 +329,7 @@ def _symbol_set_char(self, symb):
|
||||
|
||||
class char(_column):
|
||||
_type = 'char'
|
||||
__slots__ = ['_symbol_f', '_symbol_set', '_symbol_set_char']
|
||||
|
||||
def __init__(self, string="unknown", size=None, **args):
|
||||
_column.__init__(self, string=string, size=size or None, **args)
|
||||
@@ -306,11 +339,13 @@ class char(_column):
|
||||
|
||||
class text(_column):
|
||||
_type = 'text'
|
||||
__slots__ = []
|
||||
|
||||
|
||||
class html(text):
|
||||
_type = 'html'
|
||||
_symbol_c = '%s'
|
||||
__slots__ = ['_sanitize', '_strip_style', '_symbol_f', '_symbol_set']
|
||||
|
||||
def _symbol_set_html(self, value):
|
||||
if value is None or value is False:
|
||||
@@ -334,39 +369,47 @@ class html(text):
|
||||
|
||||
import __builtin__
|
||||
|
||||
def _symbol_set_float(self, x):
|
||||
result = __builtin__.float(x or 0.0)
|
||||
digits = self.digits
|
||||
if digits:
|
||||
precision, scale = digits
|
||||
result = float_repr(float_round(result, precision_digits=scale), precision_digits=scale)
|
||||
return result
|
||||
|
||||
class float(_column):
|
||||
_type = 'float'
|
||||
_symbol_c = '%s'
|
||||
_symbol_f = lambda x: __builtin__.float(x or 0.0)
|
||||
_symbol_set = (_symbol_c, _symbol_f)
|
||||
_symbol_get = lambda self,x: x or 0.0
|
||||
__slots__ = ['_digits', '_digits_compute', '_symbol_f', '_symbol_set']
|
||||
|
||||
@property
|
||||
def digits(self):
|
||||
if self._digits_compute:
|
||||
with registry().cursor() as cr:
|
||||
return self._digits_compute(cr)
|
||||
else:
|
||||
return self._digits
|
||||
|
||||
def __init__(self, string='unknown', digits=None, digits_compute=None, required=False, **args):
|
||||
_column.__init__(self, string=string, required=required, **args)
|
||||
self.digits = digits
|
||||
# synopsis: digits_compute(cr) -> (precision, scale)
|
||||
self.digits_compute = digits_compute
|
||||
|
||||
def new(self, _computed_field=False, **args):
|
||||
# float columns are database-dependent, so always recreate them
|
||||
return type(self)(**args)
|
||||
self._digits = digits
|
||||
self._digits_compute = digits_compute
|
||||
self._symbol_f = lambda x: _symbol_set_float(self, x)
|
||||
self._symbol_set = (self._symbol_c, self._symbol_f)
|
||||
|
||||
def to_field_args(self):
|
||||
args = super(float, self).to_field_args()
|
||||
args['digits'] = self.digits_compute or self.digits
|
||||
args['digits'] = self._digits_compute or self._digits
|
||||
return args
|
||||
|
||||
def digits_change(self, cr):
|
||||
if self.digits_compute:
|
||||
self.digits = self.digits_compute(cr)
|
||||
if self.digits:
|
||||
precision, scale = self.digits
|
||||
self._symbol_set = ('%s', lambda x: float_repr(float_round(__builtin__.float(x or 0.0),
|
||||
precision_digits=scale),
|
||||
precision_digits=scale))
|
||||
pass
|
||||
|
||||
class date(_column):
|
||||
_type = 'date'
|
||||
__slots__ = []
|
||||
|
||||
MONTHS = [
|
||||
('01', 'January'),
|
||||
@@ -457,6 +500,7 @@ class date(_column):
|
||||
|
||||
class datetime(_column):
|
||||
_type = 'datetime'
|
||||
__slots__ = []
|
||||
|
||||
MONTHS = [
|
||||
('01', 'January'),
|
||||
@@ -526,7 +570,7 @@ class datetime(_column):
|
||||
|
||||
class binary(_column):
|
||||
_type = 'binary'
|
||||
_symbol_c = '%s'
|
||||
_classic_read = False
|
||||
|
||||
# Binary values may be byte strings (python 2.6 byte array), but
|
||||
# the legacy OpenERP convention is to transfer and store binaries
|
||||
@@ -534,17 +578,16 @@ class binary(_column):
|
||||
# unicode in some circumstances, hence the str() cast in symbol_f.
|
||||
# This str coercion will only work for pure ASCII unicode strings,
|
||||
# on purpose - non base64 data must be passed as a 8bit byte strings.
|
||||
_symbol_c = '%s'
|
||||
_symbol_f = lambda symb: symb and Binary(str(symb)) or None
|
||||
|
||||
_symbol_set = (_symbol_c, _symbol_f)
|
||||
_symbol_get = lambda self, x: x and str(x)
|
||||
|
||||
_classic_read = False
|
||||
_prefetch = False
|
||||
__slots__ = ['filters']
|
||||
|
||||
def __init__(self, string='unknown', filters=None, **args):
|
||||
_column.__init__(self, string=string, **args)
|
||||
self.filters = filters
|
||||
args['_prefetch'] = args.get('_prefetch', False)
|
||||
_column.__init__(self, string=string, filters=filters, **args)
|
||||
|
||||
def get(self, cr, obj, ids, name, user=None, context=None, values=None):
|
||||
if not context:
|
||||
@@ -572,13 +615,13 @@ class binary(_column):
|
||||
|
||||
class selection(_column):
|
||||
_type = 'selection'
|
||||
__slots__ = ['selection']
|
||||
|
||||
def __init__(self, selection, string='unknown', **args):
|
||||
if callable(selection):
|
||||
from openerp import api
|
||||
selection = api.expected(api.cr_uid_context, selection)
|
||||
_column.__init__(self, string=string, **args)
|
||||
self.selection = selection
|
||||
_column.__init__(self, string=string, selection=selection, **args)
|
||||
|
||||
def to_field_args(self):
|
||||
args = super(selection, self).to_field_args()
|
||||
@@ -639,9 +682,10 @@ class many2one(_column):
|
||||
_symbol_f = lambda x: x or None
|
||||
_symbol_set = (_symbol_c, _symbol_f)
|
||||
|
||||
ondelete = 'set null'
|
||||
__slots__ = ['_obj', '_auto_join']
|
||||
|
||||
def __init__(self, obj, string='unknown', auto_join=False, **args):
|
||||
args['ondelete'] = args.get('ondelete', 'set null')
|
||||
_column.__init__(self, string=string, **args)
|
||||
self._obj = obj
|
||||
self._auto_join = auto_join
|
||||
@@ -687,13 +731,14 @@ class many2one(_column):
|
||||
class one2many(_column):
|
||||
_classic_read = False
|
||||
_classic_write = False
|
||||
_prefetch = False
|
||||
_type = 'one2many'
|
||||
|
||||
# one2many columns are not copied by default
|
||||
copy = False
|
||||
__slots__ = ['_obj', '_fields_id', '_limit', '_auto_join']
|
||||
|
||||
def __init__(self, obj, fields_id, string='unknown', limit=None, auto_join=False, **args):
|
||||
# one2many columns are not copied by default
|
||||
args['copy'] = args.get('copy', False)
|
||||
args['_prefetch'] = args.get('_prefetch', False)
|
||||
_column.__init__(self, string=string, **args)
|
||||
self._obj = obj
|
||||
self._fields_id = fields_id
|
||||
@@ -834,12 +879,14 @@ class many2many(_column):
|
||||
"""
|
||||
_classic_read = False
|
||||
_classic_write = False
|
||||
_prefetch = False
|
||||
_type = 'many2many'
|
||||
|
||||
__slots__ = ['_obj', '_rel', '_id1', '_id2', '_limit', '_auto_join']
|
||||
|
||||
def __init__(self, obj, rel=None, id1=None, id2=None, string='unknown', limit=None, **args):
|
||||
"""
|
||||
"""
|
||||
args['_prefetch'] = args.get('_prefetch', False)
|
||||
_column.__init__(self, string=string, **args)
|
||||
self._obj = obj
|
||||
if rel and '.' in rel:
|
||||
@@ -849,6 +896,7 @@ class many2many(_column):
|
||||
self._id1 = id1
|
||||
self._id2 = id2
|
||||
self._limit = limit
|
||||
self._auto_join = False
|
||||
|
||||
def to_field_args(self):
|
||||
args = super(many2many, self).to_field_args()
|
||||
@@ -1231,44 +1279,80 @@ class function(_column):
|
||||
}
|
||||
|
||||
"""
|
||||
_classic_read = False
|
||||
_classic_write = False
|
||||
_prefetch = False
|
||||
_type = 'function'
|
||||
_properties = True
|
||||
|
||||
# function fields are not copied by default
|
||||
copy = False
|
||||
__slots__ = [
|
||||
'_type',
|
||||
'_classic_read',
|
||||
'_classic_write',
|
||||
'_symbol_c',
|
||||
'_symbol_f',
|
||||
'_symbol_set',
|
||||
'_symbol_get',
|
||||
|
||||
'_fnct',
|
||||
'_arg',
|
||||
'_fnct_inv',
|
||||
'_fnct_inv_arg',
|
||||
'_fnct_search',
|
||||
'_multi',
|
||||
'store',
|
||||
|
||||
'_digits',
|
||||
'_digits_compute',
|
||||
'selection',
|
||||
'_obj',
|
||||
]
|
||||
|
||||
@property
|
||||
def digits(self):
|
||||
if self._digits_compute:
|
||||
with registry().cursor() as cr:
|
||||
return self._digits_compute(cr)
|
||||
else:
|
||||
return self._digits
|
||||
|
||||
#
|
||||
# multi: compute several fields in one call
|
||||
#
|
||||
def __init__(self, fnct, arg=None, fnct_inv=None, fnct_inv_arg=None, type='float', fnct_search=None, obj=None, store=False, multi=False, **args):
|
||||
self._classic_read = False
|
||||
self._classic_write = False
|
||||
self._prefetch = False
|
||||
self._symbol_c = '%s'
|
||||
self._symbol_f = _symbol_set
|
||||
self._symbol_set = (self._symbol_c, self._symbol_f)
|
||||
self._symbol_get = None
|
||||
|
||||
# pop attributes that should not be assigned to self
|
||||
self._digits = args.pop('digits', (16,2))
|
||||
self._digits_compute = args.pop('digits_compute', None)
|
||||
self._obj = args.pop('relation', obj)
|
||||
|
||||
# function fields are not copied by default
|
||||
args['copy'] = args.get('copy', False)
|
||||
|
||||
_column.__init__(self, **args)
|
||||
self._obj = obj
|
||||
self._fnct = fnct
|
||||
self._fnct_inv = fnct_inv
|
||||
self._arg = arg
|
||||
self._multi = multi
|
||||
if 'relation' in args:
|
||||
self._obj = args['relation']
|
||||
|
||||
self.digits = args.get('digits', (16,2))
|
||||
self.digits_compute = args.get('digits_compute', None)
|
||||
if callable(args.get('selection')):
|
||||
from openerp import api
|
||||
self.selection = api.expected(api.cr_uid_context, args['selection'])
|
||||
|
||||
self._fnct_inv_arg = fnct_inv_arg
|
||||
if not fnct_inv:
|
||||
self.readonly = 1
|
||||
self._type = type
|
||||
self._fnct = fnct
|
||||
self._arg = arg
|
||||
self._fnct_inv = fnct_inv
|
||||
self._fnct_inv_arg = fnct_inv_arg
|
||||
self._fnct_search = fnct_search
|
||||
self.store = store
|
||||
self._multi = multi
|
||||
|
||||
if not fnct_inv:
|
||||
self.readonly = 1
|
||||
|
||||
if not fnct_search and not store:
|
||||
self.selectable = False
|
||||
|
||||
if callable(args.get('selection')):
|
||||
from openerp import api
|
||||
self.selection = api.expected(api.cr_uid_context, args['selection'])
|
||||
|
||||
if store:
|
||||
if self._type != 'many2one':
|
||||
# m2o fields need to return tuples with name_get, not just foreign keys
|
||||
@@ -1283,6 +1367,10 @@ class function(_column):
|
||||
self._symbol_c = char._symbol_c
|
||||
self._symbol_f = lambda x: _symbol_set_char(self, x)
|
||||
self._symbol_set = (self._symbol_c, self._symbol_f)
|
||||
elif type == 'float':
|
||||
self._symbol_c = float._symbol_c
|
||||
self._symbol_f = lambda x: _symbol_set_float(self, x)
|
||||
self._symbol_set = (self._symbol_c, self._symbol_f)
|
||||
else:
|
||||
type_class = globals().get(type)
|
||||
if type_class is not None:
|
||||
@@ -1304,7 +1392,7 @@ class function(_column):
|
||||
args = super(function, self).to_field_args()
|
||||
args['store'] = bool(self.store)
|
||||
if self._type in ('float',):
|
||||
args['digits'] = self.digits_compute or self.digits
|
||||
args['digits'] = self._digits_compute or self._digits
|
||||
elif self._type in ('selection', 'reference'):
|
||||
args['selection'] = self.selection
|
||||
elif self._type in ('many2one', 'one2many', 'many2many'):
|
||||
@@ -1312,14 +1400,7 @@ class function(_column):
|
||||
return args
|
||||
|
||||
def digits_change(self, cr):
|
||||
if self._type == 'float':
|
||||
if self.digits_compute:
|
||||
self.digits = self.digits_compute(cr)
|
||||
if self.digits:
|
||||
precision, scale = self.digits
|
||||
self._symbol_set = ('%s', lambda x: float_repr(float_round(__builtin__.float(x or 0.0),
|
||||
precision_digits=scale),
|
||||
precision_digits=scale))
|
||||
pass
|
||||
|
||||
def search(self, cr, uid, obj, name, args, context=None):
|
||||
if not self._fnct_search:
|
||||
@@ -1407,14 +1488,15 @@ class related(function):
|
||||
'bar': fields.related('foo_id', 'frol', type='char', string='Frol of Foo'),
|
||||
}
|
||||
"""
|
||||
__slots__ = ['arg', '_relations']
|
||||
|
||||
def _fnct_search(self, tobj, cr, uid, obj=None, name=None, domain=None, context=None):
|
||||
def _related_search(self, tobj, cr, uid, obj=None, name=None, domain=None, context=None):
|
||||
# assume self._arg = ('foo', 'bar', 'baz')
|
||||
# domain = [(name, op, val)] => search [('foo.bar.baz', op, val)]
|
||||
field = '.'.join(self._arg)
|
||||
return map(lambda x: (field, x[1], x[2]), domain)
|
||||
|
||||
def _fnct_write(self, obj, cr, uid, ids, field_name, values, args, context=None):
|
||||
def _related_write(self, obj, cr, uid, ids, field_name, values, args, context=None):
|
||||
if isinstance(ids, (int, long)):
|
||||
ids = [ids]
|
||||
for instance in obj.browse(cr, uid, ids, context=context):
|
||||
@@ -1425,7 +1507,7 @@ class related(function):
|
||||
# write on the last field of the target record
|
||||
instance.write({self.arg[-1]: values})
|
||||
|
||||
def _fnct_read(self, obj, cr, uid, ids, field_name, args, context=None):
|
||||
def _related_read(self, obj, cr, uid, ids, field_name, args, context=None):
|
||||
res = {}
|
||||
for record in obj.browse(cr, SUPERUSER_ID, ids, context=context):
|
||||
value = record
|
||||
@@ -1452,13 +1534,14 @@ class related(function):
|
||||
def __init__(self, *arg, **args):
|
||||
self.arg = arg
|
||||
self._relations = []
|
||||
super(related, self).__init__(self._fnct_read, arg, self._fnct_write, fnct_inv_arg=arg, fnct_search=self._fnct_search, **args)
|
||||
super(related, self).__init__(self._related_read, arg, self._related_write, fnct_inv_arg=arg, fnct_search=self._related_search, **args)
|
||||
if self.store is True:
|
||||
# TODO: improve here to change self.store = {...} according to related objects
|
||||
pass
|
||||
|
||||
|
||||
class sparse(function):
|
||||
class sparse(function):
|
||||
__slots__ = ['serialization_field']
|
||||
|
||||
def convert_value(self, obj, cr, uid, record, value, read_value, context=None):
|
||||
"""
|
||||
@@ -1507,8 +1590,7 @@ class sparse(function):
|
||||
return read_value
|
||||
return value
|
||||
|
||||
|
||||
def _fnct_write(self,obj,cr, uid, ids, field_name, value, args, context=None):
|
||||
def _sparse_write(self,obj,cr, uid, ids, field_name, value, args, context=None):
|
||||
if not type(ids) == list:
|
||||
ids = [ids]
|
||||
records = obj.browse(cr, uid, ids, context=context)
|
||||
@@ -1523,7 +1605,7 @@ class sparse(function):
|
||||
obj.write(cr, uid, ids, {self.serialization_field: serialized}, context=context)
|
||||
return True
|
||||
|
||||
def _fnct_read(self, obj, cr, uid, ids, field_names, args, context=None):
|
||||
def _sparse_read(self, obj, cr, uid, ids, field_names, args, context=None):
|
||||
results = {}
|
||||
records = obj.browse(cr, uid, ids, context=context)
|
||||
for record in records:
|
||||
@@ -1549,8 +1631,7 @@ class sparse(function):
|
||||
|
||||
def __init__(self, serialization_field, **kwargs):
|
||||
self.serialization_field = serialization_field
|
||||
super(sparse, self).__init__(self._fnct_read, fnct_inv=self._fnct_write, multi='__sparse_multi', **kwargs)
|
||||
|
||||
super(sparse, self).__init__(self._sparse_read, fnct_inv=self._sparse_write, multi='__sparse_multi', **kwargs)
|
||||
|
||||
|
||||
# ---------------------------------------------------------
|
||||
@@ -1558,19 +1639,21 @@ class sparse(function):
|
||||
# ---------------------------------------------------------
|
||||
|
||||
class dummy(function):
|
||||
def _fnct_search(self, tobj, cr, uid, obj=None, name=None, domain=None, context=None):
|
||||
__slots__ = ['arg', '_relations']
|
||||
|
||||
def _dummy_search(self, tobj, cr, uid, obj=None, name=None, domain=None, context=None):
|
||||
return []
|
||||
|
||||
def _fnct_write(self, obj, cr, uid, ids, field_name, values, args, context=None):
|
||||
def _dummy_write(self, obj, cr, uid, ids, field_name, values, args, context=None):
|
||||
return False
|
||||
|
||||
def _fnct_read(self, obj, cr, uid, ids, field_name, args, context=None):
|
||||
def _dummy_read(self, obj, cr, uid, ids, field_name, args, context=None):
|
||||
return {}
|
||||
|
||||
def __init__(self, *arg, **args):
|
||||
self.arg = arg
|
||||
self._relations = []
|
||||
super(dummy, self).__init__(self._fnct_read, arg, self._fnct_write, fnct_inv_arg=arg, fnct_search=self._fnct_search, **args)
|
||||
super(dummy, self).__init__(self._dummy_read, arg, self._dummy_write, fnct_inv_arg=arg, fnct_search=self._dummy_search, **args)
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Serialized fields
|
||||
@@ -1581,42 +1664,46 @@ class serialized(_column):
|
||||
|
||||
Note: only plain components allowed.
|
||||
"""
|
||||
|
||||
_type = 'serialized'
|
||||
__slots__ = []
|
||||
|
||||
def _symbol_set_struct(val):
|
||||
return simplejson.dumps(val)
|
||||
|
||||
def _symbol_get_struct(self, val):
|
||||
return simplejson.loads(val or '{}')
|
||||
|
||||
_prefetch = False
|
||||
_type = 'serialized'
|
||||
|
||||
_symbol_c = '%s'
|
||||
_symbol_f = _symbol_set_struct
|
||||
_symbol_set = (_symbol_c, _symbol_f)
|
||||
_symbol_get = _symbol_get_struct
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs['_prefetch'] = kwargs.get('_prefetch', False)
|
||||
super(serialized, self).__init__(*args, **kwargs)
|
||||
|
||||
# TODO: review completly this class for speed improvement
|
||||
class property(function):
|
||||
__slots__ = []
|
||||
|
||||
def to_field_args(self):
|
||||
args = super(property, self).to_field_args()
|
||||
args['company_dependent'] = True
|
||||
return args
|
||||
|
||||
def _fnct_search(self, tobj, cr, uid, obj, name, domain, context=None):
|
||||
def _property_search(self, tobj, cr, uid, obj, name, domain, context=None):
|
||||
ir_property = obj.pool['ir.property']
|
||||
result = []
|
||||
for field, operator, value in domain:
|
||||
result += ir_property.search_multi(cr, uid, name, tobj._name, operator, value, context=context)
|
||||
return result
|
||||
|
||||
def _fnct_write(self, obj, cr, uid, id, prop_name, value, obj_dest, context=None):
|
||||
def _property_write(self, obj, cr, uid, id, prop_name, value, obj_dest, context=None):
|
||||
ir_property = obj.pool['ir.property']
|
||||
ir_property.set_multi(cr, uid, prop_name, obj._name, {id: value}, context=context)
|
||||
return True
|
||||
|
||||
def _fnct_read(self, obj, cr, uid, ids, prop_names, obj_dest, context=None):
|
||||
def _property_read(self, obj, cr, uid, ids, prop_names, obj_dest, context=None):
|
||||
ir_property = obj.pool['ir.property']
|
||||
|
||||
res = {id: {} for id in ids}
|
||||
@@ -1645,9 +1732,9 @@ class property(function):
|
||||
args = dict(args)
|
||||
args['obj'] = args.pop('relation', '') or args.get('obj', '')
|
||||
super(property, self).__init__(
|
||||
fnct=self._fnct_read,
|
||||
fnct_inv=self._fnct_write,
|
||||
fnct_search=self._fnct_search,
|
||||
fnct=self._property_read,
|
||||
fnct_inv=self._property_write,
|
||||
fnct_search=self._property_search,
|
||||
multi='properties',
|
||||
**args
|
||||
)
|
||||
@@ -1681,6 +1768,8 @@ class column_info(object):
|
||||
contains it i.e in case of multilevel inheritance, ``None`` for
|
||||
local columns.
|
||||
"""
|
||||
__slots__ = ['name', 'column', 'parent_model', 'parent_column', 'original_parent']
|
||||
|
||||
def __init__(self, name, column, parent_model=None, parent_column=None, original_parent=None):
|
||||
self.name = name
|
||||
self.column = column
|
||||
|
||||
@@ -39,7 +39,7 @@ import openerp
|
||||
from openerp.modules.registry import RegistryManager
|
||||
from openerp.release import nt_service_name
|
||||
import openerp.tools.config as config
|
||||
from openerp.tools.misc import stripped_sys_argv, dumpstacks
|
||||
from openerp.tools import stripped_sys_argv, dumpstacks, log_ormcache_stats
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -251,6 +251,7 @@ class ThreadedServer(CommonServer):
|
||||
signal.signal(signal.SIGCHLD, self.signal_handler)
|
||||
signal.signal(signal.SIGHUP, self.signal_handler)
|
||||
signal.signal(signal.SIGQUIT, dumpstacks)
|
||||
signal.signal(signal.SIGUSR1, log_ormcache_stats)
|
||||
elif os.name == 'nt':
|
||||
import win32api
|
||||
win32api.SetConsoleCtrlHandler(lambda sig: self.signal_handler(sig, None), 1)
|
||||
@@ -344,6 +345,7 @@ class GeventServer(CommonServer):
|
||||
|
||||
if os.name == 'posix':
|
||||
signal.signal(signal.SIGQUIT, dumpstacks)
|
||||
signal.signal(signal.SIGUSR1, log_ormcache_stats)
|
||||
|
||||
gevent.spawn(self.watch_parent)
|
||||
self.httpd = WSGIServer((self.interface, self.port), self.app)
|
||||
@@ -465,6 +467,9 @@ class PreforkServer(CommonServer):
|
||||
elif sig == signal.SIGQUIT:
|
||||
# dump stacks on kill -3
|
||||
self.dumpstacks()
|
||||
elif sig == signal.SIGUSR1:
|
||||
# log ormcache stats on kill -SIGUSR1
|
||||
log_ormcache_stats()
|
||||
elif sig == signal.SIGTTIN:
|
||||
# increase number of workers
|
||||
self.population += 1
|
||||
@@ -541,6 +546,7 @@ class PreforkServer(CommonServer):
|
||||
signal.signal(signal.SIGTTIN, self.signal_handler)
|
||||
signal.signal(signal.SIGTTOU, self.signal_handler)
|
||||
signal.signal(signal.SIGQUIT, dumpstacks)
|
||||
signal.signal(signal.SIGUSR1, log_ormcache_stats)
|
||||
|
||||
# listen to socket
|
||||
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
|
||||
+56
-37
@@ -21,13 +21,29 @@
|
||||
|
||||
# decorator makes wrappers that have the same API as their wrapped function;
|
||||
# this is important for the openerp.api.guess() that relies on signatures
|
||||
from collections import defaultdict
|
||||
from decorator import decorator
|
||||
from inspect import getargspec
|
||||
|
||||
import lru
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ormcache_counter(object):
|
||||
""" Statistic counters for cache entries. """
|
||||
__slots__ = ['hit', 'miss', 'err']
|
||||
|
||||
def __init__(self):
|
||||
self.hit = 0
|
||||
self.miss = 0
|
||||
self.err = 0
|
||||
|
||||
@property
|
||||
def ratio(self):
|
||||
return 100.0 * self.hit / (self.hit + self.miss or 1)
|
||||
|
||||
# statistic counters dictionary, maps (dbname, modelname, method) to counter
|
||||
STAT = defaultdict(ormcache_counter)
|
||||
|
||||
|
||||
class ormcache(object):
|
||||
@@ -35,10 +51,6 @@ class ormcache(object):
|
||||
|
||||
def __init__(self, skiparg=2, size=8192, multi=None, timeout=None):
|
||||
self.skiparg = skiparg
|
||||
self.size = size
|
||||
self.stat_miss = 0
|
||||
self.stat_hit = 0
|
||||
self.stat_err = 0
|
||||
|
||||
def __call__(self, method):
|
||||
self.method = method
|
||||
@@ -46,42 +58,32 @@ class ormcache(object):
|
||||
lookup.clear_cache = self.clear
|
||||
return lookup
|
||||
|
||||
def stat(self):
|
||||
return "lookup-stats hit=%s miss=%s err=%s ratio=%.1f" % \
|
||||
(self.stat_hit, self.stat_miss, self.stat_err,
|
||||
(100*float(self.stat_hit))/(self.stat_miss+self.stat_hit))
|
||||
|
||||
def lru(self, model):
|
||||
ormcache = model._ormcache
|
||||
try:
|
||||
d = ormcache[self.method]
|
||||
except KeyError:
|
||||
d = ormcache[self.method] = lru.LRU(self.size)
|
||||
return d
|
||||
return model.pool.cache, (model.pool.db_name, model._name, self.method)
|
||||
|
||||
def lookup(self, method, *args, **kwargs):
|
||||
d = self.lru(args[0])
|
||||
key = args[self.skiparg:]
|
||||
d, key0 = self.lru(args[0])
|
||||
key = key0 + args[self.skiparg:]
|
||||
try:
|
||||
r = d[key]
|
||||
self.stat_hit += 1
|
||||
STAT[key0].hit += 1
|
||||
return r
|
||||
except KeyError:
|
||||
self.stat_miss += 1
|
||||
STAT[key0].miss += 1
|
||||
value = d[key] = self.method(*args, **kwargs)
|
||||
return value
|
||||
except TypeError:
|
||||
self.stat_err += 1
|
||||
STAT[key0].err += 1
|
||||
return self.method(*args, **kwargs)
|
||||
|
||||
def clear(self, model, *args):
|
||||
""" Remove *args entry from the cache or all keys if *args is undefined """
|
||||
d = self.lru(model)
|
||||
d, key0 = self.lru(model)
|
||||
if args:
|
||||
logger.warn("ormcache.clear arguments are deprecated and ignored "
|
||||
"(while clearing caches on (%s).%s)",
|
||||
model._name, self.method.__name__)
|
||||
d.clear()
|
||||
_logger.warn("ormcache.clear arguments are deprecated and ignored "
|
||||
"(while clearing caches on (%s).%s)",
|
||||
model._name, self.method.__name__)
|
||||
d.clear_prefix(key0)
|
||||
model.pool._any_cache_cleared = True
|
||||
|
||||
|
||||
@@ -97,7 +99,7 @@ class ormcache_context(ormcache):
|
||||
return super(ormcache_context, self).__call__(method)
|
||||
|
||||
def lookup(self, method, *args, **kwargs):
|
||||
d = self.lru(args[0])
|
||||
d, key0 = self.lru(args[0])
|
||||
|
||||
# Note. The decorator() wrapper (used in __call__ above) will resolve
|
||||
# arguments, and pass them positionally to lookup(). This is why context
|
||||
@@ -109,17 +111,17 @@ class ormcache_context(ormcache):
|
||||
ckey = [(k, context[k]) for k in self.accepted_keys if k in context]
|
||||
|
||||
# Beware: do not take the context from args!
|
||||
key = args[self.skiparg:self.context_pos] + tuple(ckey)
|
||||
key = key0 + args[self.skiparg:self.context_pos] + tuple(ckey)
|
||||
try:
|
||||
r = d[key]
|
||||
self.stat_hit += 1
|
||||
STAT[key0].hit += 1
|
||||
return r
|
||||
except KeyError:
|
||||
self.stat_miss += 1
|
||||
STAT[key0].miss += 1
|
||||
value = d[key] = self.method(*args, **kwargs)
|
||||
return value
|
||||
except TypeError:
|
||||
self.stat_err += 1
|
||||
STAT[key0].err += 1
|
||||
return self.method(*args, **kwargs)
|
||||
|
||||
|
||||
@@ -130,8 +132,8 @@ class ormcache_multi(ormcache):
|
||||
self.multi = multi
|
||||
|
||||
def lookup(self, method, *args, **kwargs):
|
||||
d = self.lru(args[0])
|
||||
base_key = args[self.skiparg:self.multi] + args[self.multi+1:]
|
||||
d, key0 = self.lru(args[0])
|
||||
base_key = key0 + args[self.skiparg:self.multi] + args[self.multi+1:]
|
||||
ids = args[self.multi]
|
||||
result = {}
|
||||
missed = []
|
||||
@@ -141,9 +143,9 @@ class ormcache_multi(ormcache):
|
||||
key = base_key + (i,)
|
||||
try:
|
||||
result[i] = d[key]
|
||||
self.stat_hit += 1
|
||||
STAT[key0].hit += 1
|
||||
except Exception:
|
||||
self.stat_miss += 1
|
||||
STAT[key0].miss += 1
|
||||
missed.append(i)
|
||||
|
||||
if missed:
|
||||
@@ -173,5 +175,22 @@ class dummy_cache(object):
|
||||
pass
|
||||
|
||||
|
||||
def log_ormcache_stats(sig=None, frame=None):
|
||||
""" Log statistics of ormcache usage by database, model, and method. """
|
||||
from openerp.modules.registry import RegistryManager
|
||||
import threading
|
||||
|
||||
me = threading.currentThread()
|
||||
entries = defaultdict(int)
|
||||
for key in RegistryManager.cache.iterkeys():
|
||||
entries[key[:3]] += 1
|
||||
for key, count in sorted(entries.items()):
|
||||
dbname, model_name, method = key
|
||||
me.dbname = dbname
|
||||
stat = STAT[key]
|
||||
_logger.info("%6d entries, %6d hit, %6d miss, %6d err, %4.1f%% ratio, for %s.%s",
|
||||
count, stat.hit, stat.miss, stat.err, stat.ratio, model_name, method.__name__)
|
||||
|
||||
|
||||
# For backward compatibility
|
||||
cache = ormcache
|
||||
|
||||
@@ -118,3 +118,11 @@ class LRU(object):
|
||||
self.d = {}
|
||||
self.first = None
|
||||
self.last = None
|
||||
|
||||
@synchronized()
|
||||
def clear_prefix(self, prefix):
|
||||
""" Remove from `self` all the items with the given `prefix`. """
|
||||
n = len(prefix)
|
||||
for key in self.keys():
|
||||
if key[:n] == prefix:
|
||||
del self[key]
|
||||
|
||||
Reference in New Issue
Block a user