[FIX] read() method returns None values instead of False, incompatible with XML-RPC
or (not st.journal_id.default_debit_account_id):
raise osv.except_osv(_('Configuration Error!'), _('Please verify that an account is defined in the journal.'))
for line in st.move_line_ids:
- if line.state <> 'valid':
+ if line.state != 'valid':
raise osv.except_osv(_('Error!'), _('The account entries lines are not in valid state.'))
move_ids = []
for st_line in st.line_ids:
if not st_line.amount:
continue
- if not st_line.journal_entry_id.id:
+ if st_line.account_id and not st_line.journal_entry_id.id:
+ #make an account move as before
+ vals = {
+ 'debit': st_line.amount < 0 and -st_line.amount or 0.0,
+ 'credit': st_line.amount > 0 and st_line.amount or 0.0,
+ 'account_id': st_line.account_id.id,
+ 'name': st_line.name
+ }
+ self.pool.get('account.bank.statement.line').process_reconciliation(cr, uid, st_line.id, [vals], context=context)
+ elif not st_line.journal_entry_id.id:
raise osv.except_osv(_('Error!'), _('All the account entries lines must be processed in order to close the statement.'))
move_ids.append(st_line.journal_entry_id.id)
- self.pool.get('account.move').post(cr, uid, move_ids, context=context)
+ if move_ids:
+ self.pool.get('account.move').post(cr, uid, move_ids, context=context)
self.message_post(cr, uid, [st.id], body=_('Statement %s confirmed, journal items were created.') % (st.name,), context=context)
self.link_bank_to_partner(cr, uid, ids, context=context)
return self.write(cr, uid, ids, {'state': 'confirm'}, context=context)
move_id = am_obj.create(cr, uid, move_vals, context=context)
# Create the move line for the statement line
- amount = currency_obj.compute(cr, uid, st_line.statement_id.currency.id, company_currency.id, st_line.amount, context=context)
+ if st_line.statement_id.currency.id != company_currency.id:
+ ctx = context.copy()
+ ctx['date'] = st_line.date
+ amount = currency_obj.compute(cr, uid, st_line.statement_id.currency.id, company_currency.id, st_line.amount_currency, context=ctx)
+ else:
+ amount = st_line.amount
bank_st_move_vals = bs_obj._prepare_bank_move_line(cr, uid, st_line, move_id, amount, company_currency.id, context=context)
aml_obj.create(cr, uid, bank_st_move_vals, context=context)
# Complete the dicts
_description = "Bank Statement Line"
_inherit = ['ir.needaction_mixin']
_columns = {
- 'name': fields.char('Description', required=True, copy=False),
- 'date': fields.date('Date', required=True, copy=False),
+ 'name': fields.char('Description', required=True),
+ 'date': fields.date('Date', required=True),
'amount': fields.float('Amount', digits_compute=dp.get_precision('Account')),
'partner_id': fields.many2one('res.partner', 'Partner'),
'bank_account_id': fields.many2one('res.partner.bank','Bank Account'),
+ 'account_id': fields.many2one('account.account', 'Account', help="This technical field can be used at the statement line creation/import time in order to avoid the reconciliation process on it later on. The statement line will simply create a counterpart on this account"),
'statement_id': fields.many2one('account.bank.statement', 'Statement', select=True, required=True, ondelete='cascade'),
'journal_id': fields.related('statement_id', 'journal_id', type='many2one', relation='account.journal', string='Journal', store=True, readonly=True),
'ref': fields.char('Structured Communication'),
'note': fields.text('Notes'),
'sequence': fields.integer('Sequence', select=True, help="Gives the sequence order when displaying a list of bank statement lines."),
'company_id': fields.related('statement_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
- 'journal_entry_id': fields.many2one('account.move', 'Journal Entry'),
+ 'journal_entry_id': fields.many2one('account.move', 'Journal Entry', copy=False),
'amount_currency': fields.float('Amount Currency', help="The amount expressed in an optional other currency if it is a multi-currency entry.", digits_compute=dp.get_precision('Account')),
'currency_id': fields.many2one('res.currency', 'Currency', help="The optional other currency if it is a multi-currency entry."),
}
else:
self.residual = new_value
# prevent the residual amount on the invoice to be less than 0
- self.residual = max(self.residual, 0.0)
+ self.residual = max(self.residual, 0.0)
@api.one
@api.depends(
#TODO: implement messages system
return True
- @api.one
- def _compute_display_name(self):
+ @api.multi
+ def name_get(self):
TYPES = {
'out_invoice': _('Invoice'),
'in_invoice': _('Supplier Invoice'),
'out_refund': _('Refund'),
'in_refund': _('Supplier Refund'),
}
- self.display_name = "%s %s" % (self.number or TYPES[self.type], self.name or '')
+ result = []
+ for inv in self:
+ result.append((inv.id, "%s %s" % (inv.number or TYPES[inv.type], inv.name or '')))
+ return result
@api.model
def name_search(self, name, args=None, operator='ilike', limit=100):
context.get('default_res_id') and context.get('mark_invoice_as_sent'):
invoice = self.env['account.invoice'].browse(context['default_res_id'])
invoice = invoice.with_context(mail_post_autofollow=True)
- self.write({'sent': True})
- self.message_post(body=_("Invoice sent"))
+ invoice.write({'sent': True})
+ invoice.message_post(body=_("Invoice sent"))
return super(mail_compose_message, self).send_mail()
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
all_moves = list(set(all_moves) - set(move_ids))
if unlink_ids:
if opening_reconciliation:
+ raise osv.except_osv(_('Warning!'),
+ _('Opening Entries have already been generated. Please run "Cancel Closing Entries" wizard to cancel those entries and then run this wizard.'))
obj_move_rec.write(cr, uid, unlink_ids, {'opening_reconciliation': False})
obj_move_rec.unlink(cr, uid, unlink_ids)
if len(all_moves) >= 2:
<xpath expr="//div[@name='buttons']" position="inside">
<button type="action"
class="oe_stat_button"
+ id="invoice_button"
icon="fa-pencil-square-o"
name="%(account.action_invoice_tree)d"
attrs="{'invisible': [('customer', '=', False)]}"
<field name="property_account_income" domain="[('type','=','other')]" groups="account.group_account_user"
attrs="{'readonly': [('is_product_variant', '=', True)]}"/>
<field name="taxes_id" colspan="2" widget="many2many_tags"
- attrs="{'readonly':[ '|', ('sale_ok','=',0), ('is_product_variant', '=', True)]}"/>
+ attrs="{'readonly':[('sale_ok','=',0)]}"/>
</group>
<group>
<field name="property_account_expense" domain="[('type','=','other')]" groups="account.group_account_user"
}
// Retreive statement infos and reconciliation data from the model
- var lines_filter = [['journal_entry_id', '=', false]];
+ var lines_filter = [['journal_entry_id', '=', false], ['account_id', '=', false]];
var deferred_promises = [];
if (self.statement_id) {
from openerp.osv import fields, osv
+from openerp import api
AVAILABLE_STATES = [
('draft', 'New'),
'active' : True,
}
+ @api.cr_uid_ids_context
def message_post(self, cr, uid, thread_id, body='', subject=None, type='notification', subtype=None, parent_id=False, attachments=None, context=None, **kwargs):
pass
'required': False,
'fields': [],
}
-DISPLAY_NAME_FIELD = {
- 'id': 'display_name',
- 'name': 'display_name',
- 'string': "Name",
- 'required': False,
- 'fields': [],
-}
def make_field(name='value', string='unknown', required=False, fields=[]):
return [
ID_FIELD,
- DISPLAY_NAME_FIELD,
{'id': name, 'name': name, 'string': string, 'required': required, 'fields': fields},
]
def test_readonly(self):
""" Readonly fields should be filtered out"""
- self.assertEqualFields(self.get_fields('char.readonly'), [ID_FIELD, DISPLAY_NAME_FIELD])
+ self.assertEqualFields(self.get_fields('char.readonly'), [ID_FIELD])
def test_readonly_states(self):
""" Readonly fields with states should not be filtered out"""
def test_readonly_states_noreadonly(self):
""" Readonly fields with states having nothing to do with
readonly should still be filtered out"""
- self.assertEqualFields(self.get_fields('char.noreadonly'), [ID_FIELD, DISPLAY_NAME_FIELD])
+ self.assertEqualFields(self.get_fields('char.noreadonly'), [ID_FIELD])
def test_readonly_states_stillreadonly(self):
""" Readonly fields with readonly states leaving them readonly
always... filtered out"""
- self.assertEqualFields(self.get_fields('char.stillreadonly'), [ID_FIELD, DISPLAY_NAME_FIELD])
+ self.assertEqualFields(self.get_fields('char.stillreadonly'), [ID_FIELD])
def test_m2o(self):
""" M2O fields should allow import of themselves (name_get),
def test_shallow(self):
self.assertEqualFields(self.get_fields('o2m'), make_field(fields=[
ID_FIELD,
- DISPLAY_NAME_FIELD,
# FIXME: should reverse field be ignored?
{'id': 'parent_id', 'name': 'parent_id', 'string': 'unknown', 'required': False, 'fields': [
{'id': 'parent_id', 'name': 'id', 'string': 'External ID', 'required': False, 'fields': []},
# Order depends on iteration order of fields_get
self.assertItemsEqual(result['fields'], [
ID_FIELD,
- DISPLAY_NAME_FIELD,
{'id': 'name', 'name': 'name', 'string': 'Name', 'required':False, 'fields': []},
{'id': 'somevalue', 'name': 'somevalue', 'string': 'Some Value', 'required':True, 'fields': []},
{'id': 'othervalue', 'name': 'othervalue', 'string': 'Other Variable', 'required':False, 'fields': []},
from dateutil import parser
from dateutil import rrule
from dateutil.relativedelta import relativedelta
+from openerp import api
from openerp import tools, SUPERUSER_ID
from openerp.osv import fields, osv
from openerp.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT
('user_id', '=', uid),
]
+ @api.cr_uid_ids_context
def message_post(self, cr, uid, thread_id, body='', subject=None, type='notification', subtype=None, parent_id=False, attachments=None, context=None, **kwargs):
if isinstance(thread_id, str):
thread_id = get_real_ids(thread_id)
_columns = {
'campaign_id': fields.many2one('crm.tracking.campaign', 'Campaign', # old domain ="['|',('section_id','=',section_id),('section_id','=',False)]"
- help="This is a name that helps you keep track of your different campaign efforts Example: Fall_Drive, Christmas_Special"),
- 'source_id': fields.many2one('crm.tracking.source', 'Source', help="This is the source of the link Example: Search Engine, another domain, or name of email list"),
- 'medium_id': fields.many2one('crm.tracking.medium', 'Channel', help="This is the method of delivery. EX: Postcard, Email, or Banner Ad"),
+ help="This is a name that helps you keep track of your different campaign efforts Ex: Fall_Drive, Christmas_Special"),
+ 'source_id': fields.many2one('crm.tracking.source', 'Source', help="This is the source of the link Ex: Search Engine, another domain, or name of email list"),
+ 'medium_id': fields.many2one('crm.tracking.medium', 'Channel', help="This is the method of delivery. Ex: Postcard, Email, or Banner Ad"),
}
def tracking_fields(self):
def tracking_get_values(self, cr, uid, vals, context=None):
for key, field in self.tracking_fields():
column = self._all_columns[field].column
- value = vals.get(field) or (request and request.session.get(key)) # params.get sould be always in session by the dispatch from ir_http
- if column._type in ['many2one'] and isinstance(value, basestring): # if we receive a string for a many2one, we search / create the id
+ value = vals.get(field) or (request and request.session.get(key)) # params.get should be always in session by the dispatch from ir_http
+ if column._type in ['many2one'] and isinstance(value, basestring): # if we receive a string for a many2one, we search / create the id
if value:
Model = self.pool[column._obj]
rel_id = Model.name_search(cr, uid, value, context=context)
- if not rel_id:
+ if rel_id:
+ rel_id = rel_id[0][0]
+ else:
rel_id = Model.create(cr, uid, {'name': value}, context=context)
vals[field] = rel_id
- # Here the code for other cases that many2one
+ # Here the code for others cases that many2one
else:
vals[field] = value
return vals
'on_change': fields.boolean('Change Probability Automatically', help="Setting this stage will change the probability automatically on the opportunity."),
'requirements': fields.text('Requirements'),
'section_ids': fields.many2many('crm.case.section', 'section_stage_rel', 'stage_id', 'section_id', string='Sections',
- help="Link between stages and sales teams. When set, this limitate the current stage to the selected sales teams."),
+ help="Link between stages and sales teams. When set, this limitate the current stage to the selected sales teams."),
'case_default': fields.boolean('Default to New Sales Team',
- help="If you check this field, this stage will be proposed by default on each sales team. It will not assign this stage to existing teams."),
+ help="If you check this field, this stage will be proposed by default on each sales team. It will not assign this stage to existing teams."),
'fold': fields.boolean('Folded in Kanban View',
help='This stage is folded in the kanban view when'
'there are no records in that stage to display.'),
- 'type': fields.selection([('lead', 'Lead'),
- ('opportunity', 'Opportunity'),
- ('both', 'Both')],
- string='Type', required=True,
- help="This field is used to distinguish stages related to Leads from stages related to Opportunities, or to specify stages available for both types."),
+ 'type': fields.selection([('lead', 'Lead'), ('opportunity', 'Opportunity'), ('both', 'Both')],
+ string='Type', required=True,
+ help="This field is used to distinguish stages related to Leads from stages related to Opportunities, or to specify stages available for both types."),
}
_defaults = {
'case_default': True,
}
+
class crm_case_categ(osv.osv):
""" Category of Case """
_name = "crm.case.categ"
'section_id': fields.many2one('crm.case.section', 'Sales Team'),
'object_id': fields.many2one('ir.model', 'Object Name'),
}
+
def _find_object_id(self, cr, uid, context=None):
"""Finds id for case object"""
context = context or {}
object_id = context.get('object_id', False)
- ids = self.pool.get('ir.model').search(cr, uid, ['|',('id', '=', object_id),('model', '=', context.get('object_name', False))])
+ ids = self.pool.get('ir.model').search(cr, uid, ['|', ('id', '=', object_id), ('model', '=', context.get('object_name', False))])
return ids and ids[0] or False
_defaults = {
- 'object_id' : _find_object_id
+ 'object_id': _find_object_id
}
+
class crm_payment_mode(osv.osv):
""" Payment Mode for Fund """
_name = "crm.payment.mode"
'section_id': fields.many2one('crm.case.section', 'Sales Team'),
}
-
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
<filter string="Sales Team" domain="[]" context="{'group_by':'section_id'}" groups="base.group_multi_salesteams"/>
<filter string="Stage" domain="[]" context="{'group_by':'stage_id'}"/>
<filter string="Customer" help="Partner" domain="[]" context="{'group_by':'partner_id'}"/>
+ <filter string="Campaign" domain="[]" context="{'group_by':'campaign_id'}"/>
+ <filter string="Source" domain="[]" context="{'group_by':'source_id'}"/>
+ <filter string="Channel" domain="[]" context="{'group_by':'medium_id'}"/>
<filter string="Expected Closing" domain="[]" context="{'group_by':'date_deadline:week'}"/>
<filter string="Last Message" name="group_message_last_post" domain="[]" context="{'group_by':'message_last_post:week'}"/>
</group>
'partner_phone' : call.partner_phone,
'partner_mobile' : call.partner_mobile,
'priority': call.priority,
+ 'opportunity_id': call.opportunity_id and call.opportunity_id.id or False,
}
new_id = self.create(cr, uid, vals, context=context)
if action == 'log':
<menuitem id="menu_crm_config_phonecall" name="Phone Calls"
parent="base.menu_base_config" sequence="45" groups="base.group_sale_salesman"/>
- <menuitem id="base.next_id_64" name="Sales"
- parent="base.menu_reporting" sequence="1"/>
-
<!-- crm.tracking.medium -->
<record id="crm_tracking_medium_view_tree" model="ir.ui.view">
<field name="name">crm.tracking.medium.tree</field>
help="The first contact you get with a potential customer is a lead you qualify before converting it into a real business opportunity. Check this box to manage leads in this sales team."),
'use_opportunities': fields.boolean('Opportunities', help="Check this box to manage opportunities in this sales team."),
'monthly_open_leads': fields.function(_get_opportunities_data,
- type="any", readonly=True, multi='_get_opportunities_data',
+ type="char", readonly=True, multi='_get_opportunities_data',
string='Open Leads per Month'),
'monthly_planned_revenue': fields.function(_get_opportunities_data,
- type="any", readonly=True, multi='_get_opportunities_data',
+ type="char", readonly=True, multi='_get_opportunities_data',
string='Planned Revenue per Month'),
'alias_id': fields.many2one('mail.alias', 'Alias', ondelete="restrict", required=True, help="The email address associated with this team. New emails received will automatically create new leads assigned to the team."),
}
<field name="act_window_id" ref="action_report_crm_claim"/>
</record>
- <menuitem id="base.menu_project_report" name="Project"
- groups="base.group_no_one"
- parent="base.menu_reporting" sequence="30"/>
-
<menuitem name="Claims Analysis" id="menu_report_crm_claim_tree"
- action="action_report_crm_claim" parent="base.menu_project_report" sequence="15"/>
-
+ action="action_report_crm_claim" parent="base.next_id_64" sequence="15"/>
</data>
</openerp>
<field name="act_window_id" ref="action_report_crm_helpdesk"/>
</record>
- <menuitem id="base.menu_project_report" name="Project"
- groups="base.group_no_one"
- parent="base.menu_reporting" sequence="30"/>
-
<menuitem name="Helpdesk Analysis" action="action_report_crm_helpdesk"
- id="menu_report_crm_helpdesks_tree" parent="base.menu_project_report" sequence="20"/>
+ id="menu_report_crm_helpdesks_tree" parent="base.next_id_64" sequence="20"/>
</data>
</openerp>
#
##############################################################################
+import logging
import time
from openerp.osv import fields,osv
from openerp.tools.translate import _
import openerp.addons.decimal_precision as dp
+_logger = logging.getLogger(__name__)
+
class delivery_carrier(osv.osv):
_name = "delivery.carrier"
_description = "Carrier"
for carrier in self.browse(cr, uid, ids, context=context):
order_id=context.get('order_id',False)
price=False
+ available = False
if order_id:
order = sale_obj.browse(cr, uid, order_id, context=context)
carrier_grid=self.grid_get(cr,uid,[carrier.id],order.partner_shipping_id.id,context)
if carrier_grid:
- price=grid_obj.get_price(cr, uid, carrier_grid, order, time.strftime('%Y-%m-%d'), context)
+ try:
+ price=grid_obj.get_price(cr, uid, carrier_grid, order, time.strftime('%Y-%m-%d'), context)
+ available = True
+ except osv.except_osv, e:
+ # no suitable delivery method found, probably configuration error
+ _logger.error("Carrier %s: %s\n%s" % (carrier.name, e.name, e.value))
+ price = 0.0
else:
price = 0.0
- res[carrier.id]=price
+ res[carrier.id] = {
+ 'price': price,
+ 'available': available
+ }
return res
_columns = {
'partner_id': fields.many2one('res.partner', 'Transport Company', required=True, help="The partner that is doing the delivery service."),
'product_id': fields.many2one('product.product', 'Delivery Product', required=True),
'grids_id': fields.one2many('delivery.grid', 'carrier_id', 'Delivery Grids'),
- 'price' : fields.function(get_price, string='Price'),
+ 'available' : fields.function(get_price, string='Available',type='boolean', multi='price',
+ help="Is the carrier method possible with the current order."),
+ 'price' : fields.function(get_price, string='Price', multi='price'),
'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the delivery carrier without removing it."),
'normal_price': fields.float('Normal Price', help="Keep empty if the pricing depends on the advanced pricing per destination"),
'free_if_more_than': fields.boolean('Free If Order Total Amount Is More Than', help="If the order is more expensive than a certain amount, the customer can benefit from a free shipping"),
for reg in self.registration_ids
)
- @api.one
+ @api.multi
@api.depends('name', 'date_begin', 'date_end')
- def _compute_display_name(self):
- dates = [dt.split(' ')[0] for dt in [self.date_begin, self.date_end] if dt]
- dates = sorted(set(dates))
- self.display_name = '%s (%s)' % (self.name, ' - '.join(dates))
+ def name_get(self):
+ result = []
+ for event in self:
+ dates = [dt.split(' ')[0] for dt in [event.date_begin, event.date_end] if dt]
+ dates = sorted(set(dates))
+ result.append((event.id, '%s (%s)' % (event.name, ' - '.join(dates))))
+ return result
@api.one
@api.constrains('seats_max', 'seats_available')
cal_client_id = params.get_param(cr, uid, 'google_calendar_client_id',default='',context=context)
cal_client_secret = params.get_param(cr, uid, 'google_calendar_client_secret',default='',context=context)
- server_uri= "%s/google_account/authentication" % params.get_param(cr, uid, 'web.base.url',default="http://yourcompany.my.openerp.com",context=context)
+ server_uri= "%s/google_account/authentication" % params.get_param(cr, uid, 'web.base.url',default="http://yourcompany.odoo.com",context=context)
return dict(cal_client_id=cal_client_id,cal_client_secret=cal_client_secret,server_uri=server_uri)
local_context: context
}).done(function(o) {
if (o.status === "need_auth") {
- alert(_t("You will be redirected on gmail to authorize your Odoo to access your calendar !"));
+ alert(_t("You will be redirected to Google to authorize access to your calendar!"));
instance.web.redirect(o.url);
}
else if (o.status === "need_config_from_admin"){
if (!_.isUndefined(o.action) && parseInt(o.action)){
- if (confirm(_t("An admin need to configure Google Synchronization before to use it, do you want to configure it now ? !"))){
+ if (confirm(_t("The Google Synchronization needs to be configured before you can use it, do you want to do it now?"))) {
self.do_action(o.action);
}
}
else{
- alert(_t("An admin need to configure Google Synchronization before to use it !"));
+ alert(_t("An administrator needs to configure Google Synchronization before you can use it!"));
}
}
else if (o.status === "need_refresh"){
self.$calendar.fullCalendar('refetchEvents');
}
else if (o.status === "need_reset"){
- if (confirm(_t("The account that you are trying to synchronize (" + o.info.new_name + "), is not the same that the last one used \
-(" + o.info.old_name + "! )" + "\r\n\r\nDo you want remove all references from the old account ?"))){
-
+ var confirm_text1 = _t("The account you are trying to synchronize (%s) is not the same as the last one used (%s)!");
+ var confirm_text2 = _t("In order to do this, you first need to disconnect all existing events from the old account.");
+ var confirm_text3 = _t("Do you want to do this now?");
+ var text = _.str.sprintf(confirm_text1 + "\n" + confirm_text2 + "\n\n" + confirm_text3, o.info.new_name, o.info.old_name);
+ if (confirm(text)) {
self.rpc('/google_calendar/remove_references', {
model:res.model,
local_context:context
}).done(function(o) {
if (o.status === "OK") {
- alert(_t("All old references have been deleted. You can now restart the synchronization"));
+ alert(_t("All events have been disconnected from your previous account. You can now restart the synchronization"));
}
else if (o.status === "KO") {
- alert(_t("An error has occured when we was removing all old references. Please retry or contact your administrator."));
+ alert(_t("An error occured while disconnecting events from your previous account. Please retry or contact your administrator."));
}
//else NOP
});
+from openerp import api
from openerp.osv import fields, osv
-from openerp.tools.translate import _
class res_users(osv.Model):
thread_id = thread_id[0]
return self.pool.get('hr.employee').search(cr, uid, [('user_id', '=', thread_id)], context=context)
+ @api.cr_uid_ids_context
def message_post(self, cr, uid, thread_id, context=None, **kwargs):
""" Redirect the posting of message on res.users to the related employee.
This is done because when giving the context of Chatter on the
('employee_id', '=', obj.employee_id.id),
('name', '<', obj.name), ('action', '=', 'sign_in')
], limit=1, order='name DESC')
- last_signin = self.browse(cr, uid, last_signin_id, context=context)[0]
-
- # Compute time elapsed between sign-in and sign-out
- last_signin_datetime = datetime.strptime(last_signin.name, '%Y-%m-%d %H:%M:%S')
- signout_datetime = datetime.strptime(obj.name, '%Y-%m-%d %H:%M:%S')
- workedhours_datetime = (signout_datetime - last_signin_datetime)
- res[obj.id] = ((workedhours_datetime.seconds) / 60) / 60
+ if last_signin_id:
+ last_signin = self.browse(cr, uid, last_signin_id, context=context)[0]
+
+ # Compute time elapsed between sign-in and sign-out
+ last_signin_datetime = datetime.strptime(last_signin.name, '%Y-%m-%d %H:%M:%S')
+ signout_datetime = datetime.strptime(obj.name, '%Y-%m-%d %H:%M:%S')
+ workedhours_datetime = (signout_datetime - last_signin_datetime)
+ res[obj.id] = ((workedhours_datetime.seconds) / 60) / 60
+ else:
+ res[obj.id] = False
return res
_columns = {
#OR if it starts between the given dates
clause_2 = ['&',('date_start', '<=', date_to),('date_start','>=', date_from)]
#OR if it starts before the date_from and finish after the date_end (or never finish)
- clause_3 = [('date_start','<=', date_from),'|',('date_end', '=', False),('date_end','>=', date_to)]
+ clause_3 = ['&',('date_start','<=', date_from),'|',('date_end', '=', False),('date_end','>=', date_to)]
clause_final = [('employee_id', '=', employee.id),'|','|'] + clause_1 + clause_2 + clause_3
contract_ids = contract_obj.search(cr, uid, clause_final, context=context)
return contract_ids
_columns = {
'alias_name': fields.char('Alias Name',
- help="The name of the email alias, e.g. 'jobs' if you want to catch emails for <jobs@example.my.openerp.com>",),
+ help="The name of the email alias, e.g. 'jobs' if you want to catch emails for <jobs@example.odoo.com>",),
'alias_model_id': fields.many2one('ir.model', 'Aliased Model', required=True, ondelete="cascade",
help="The model (Odoo Document Kind) to which this alias "
"corresponds. Any incoming email that does not reply to an "
def name_get(self, cr, uid, ids, context=None):
"""Return the mail alias display alias_name, including the implicit
mail catchall domain if exists from config otherwise "New Alias".
- e.g. `jobs@openerp.my.openerp.com` or `jobs` or 'New Alias'
+ e.g. `jobs@mail.odoo.com` or `jobs` or 'New Alias'
"""
res = []
for record in self.browse(cr, uid, ids, context=context):
company = "<a style='color:inherit' href='%s'>%s</a>" % (website_url, user.company_id.name)
else:
company = user.company_id.name
- sent_by = _('Sent from %(company)s using %(openerp)s')
+ sent_by = _('Sent by %(company)s using %(odoo)s.')
+
signature_company = '<small>%s</small>' % (sent_by % {
'company': company,
- 'openerp': "<a style='color:inherit' href='https://www.odoo.com/'>Odoo</a>"
+ 'odoo': "<a style='color:inherit' href='https://www.odoo.com/'>Odoo</a>"
})
footer = tools.append_content_to_html(footer, signature_company, plaintext=False, container_tag='div')
# compute email body (signature, company data)
body_html = message.body
- user_id = message.author_id and message.author_id.user_ids and message.author_id.user_ids[0] and message.author_id.user_ids[0].id or None
- if user_signature:
+ # add user signature except for mail groups, where users are usually adding their own signatures already
+ if user_signature and message.model != 'mail.group':
+ user_id = message.author_id and message.author_id.user_ids and message.author_id.user_ids[0] and message.author_id.user_ids[0].id or None
signature_company = self.get_signature_footer(cr, uid, user_id, res_model=message.model, res_id=message.res_id, context=context)
body_html = tools.append_content_to_html(body_html, signature_company, plaintext=False, container_tag='div')
except Exception:
headers = {}
headers['Precedence'] = 'list'
+ # avoid out-of-office replies from MS Exchange
+ # http://blogs.technet.com/b/exchange/archive/2006/10/06/3395024.aspx
+ headers['X-Auto-Response-Suppress'] = 'OOF'
if group.alias_domain and group.alias_name:
headers['List-Id'] = '%s.%s' % (group.alias_name, group.alias_domain)
headers['List-Post'] = '<mailto:%s@%s>' % (group.alias_name, group.alias_domain)
+ # Avoid users thinking it was a personal message
+ # X-Forge-To: will replace To: after SMTP envelope is determined by ir.mail.server
+ list_to = '"%s" <%s@%s>' % (group.name, group.alias_name, group.alias_domain)
+ headers['X-Forge-To'] = list_to
res['headers'] = '%s' % headers
return res
<xpath expr="//div[@name='email']" position='inside'>
<div>
<label for="alias_domain" class="oe_inline"/>
- <field name="alias_domain" placeholder="mycompany.my.openerp.com" class="oe_inline"/>
+ <field name="alias_domain" placeholder="mycompany.odoo.com" class="oe_inline"/>
</div>
</xpath>
</field>
##############################################################################
from openerp.osv import fields, osv
+from openerp import api
from openerp import SUPERUSER_ID
from openerp.tools.translate import _
import openerp
thread_id = thread_id[0]
return self.browse(cr, SUPERUSER_ID, thread_id).partner_id.id
+ @api.cr_uid_ids_context
def message_post(self, cr, uid, thread_id, context=None, **kwargs):
""" Redirect the posting of message on res.users to the related partner.
This is done because when giving the context of Chatter on the
'message_post: notification email subject incorrect')
self.assertIn(_body1, sent_email['body'],
'message_post: notification email body incorrect')
- self.assertIn(user_raoul.signature, sent_email['body'],
- 'message_post: notification email body should contain the sender signature')
self.assertIn('Pigs rules', sent_email['body_alternative'],
'message_post: notification email body alternative should contain the body')
self.assertNotIn('<p>', sent_email['body_alternative'],
'message_post: notification email body alternative still contains html')
- self.assertIn(html2plaintext(user_raoul.signature), sent_email['body_alternative'],
- 'message_post: notification email body alternative should contain the sender signature')
self.assertFalse(sent_email['references'],
'message_post: references should be False when sending a message that is not a reply')
'message_post: notification email subject incorrect')
self.assertIn(html_sanitize(_body2), sent_email['body'],
'message_post: notification email does not contain the body')
- self.assertIn(user_raoul.signature, sent_email['body'],
- 'message_post: notification email body should contain the sender signature')
self.assertIn('Pigs rocks', sent_email['body_alternative'],
'message_post: notification email body alternative should contain the body')
self.assertNotIn('<p>', sent_email['body_alternative'],
'message_post: notification email body alternative still contains html')
- self.assertIn(html2plaintext(user_raoul.signature), sent_email['body_alternative'],
- 'message_post: notification email body alternative should contain the sender signature')
self.assertIn(msg_message_id, sent_email['references'],
'message_post: notification email references lacks parent message message_id')
# Test: attachments + download
pos_order_obj._create_account_move_line(cr, uid, order_ids, session, move_id, context=local_context)
for order in session.order_ids:
+ if order.state == 'done':
+ continue
if order.state not in ('paid', 'invoiced'):
raise osv.except_osv(
_('Error!'),
'name': order.name + ': ' + (data.get('payment_name', '') or ''),
'partner_id': order.partner_id and order.partner_id.id or None,
}
+ account_def = property_obj.get(cr, uid, 'property_account_receivable', 'res.partner', context=context)
+ args['account_id'] = (order.partner_id and order.partner_id.property_account_receivable \
+ and order.partner_id.property_account_receivable.id) or (account_def and account_def.id) or False
+
+ if not args['account_id']:
+ if not args['partner_id']:
+ msg = _('There is no receivable account defined to make payment.')
+ else:
+ msg = _('There is no receivable account defined to make payment for the partner: "%s" (id:%d).') % (order.partner_id.name, order.partner_id.id,)
+ raise osv.except_osv(_('Configuration Error!'), msg)
context.pop('pos_session_id', False)
raise osv.except_osv(_('Error!'), _('You have to open at least one cashbox.'))
args.update({
- 'statement_id' : statement_id,
- 'pos_statement_id' : order_id,
- 'journal_id' : journal_id,
- 'ref' : order.session_id.name,
+ 'statement_id': statement_id,
+ 'pos_statement_id': order_id,
+ 'journal_id': journal_id,
+ 'ref': order.session_id.name,
})
statement_line_obj.create(cr, uid, args, context=context)
#
##############################################################################
+import logging
import threading
+from openerp import tools
from openerp.osv import osv
from openerp.api import Environment
+_logger = logging.getLogger(__name__)
+
class procurement_compute_all(osv.osv_memory):
_name = 'procurement.order.compute.all'
_description = 'Compute all schedulers'
with Environment.manage():
proc_obj = self.pool.get('procurement.order')
#As this function is in a new thread, i need to open a new cursor, because the old one may be closed
-
+
new_cr = self.pool.cursor()
+ # Avoid to run the scheduler multiple times in the same time
+ try:
+ with tools.mute_logger('openerp.sql_db'):
+ new_cr.execute("SELECT id FROM ir_cron WHERE id = %s FOR UPDATE NOWAIT", (scheduler_cron_id,))
+ except Exception:
+ _logger.info('Attempt to run procurement scheduler aborted, as already running')
+ new_cr.rollback()
+ new_cr.close()
+ return {}
user = self.pool.get('res.users').browse(new_cr, uid, uid, context=context)
comps = [x.id for x in user.company_ids]
for comp in comps:
class product_attribute_line(osv.osv):
_name = "product.attribute.line"
+ _rec_name = 'attribute_id'
_columns = {
'product_tmpl_id': fields.many2one('product.template', 'Product Template', required=True, ondelete='cascade'),
'attribute_id': fields.many2one('product.attribute', 'Attribute', required=True, ondelete='restrict'),
<div class="oe_right">
<button class="oe_inline oe_stat_button" string="Variant Prices" name="%(variants_template_action)d" type="action" icon="fa-strikethrough"/>
<button class="oe_inline oe_stat_button" name="%(product.product_variant_action)d" type="action" icon="fa-sitemap">
- <field string="Variants" name="product_variant_count" widget="statinfo" />
+ <field string="List of Variants" name="product_variant_count" widget="statinfo" />
</button>
</div>
<field name="attribute_line_ids" widget="one2many_list">
<field name="name"/>
</a>
</h4>
- <a name="%(product.product_variant_action)d" type="action" t-if="record.product_variant_count.raw_value>1" >
+ <a name="%(product.product_variant_action)d" type="action">
<t t-esc="record.product_variant_count.value"/> Variants
</a>
<div name="tags"/>
</h1>
<group>
<group>
- <field name="project_id" domain="[('state', '!=', 'close')]" on_change="onchange_project(project_id)" context="{'default_use_tasks':1}"/>
+ <field name="project_id" domain="[('state','not in', ('close', 'cancelled'))]" on_change="onchange_project(project_id)" context="{'default_use_tasks':1}"/>
<field name="user_id"
options='{"no_open": True}'
context="{'default_groups_ref': ['base.group_user', 'base.group_partner_manager', 'project.group_project_user']}"/>
from datetime import datetime
+from openerp import api
from openerp import SUPERUSER_ID
from openerp import tools
from openerp.osv import fields, osv, orm
res_id = super(project_issue, self).message_new(cr, uid, msg, custom_values=defaults, context=context)
return res_id
+ @api.cr_uid_ids_context
def message_post(self, cr, uid, thread_id, body='', subject=None, type='notification', subtype=None, parent_id=False, attachments=None, context=None, content_subtype='html', **kwargs):
""" Overrides mail_thread message_post so that we can set the date of last action field when
a new message is posted on the issue.
def action_picking_create(self, cr, uid, ids, context=None):
for order in self.browse(cr, uid, ids):
- picking_id = self.pool.get('stock.picking').create(cr, uid, {'picking_type_id': order.picking_type_id.id, 'partner_id': order.dest_address_id.id or order.partner_id.id}, context=context)
+ picking_vals = {
+ 'picking_type_id': order.picking_type_id.id,
+ 'partner_id': order.dest_address_id.id or order.partner_id.id,
+ 'date': max([l.date_planned for l in order.order_line])
+ }
+ picking_id = self.pool.get('stock.picking').create(cr, uid, picking_vals, context=context)
self._create_stock_moves(cr, uid, order, order.order_line, picking_id, context=context)
def picking_done(self, cr, uid, ids, context=None):
<field name="help">This report performs analysis on your quotations and sales orders. Analysis check your sales revenues and sort it by different group criteria (salesman, partner, product, etc.) Use this report to perform analysis on sales not having invoiced yet. If you want to analyse your turnover, you should use the Invoice Analysis report in the Accounting application.</field>
</record>
- <menuitem id="base.next_id_64" name="Sales" parent="base.menu_reporting" sequence="1" groups="base.group_sale_manager"/>
<menuitem action="action_order_report_all" id="menu_report_product_all" parent="base.next_id_64" sequence="10"/>
</data>
compose_form_id = ir_model_data.get_object_reference(cr, uid, 'mail', 'email_compose_message_wizard_form')[1]
except ValueError:
compose_form_id = False
- ctx = dict(context)
+ ctx = dict()
ctx.update({
'default_model': 'sale.order',
'default_res_id': ids[0],
help="Target of invoice revenue for the current month. This is the amount the sales \n"
"team estimates to be able to invoice this month."),
'monthly_quoted': fields.function(_get_sale_orders_data,
- type='any', readonly=True, multi='_get_sale_orders_data',
+ type='char', readonly=True, multi='_get_sale_orders_data',
string='Rate of created quotation per duration'),
'monthly_confirmed': fields.function(_get_sale_orders_data,
- type='any', readonly=True, multi='_get_sale_orders_data',
+ type='char', readonly=True, multi='_get_sale_orders_data',
string='Rate of validate sales orders per duration'),
'monthly_invoiced': fields.function(_get_invoices_data,
- type='any', readonly=True,
+ type='char', readonly=True,
string='Rate of sent invoices per duration'),
}
Use Some Order Lines to invoice a selection of the sales order lines."""),
'qtty': fields.float('Quantity', digits=(16, 2), required=True),
'product_id': fields.many2one('product.product', 'Advance Product',
+ domain=[('type', '=', 'service')],
help="""Select a product of type service which is called 'Advance Product'.
You may have to create it and set it as a default value on this field."""),
'amount': fields.float('Advance Amount', digits_compute= dp.get_precision('Account'),
<field name="model">product.template</field>
<field name="inherit_id" ref="product.product_template_form_view"/>
<field name="arch" type="xml">
- <group name="procurement_uom" position="after">
+ <group name="procurement" position="after">
<group string="Project Management Information" attrs="{'invisible': [('type', '!=', 'service')]}">
<field name="auto_create_task"/>
<field name="project_id" attrs="{'invisible':['|', ('type','!=','service'), ('auto_create_task', '=', False)]}"/>
<xpath expr="//button[@name='action_view_invoice']" position="after">
<field name="picking_ids" invisible="1"/>
<button name="action_view_delivery" string="View Delivery Order" type="object" class="oe_highlight"
- attrs="{'invisible': ['|','|','|',('picking_ids','=',False),('picking_ids','=',[]), ('state', 'not in', ('progress','manual', 'done')),('shipped','=',True)]}" groups="base.group_user"/>
+ attrs="{'invisible': ['|',('picking_ids','=',False),('picking_ids','=',[])]}" groups="base.group_user"/>
</xpath>
<xpath expr="//button[@name='action_cancel']" position="after">
<button name="ship_cancel" states="shipping_except" string="Cancel Order"/>
</xpath>
</template>
+ <menuitem id="base.next_id_64" name="Sales" parent="base.menu_reporting" sequence="1" groups="base.group_sale_manager"/>
+
</data>
</openerp>
def _get_users_from_group(self, cr, uid, ids, context=None):
result = set()
- for group in self.pool['res.groups'].browse(cr, uid, ids, context=context):
+ groups = self.pool['res.groups'].browse(cr, uid, ids, context=context)
+ # Clear cache to avoid perf degradation on databases with thousands of users
+ groups.invalidate_cache()
+ for group in groups:
result.update(user.id for user in group.users)
return list(result)
res = dict.fromkeys(ids, False)
for move in self.browse(cr, uid, ids, context=context):
if move.state == 'done':
- res[move.id] = [q.id for q in move.quant_ids]
+ res[move.id] = [q.lot_id.id for q in move.quant_ids if q.lot_id]
else:
- res[move.id] = [q.id for q in move.reserved_quant_ids]
+ res[move.id] = [q.lot_id.id for q in move.reserved_quant_ids if q.lot_id]
return res
def _get_product_availability(self, cr, uid, ids, field_name, args, context=None):
'propagate': fields.boolean('Propagate cancel and split', help='If checked, when this move is cancelled, cancel the linked move too'),
'picking_type_id': fields.many2one('stock.picking.type', 'Picking Type'),
'inventory_id': fields.many2one('stock.inventory', 'Inventory'),
- 'lot_ids': fields.function(_get_lot_ids, type='many2many', relation='stock.quant', string='Lots'),
+ 'lot_ids': fields.function(_get_lot_ids, type='many2many', relation='stock.production.lot', string='Lots'),
'origin_returned_move_id': fields.many2one('stock.move', 'Origin return move', help='move that created the return move', copy=False),
'returned_move_ids': fields.one2many('stock.move', 'origin_returned_move_id', 'All returned moves', help='Optional: all returned moves created from this move'),
'reserved_availability': fields.function(_get_reserved_availability, type='float', string='Quantity Reserved', readonly=True, help='Quantity that has already been reserved for this move'),
# Statistics for the kanban view
'last_done_picking': fields.function(_get_tristate_values,
- type='any',
+ type='char',
string='Last 10 Done Pickings'),
'count_picking_draft': fields.function(_get_picking_count,
}
// add a tooltip to cropped menu items
this.$secondary_menus.find('.oe_secondary_submenu li a span').each(function() {
- $(this).tooltip(this.scrollWidth > this.clientWidth ? {title: $(this).text().trim(), placement: 'auto right'} :'destroy');
+ $(this).tooltip(this.scrollWidth > this.clientWidth ? {title: $(this).text().trim(), placement: 'right'} :'destroy');
});
},
/**
start: function() {
var self = this;
+ if (this.searchview.headless) return $.when(this._super(), this.searchview.ready);
var filters_ready = this.searchview.fields_view_get
.then(this.proxy('prepare_filters'));
return $.when(this._super(), filters_ready).then(function () {
}
var title = this.$node.html() || this.field.string;
// current gauge value
- var val = this.field.value;
+ var val = this.field.raw_value;
if (_.isArray(JSON.parse(val))) {
val = JSON.parse(val);
}
class test_converter(orm.Model):
_name = 'website.converter.test'
+ # disable translation export for those brilliant field labels and values
+ _translate = False
+
_columns = {
'char': fields.char(),
'integer': fields.integer(),
from sys import maxint
import werkzeug
-import werkzeug.exceptions
-import werkzeug.utils
-import werkzeug.wrappers
# optional python-slugify import (https://github.com/un33k/python-slugify)
try:
import slugify as slugify_lib
_description = "Website Menu"
_columns = {
'name': fields.char('Menu', required=True, translate=True),
- 'url': fields.char('Url', translate=True),
+ 'url': fields.char('Url'),
'new_window': fields.boolean('New Window'),
'sequence': fields.integer('Sequence'),
# TODO: support multiwebsite once done for ir.ui.views
}
.oe_overlay .oe_overlay_options > .btn-group {
left: -50%;
+ white-space: nowrap;
+}
+.oe_overlay .oe_overlay_options > .btn-group > a {
+ cursor: pointer;
+ display: inline-block;
+ float: none;
+ margin: 0 -3px;
}
.oe_overlay .oe_overlay_options .btn, .oe_overlay .oe_overlay_options a {
cursor: pointer;
z-index: 1002
> .btn-group
left: -50%
+ white-space: nowrap
+ > a
+ cursor: pointer
+ display: inline-block
+ float: none
+ margin: 0 -3px
.btn, a
cursor: pointer
.dropdown
class="form-control url pull-left"
style="width: 400px;"
id="urlvideo"
- placeholder="//www.youtube.com/embed/yws1tbgNV7k"/>
+ placeholder="//www.youtube.com/embed/yws1tbgNV7k"
+ t-translation="off"/>
<button class="btn btn-default">Preview</button>
</div>
<div class="form-group btn-group">
class="form-control url pull-left"
style="width: 400px;"
id="embedvideo"
- placeholder='<iframe src="//www.youtube.com/embed/yws1tbgNV7k"></iframe>'/>
+ placeholder='<iframe src="//www.youtube.com/embed/yws1tbgNV7k"></iframe>'
+ t-translation="off"/>
<button class="btn btn-default">Preview</button>
</div>
</div>
A great way to catch your reader's attention is to tell a story.
Everything you consider writing can be told as a story.
</p><p>
- <b>Great stories have personality</b>. Consider telling
+ <b>Great stories have personality.</b> Consider telling
a great story that provides personality. Writing a story
with personality for potential clients will asist with
making a relationship connection. This shows up in small
of view, not from someone else's experience.
</p><p>
<b>Great stories are for everyone even when only written for
- just one person</b>. If you try to write with a wide general
+ just one person.</b> 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>
<template id="footer_custom" inherit_id="website.layout" name="Footer">
<xpath expr="//div[@id='footer_container']" position="replace">
- <div class="oe_structure">
+ <div class="oe_structure" id="footer">
<section data-snippet-id='three-columns' class="mt16 mb16">
<div class="container">
<div class="row">
</template>
<template id="footer_default" inherit_id="website.footer_custom" optional="enabled" name="Automatic Footer">
- <xpath expr="//div[@class='oe_structure']" position="replace">
+ <xpath expr="//div[@id='footer']" position="replace">
<div class="container hidden-print">
<div class="row">
<div class="col-md-3">
<div name="social_media">
<separator string="Social Media"/>
<group name="social_media">
- <field name="social_twitter" placeholder="https://twitter.com/openerp"/>
- <field name="social_facebook" placeholder="https://facebook.com/openerp"/>
- <field name="social_googleplus" placeholder="https://plus.google.com/+openerp"/>
- <field name="social_linkedin" placeholder="http://www.linkedin.com/company/openerp"/>
- <field name="social_youtube" placeholder="http://www.youtube.com/channel/HCU842OHPPNrQ"/>
+ <field name="social_twitter" placeholder="https://twitter.com/odooapps"/>
+ <field name="social_facebook" placeholder="https://facebook.com/odoo"/>
+ <field name="social_googleplus" placeholder="https://plus.google.com/+Odooapps"/>
+ <field name="social_linkedin" placeholder="http://www.linkedin.com/company/odoo"/>
+ <field name="social_youtube" placeholder="https://www.youtube.com/channel/UCkQPikELWZFLgQNHd73jkdg"/>
<field name="social_github" placeholder="https://youraccount.github.io"/>
</group>
</div>
cr, uid, context = request.cr, request.uid, request.context
blog_post = request.registry['blog.post']
partner_obj = request.registry['res.partner']
- thread_obj = request.registry['mail.thread']
if uid != request.website.user_id.id:
partner_ids = [user.partner_id.id]
return ids
return self._get_discussion_detail(ids, publish, **post)
+ @http.route('/blogpost/get_discussions/', type='json', auth="public", website=True)
+ def discussions(self, post_id=0, paths=None, count=False, **post):
+ ret = []
+ for path in paths:
+ result = self.discussion(post_id=post_id, path=path, count=count, **post)
+ ret.append({"path": path, "val": result})
+ return ret
+
@http.route('/blogpost/change_background', type='json', auth="public", website=True)
def change_bg(self, post_id=0, image=None, **post):
if not post_id:
$('<div id="discussions_wrapper"></div>').insertAfter($('#blog_content'));
}
// Attach a discussion to each paragraph.
- $(self.settings.content).each(function(i) {
- self.discussion_handler(i, $(this));
- });
+ self.discussions_handler(self.settings.content);
+
// Hide the discussion.
$('html').click(function(event) {
if($(event.target).parents('#discussions_wrapper, .main-discussion-link-wrp').length === 0) {
'count': comment_count, //if true only get length of total comment, display on discussion thread.
})
},
- discussion_handler : function(i, node) {
+ prepare_multi_data : function(identifiers, comment_count) {
var self = this;
- var identifier = node.attr('data-chatter-id');
- if (identifier) {
- self.prepare_data(identifier, true).then( function (data) {
- self.prepare_discuss_link(data, identifier, node);
+ return openerp.jsonRpc("/blogpost/get_discussions/", 'call', {
+ 'post_id': self.settings.post_id,
+ 'paths': identifiers,
+ 'count': comment_count, //if true only get length of total comment, display on discussion thread.
+ })
+ },
+ discussions_handler: function() {
+ var self = this;
+ var node_by_id = {};
+ $(self.settings.content).each(function(i) {
+ var node = $(this);
+ var identifier = node.attr('data-chatter-id');
+ if (identifier) {
+ node_by_id[identifier] = node;
+ }
+ });
+ self.prepare_multi_data(_.keys(node_by_id), true).then( function (multi_data) {
+ _.forEach(multi_data, function(data) {
+ self.prepare_discuss_link(data.val, data.path, node_by_id[data.path]);
});
- }
+ });
},
prepare_discuss_link : function(data, identifier, node) {
var self = this;
values.update(kwargs=kwargs.items())
return request.website.render("website.contactus", values)
+ def create_lead(self, request, values):
+ """ Allow to be overrided """
+ return request.registry['crm.lead'].create(request.cr, SUPERUSER_ID, values, request.context)
+
@http.route(['/crm/contactus'], type='http', auth="public", website=True)
def contactus(self, **kwargs):
def dict_to_str(title, dictvar):
post_description = [] # Info to add after the message
values = {}
- lead_model = request.registry['crm.lead']
-
for field_name, field_value in kwargs.items():
if hasattr(field_value, 'filename'):
post_file.append(field_value)
- elif field_name in lead_model._all_columns and field_name not in _BLACKLIST:
+ elif field_name in request.registry['crm.lead']._all_columns and field_name not in _BLACKLIST:
values[field_name] = field_value
elif field_name not in _TECHNICAL: # allow to add some free fields or blacklisted field like ID
post_description.append("%s: %s" % (field_name, field_value))
post_description.append("%s: %s" % ("REFERER", environ.get("HTTP_REFERER")))
values['description'] += dict_to_str(_("Environ Fields: "), post_description)
- lead_id = lead_model.create(request.cr, SUPERUSER_ID, dict(values, user_id=False), request.context)
+ lead_id = self.create_lead(request, dict(values, user_id=False))
if lead_id:
for field_value in post_file:
attachment_value = {
forum_id = request.registry['forum.forum'].create(request.cr, request.uid, {
'name': forum_name,
}, context=request.context)
- return request.redirect("/forum/%s" % slug(forum_id))
+ return request.redirect("/forum/%s" % forum_id)
@http.route('/forum/notification_read', type='json', auth="user", methods=['POST'], website=True)
def notification_read(self, **kwargs):
'website': kwargs.get('website'),
'email': kwargs.get('email'),
'city': kwargs.get('city'),
- 'country_id': int(kwargs.get('country')),
+ 'country_id': int(kwargs.get('country')) if kwargs.get('country') else False,
'website_description': kwargs.get('description'),
}, context=request.context)
return werkzeug.utils.redirect("/forum/%s/user/%d" % (slug(forum), user.id))
raise KarmaError('Not enough karma to downvote.')
Vote = self.pool['forum.post.vote']
- vote_ids = Vote.search(cr, uid, [('post_id', 'in', ids), ('user_id', '=', uid)], limit=1, context=context)
- new_vote = 0
+ vote_ids = Vote.search(cr, uid, [('post_id', 'in', ids), ('user_id', '=', uid)], context=context)
+ new_vote = '1' if upvote else '-1'
+ voted_forum_ids = set()
if vote_ids:
for vote in Vote.browse(cr, uid, vote_ids, context=context):
if upvote:
else:
new_vote = '0' if vote.vote == '1' else '-1'
Vote.write(cr, uid, vote_ids, {'vote': new_vote}, context=context)
- else:
+ voted_forum_ids.add(vote.post_id.id)
+ for post_id in set(ids) - voted_forum_ids:
for post_id in ids:
- new_vote = '1' if upvote else '-1'
Vote.create(cr, uid, {'post_id': post_id, 'vote': new_vote}, context=context)
return {'vote_count': self._get_vote_count(cr, uid, ids, None, None, context=context)[ids[0]], 'user_vote': new_vote}
res.append((record['id'], name))
return res
+ # TODO master remove me
def _name_get_fnc(self, cr, uid, ids, prop, unknow_none, context=None):
res = self.name_get(cr, uid, ids, context=context)
return dict(res)
_columns = {
'sequence': fields.integer('Sequence'),
- 'display_name': fields.function(_name_get_fnc, type="char", string='Full Name'),
'name': fields.char('Name', required=True, translate=True),
'introduction': fields.html('Introduction', translate=True),
'parent_id': fields.many2one('forum.documentation.toc', 'Parent Table Of Content', ondelete='cascade'),
}
this.$target.removeClass('has-error');
- openerp.jsonRpc('/website_mail/follow', 'call', {
- 'id': +this.$target.data('id'),
- 'object': this.$target.data('object'),
- 'message_is_follower': this.$target.attr("data-follow") || "off",
- 'email': $email.length ? $email.val() : false,
- }).then(function (follow) {
- self.toggle_subscription(follow, self.email);
- });
+ var email = $email.length ? $email.val() : false;
+ if (email) {
+ openerp.jsonRpc('/website_mail/follow', 'call', {
+ 'id': +this.$target.data('id'),
+ 'object': this.$target.data('object'),
+ 'message_is_follower': this.$target.attr("data-follow") || "off",
+ 'email': email,
+ }).then(function (follow) {
+ self.toggle_subscription(follow, email);
+ });
+ }
},
toggle_subscription: function(follow, email) {
console.log(follow, email);
+ follow = follow || (!email && this.$target.attr('data-unsubscribe'));
if (follow) {
this.$target.find(".js_follow_btn").addClass("hidden");
this.$target.find(".js_unfollow_btn").removeClass("hidden");
this.$target.find(".js_unfollow_btn").addClass("hidden");
}
this.$target.find('input.js_follow_email')
- .val(email ? email : "")
- .attr("disabled", follow || (email.length && this.is_user) ? "disabled" : false);
+ .val(email || "")
+ .attr("disabled", email && (follow || this.is_user) ? "disabled" : false);
this.$target.attr("data-follow", follow ? 'on' : 'off');
},
});
Everything you consider writing can be told as a story.
</p>
<p style="overflow:hidden">
- <strong>Great stories have personality</strong>. Consider telling
+ <strong>Great stories have personality.</strong> Consider telling
a great story that provides personality. Writing a story
with personality for potential clients will asist with
making a relationship connection. This shows up in small
</p>
<p style="overflow:hidden">
<strong>Great stories are for everyone even when only written for
- just one person</strong>. If you try to write with a wide general
+ just one person.</strong> 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.
<template id="follow">
<div class="input-group js_follow" t-att-data-id="object.id"
t-att-data-object="object._name"
- t-att-data-follow="object.id and object.message_is_follower and 'on' or 'off'">
+ t-att-data-follow="object.id and object.message_is_follower and 'on' or 'off'"
+ t-att-data-unsubscribe="'unsubscribe' if 'unsubscribe' in request.params else None">
<input
type="email" name="email"
class="js_follow_email form-control"
# -*- coding: utf-8 -*-
from openerp.osv import osv
+from openerp import tools
+from openerp.tools.translate import _
from openerp.tools.safe_eval import safe_eval as eval
-
+from openerp.addons.website.models.website import slug
class MailGroup(osv.Model):
_inherit = 'mail.group'
except Exception:
headers = {}
headers.update({
- 'List-Archive': '<%s/groups/%s>' % (base_url, group.id),
+ 'List-Archive': '<%s/groups/%s>' % (base_url, slug(group)),
'List-Subscribe': '<%s/groups>' % (base_url),
- 'List-Unsubscribe': '<%s/groups>' % (base_url),
+ 'List-Unsubscribe': '<%s/groups?unsubscribe>' % (base_url,),
})
res['headers'] = '%s' % headers
return res
+
+
+class MailMail(osv.Model):
+ _inherit = 'mail.mail'
+
+ def send_get_mail_body(self, cr, uid, mail, partner=None, context=None):
+ """ Short-circuit parent method for mail groups, replace the default
+ footer with one appropriate for mailing-lists."""
+
+ if mail.model == 'mail.group' and mail.res_id:
+ # no super() call on purpose, no private links that could be quoted!
+ group = self.pool['mail.group'].browse(cr, uid, mail.res_id, context=context)
+ base_url = self.pool['ir.config_parameter'].get_param(cr, uid, 'web.base.url')
+ vals = {
+ 'maillist': _('Mailing-List'),
+ 'post_to': _('Post to'),
+ 'unsub': _('Unsubscribe'),
+ 'mailto': 'mailto:%s@%s' % (group.alias_name, group.alias_domain),
+ 'group_url': '%s/groups/%s' % (base_url, slug(group)),
+ 'unsub_url': '%s/groups?unsubscribe' % (base_url,),
+ }
+ footer = """_______________________________________________
+ %(maillist)s: %(group_url)s
+ %(post_to)s: %(mailto)s
+ %(unsub)s: %(unsub_url)s
+ """ % vals
+ body = tools.append_content_to_html(mail.body, footer, container_tag='div')
+ return body
+ else:
+ return super(MailMail, self).send_get_mail_body(cr, uid, mail,
+ partner=partner,
+ context=context)
</section>
</div>
<div class="container mt32">
+ <div t-if="'unsubscribe' in request.params" class="col-md-offset-9 col-md-3 alert alert-info">
+ <h3>Need to unsubscribe? It's right here! <span class="fa fa-2x fa-arrow-down pull-right"></span></h3>
+ </div>
<div class="row mt8" t-foreach="groups" t-as="group">
<div class="col-md-3">
<img t-att-src="'/website/image?model=mail.group&field=image_small&id='+str(group['id'])" class="pull-left"/>
if not order:
return {
'state': 'error',
- 'message': '<p>There seems to be an error with your request.</p>',
+ 'message': '<p>%s</p>' % _('There seems to be an error with your request.'),
}
tx_ids = request.registry['payment.transaction'].search(
if order.amount_total:
return {
'state': 'error',
- 'message': '<p>There seems to be an error with your request.</p>',
+ 'message': '<p>%s</p>' % _('There seems to be an error with your request.'),
}
else:
state = 'done'
tx = request.registry['payment.transaction'].browse(cr, uid, tx_ids[0], context=context)
state = tx.state
if state == 'done':
- message = '<p>Your payment has been received.</p>'
+ message = '<p>%s</p>' % _('Your payment has been received.')
elif state == 'cancel':
- message = '<p>The payment seems to have been canceled.</p>'
+ message = '<p>%s</p>' % _('The payment seems to have been canceled.')
elif state == 'pending' and tx.acquirer_id.validation == 'manual':
- message = '<p>Your transaction is waiting confirmation.</p>'
+ message = '<p>%s</p>' % _('Your transaction is waiting confirmation.')
if tx.acquirer_id.post_msg:
message += tx.acquirer_id.post_msg
else:
- message = '<p>Your transaction is waiting confirmation.</p>'
+ message = '<p>%s</p>' % _('Your transaction is waiting confirmation.')
validation = tx.acquirer_id.validation
return {
product = product_obj.browse(request.cr, request.uid, id, context=request.context)
return product.write({'website_size_x': x, 'website_size_y': y})
+ def order_lines_2_google_api(self, order_lines):
+ """ Transforms a list of order lines into a dict for google analytics """
+ ret = []
+ for line in order_lines:
+ ret.append({
+ 'id': line.order_id and line.order_id.id,
+ 'name': line.product_id.categ_id and line.product_id.categ_id.name or '-',
+ 'sku': line.product_id.id,
+ 'quantity': line.product_uom_qty,
+ 'price': line.price_unit,
+ })
+ return ret
+
@http.route(['/shop/tracking_last_order'], type='json', auth="public")
def tracking_cart(self, **post):
- """ return JS code for google analytics"""
+ """ return data about order in JSON needed for google analytics"""
cr, uid, context = request.cr, request.uid, request.context
ret = {}
sale_order_id = request.session.get('sale_last_order_id')
'revenue': order.amount_total,
'currency': order.currency_id.name
}
- ret['lines'] = []
- for line in order.order_line:
- if not line.is_delivery:
- ret['lines'].append({
- 'id': line.order_id and line.order_id.id,
- 'name': line.product_id.categ_id and line.product_id.categ_id.name or '-',
- 'sku': line.product_id.id,
- 'quantity': line.product_uom_qty,
- 'price': line.price_unit,
- })
+ ret['lines'] = self.order_lines_2_google_api(order.order_line)
return ret
# vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4:
<select class="form-control" name="attrib">
<option value=""/>
<t t-foreach="a.value_ids" t-as="v">
- <option t-att-value="'%s-%s' % (a.id,v.id)" t-field="v.name" t-att-selected="'selected' if v.id in attrib_set else ''"/>
+ <option t-att-value="'%s-%s' % (a.id,v.id)" t-esc="v.name" t-att-selected="'selected' if v.id in attrib_set else ''"/>
</t>
</select>
</t>
<div class="clearfix"/>
- <div class="form-group col-lg-6" groups="sale.group_delivery_invoice_address">
+ <div class="form-group col-lg-6">
<label>
<input type="checkbox" name="shipping_different" t-att-checked="checkout.get('shipping_different')"/>
<span>Ship to a different address</span>
</label>
</div>
</div>
- <div class="js_shipping row mb16" t-att-style="not checkout.get('shipping_different') and 'display:none' or ''" groups="sale.group_delivery_invoice_address">
+ <div class="js_shipping row mb16" t-att-style="not checkout.get('shipping_different') and 'display:none' or ''">
<h3 class="oe_shipping col-lg-12 mt16">Shipping Information</h3>
<div t-attf-class="form-group #{error.get('shipping_name') and 'has-error' or ''} col-lg-6">
<div>
<a href="/shop/checkout"><span class="fa fa-arrow-right"/> Change Address</a>
</div>
- <t groups="sale.group_delivery_invoice_address">
+ <t>
<h4 class="mt32">Ship To:</h4>
<t t-if="website_sale_order.partner_shipping_id and website_sale_order.partner_shipping_id.id != website_sale_order.partner_invoice_id.id">
<div t-field="order.partner_shipping_id" t-field-options='{
"widget": "contact",
"fields": ["address", "name", "phone", "email"]
}'/>
- <t groups="sale.group_delivery_invoice_address">
+ <t>
<h4 class="mt32">Ship To:</h4>
<t t-if="order.partner_shipping_id and order.partner_shipping_id.id != order.partner_invoice_id.id">
<div t-field="order.partner_shipping_id" t-field-options='{
# -*- coding: utf-8 -*-
import openerp
from openerp import http
-from openerp import SUPERUSER_ID
from openerp.http import request
import openerp.addons.website_sale.controllers.main
+
class website_sale(openerp.addons.website_sale.controllers.main.website_sale):
@http.route(['/shop/payment'], type='http', auth="public", website=True)
res = super(website_sale, self).payment(**post)
return res
+
+ def order_lines_2_google_api(self, order_lines):
+ """ Transforms a list of order lines into a dict for google analytics """
+ order_lines_not_delivery = [line for line in order_lines if not line.is_delivery]
+ return super(website_sale, self).order_lines_2_google_api(order_lines_not_delivery)
<li t-foreach="deliveries" t-as="delivery">
<label>
<input t-att-value="delivery.id" type="radio" name="delivery_type"
- t-att-checked="order.carrier_id and order.carrier_id.id == delivery.id and 'checked' or False"/>
+ t-att-checked="order.carrier_id and order.carrier_id.id == delivery.id and 'checked' or False"
+ t-att-disabled="delivery.available and '0' or '1'"/>
<span t-field="delivery.name"/>
- <span class="badge" t-field="delivery.price"
- t-field-options='{
- "widget": "monetary",
- "display_currency": "website.pricelist_id.currency_id"
- }'/>
+ <t t-if="delivery.available">
+ <span class="badge" t-field="delivery.price"
+ t-field-options='{
+ "widget": "monetary",
+ "display_currency": "website.pricelist_id.currency_id"
+ }'/>
+ </t>
</label>
</li>
</ul>
def _storage(self, cr, uid, context=None):
return self.pool['ir.config_parameter'].get_param(cr, SUPERUSER_ID, 'ir_attachment.location', 'file')
- @tools.ormcache_context()
+ @tools.ormcache(skiparg=3)
def _filestore(self, cr, uid, context=None):
return tools.config.filestore(cr.dbname)
'create_uid': fields.integer('Uid', readonly=True), # Integer not m2o is intentionnal
'name': fields.char('Name', required=True),
'type': fields.selection(EXCEPTIONS_TYPE, string='Type', required=True, select=True),
- 'dbname': fields.char('Database Name'),
- 'level': fields.char('Level'),
+ 'dbname': fields.char('Database Name', select=True),
+ 'level': fields.char('Level', select=True),
'message': fields.text('Message', required=True),
'path': fields.char('Path', required=True),
'func': fields.char('Function', required=True),
smtp_to_list = filter(None, tools.flatten(map(extract_rfc2822_addresses,[email_to, email_cc, email_bcc])))
assert smtp_to_list, "At least one valid recipient address should be specified for outgoing emails (To/Cc/Bcc)"
+ x_forge_to = message['X-Forge-To']
+ if x_forge_to:
+ # `To:` header forged, e.g. for posting on mail.groups, to avoid confusion
+ del message['X-Forge-To']
+ del message['To'] # avoid multiple To: headers!
+ message['To'] = x_forge_to
+
# Do not actually send emails in testing mode!
if getattr(threading.currentThread(), 'testing', False):
_test_logger.info("skip sending email in test mode")
('obj_name_uniq', 'unique (model)', 'Each model must be unique!'),
]
- def _search_display_name(self, operator, value):
- # overridden to allow searching both on model name (model field) and
- # model description (name field)
- return ['|', ('model', operator, value), ('name', operator, value)]
+ # overridden to allow searching both on model name (model field)
+ # and model description (name field)
+ def _name_search(self, cr, uid, name='', args=None, operator='ilike', context=None, limit=100, name_get_uid=None):
+ if args is None:
+ args = []
+ domain = args + ['|', ('model', operator, name), ('name', operator, name)]
+ return self.name_get(cr, name_get_uid or uid,
+ super(ir_model, self).search(cr, uid, domain, limit=limit, context=context),
+ context=context)
def _drop_table(self, cr, uid, ids, context=None):
for model in self.browse(cr, uid, ids, context):
"""
_name = 'ir.model.data'
_order = 'module,model,name'
- def _display_name_get(self, cr, uid, ids, prop, unknow_none, context=None):
+ def name_get(self, cr, uid, ids, context=None):
result = {}
- result2 = {}
+ result2 = []
for res in self.browse(cr, uid, ids, context=context):
if res.id:
result.setdefault(res.model, {})
result[res.model][res.res_id] = res.id
- result2[res.id] = False
for model in result:
try:
r = dict(self.pool[model].name_get(cr, uid, result[model].keys(), context=context))
for key,val in result[model].items():
- result2[val] = r.get(key, False)
+ result2.append((val, r.get(key, False)))
except:
# some object have no valid name_get implemented, we accept this
pass
help="External Key/Identifier that can be used for "
"data integration with third-party systems"),
'complete_name': fields.function(_complete_name_get, type='char', string='Complete ID'),
- 'display_name': fields.function(_display_name_get, type='char', string='Record Name'),
'model': fields.char('Model Name', required=True, select=1),
'module': fields.char('Module', required=True, select=1),
'res_id': fields.integer('Record ID', select=1,
result = self.render_element(element, template_attributes, generated_attributes, qwebcontext)
if element.tail:
- result += element.tail
+ result += element.tail.encode('utf-8')
if isinstance(result, unicode):
return result.encode('utf-8')
(SELECT company_id from res_users where id = %%s)
)
%s
- ORDER BY v.user_id, u.company_id"""
+ ORDER BY v.user_id, u.company_id, v.key2"""
params = ('default', model, uid, uid)
if condition:
- query %= 'AND v.key2 = %s'
+ query %= 'AND (v.key2 = %s OR v.key2 IS NULL)'
params += (condition[:200],)
else:
query %= 'AND v.key2 is NULL'
'summary': terp.get('summary', ''),
}
+
+ def create(self, cr, uid, vals, context=None):
+ new_id = super(module, self).create(cr, uid, vals, context=context)
+ module_metadata = {
+ 'name': 'module_%s' % vals['name'],
+ 'model': 'ir.module.module',
+ 'module': 'base',
+ 'res_id': new_id,
+ 'noupdate': True,
+ }
+ self.pool['ir.model.data'].create(cr, uid, module_metadata)
+ return new_id
+
# update the list of available packages
def update_list(self, cr, uid, context=None):
res = [0, 0] # [update, add]
def name_get(self, cr, uid, ids, context=None):
if not len(ids):
return []
- bank_dicts = self.read(cr, uid, ids, context=context)
+ bank_dicts = self.read(cr, uid, ids, self.fields_get_keys(cr, uid, context=context), context=context)
return self._prepare_name_get(cr, uid, bank_dicts, context=context)
def onchange_company_id(self, cr, uid, ids, company_id, context=None):
def _get_company(self,cr, uid, context=None, uid2=False):
if not uid2:
uid2 = uid
- # use read method to compute default values to don't create browse record and fetch all fields
- # browse crash for install or update module
- user = self.pool['res.users'].read(cr, uid, uid2, ['company_id'], context)
- company_id = user['company_id'] and user['company_id'][0] or False
- return company_id
+ # Use read() to compute default company, and pass load=_classic_write to
+ # avoid useless name_get() calls. This will avoid prefetching fields
+ # while computing default values for new db columns, as the
+ # db backend may not be fully initialized yet.
+ user_data = self.pool['res.users'].read(cr, uid, uid2, ['company_id'],
+ context=context, load='_classic_write')
+ comp_id = user_data['company_id']
+ return comp_id or False
def _get_companies(self, cr, uid, context=None):
c = self._get_company(cr, uid, context)
<th nowrap="" valign="BASELINE" align="RIGHT">Répondre
à : </th>
- <td><a class="moz-txt-link-abbreviated" href="mailto:catchall@openerp.my.openerp.com">catchall@openerp.my.openerp.com</a></td>
+ <td><a class="moz-txt-link-abbreviated" href="mailto:catchall@mail.odoo.com">catchall@mail.odoo.com</a></td>
</tr>
<tr>
<th nowrap="" valign="BASELINE" align="RIGHT">Pour :
""" return the null value for this field in the given environment """
return False
- def convert_to_cache(self, value, env):
+ def convert_to_cache(self, value, env, validate=True):
""" convert `value` to the cache level in `env`; `value` may come from
an assignment, or have the format of methods :meth:`BaseModel.read`
or :meth:`BaseModel.write`
+
+ :param bool validate: when True, field-specific validation of
+ `value` will be performed
"""
return value
def convert_to_read(self, value, use_name_get=True):
""" convert `value` from the cache to a value as returned by method
:meth:`BaseModel.read`
+
+ :param bool use_name_get: when True, value's diplay name will
+ be computed using :meth:`BaseModel.name_get`, if relevant
+ for the field
"""
return False if value is None else value
try:
values = target._convert_to_cache({
f.name: source[f.name] for f in self.computed_fields
- })
+ }, validate=False)
except MissingError as e:
values = FailedValue(e)
target._cache.update(values)
return spec
-class Any(Field):
- """ Field for arbitrary Python values. """
- # Warning: no storage is defined for this type of field!
- type = 'any'
-
-
class Boolean(Field):
""" Boolean field. """
type = 'boolean'
- def convert_to_cache(self, value, env):
+ def convert_to_cache(self, value, env, validate=True):
return bool(value)
def convert_to_export(self, value, env):
""" Integer field. """
type = 'integer'
- def convert_to_cache(self, value, env):
+ def convert_to_cache(self, value, env, validate=True):
return int(value or 0)
def convert_to_read(self, value, use_name_get=True):
_column_digits = property(lambda self: not callable(self._digits) and self._digits)
_column_digits_compute = property(lambda self: callable(self._digits) and self._digits)
- def convert_to_cache(self, value, env):
+ def convert_to_cache(self, value, env, 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])
_related_size = property(attrgetter('size'))
_description_size = property(attrgetter('size'))
- def convert_to_cache(self, value, env):
+ def convert_to_cache(self, value, env, validate=True):
return bool(value) and ustr(value)[:self.size]
"""
type = 'text'
- def convert_to_cache(self, value, env):
+ def convert_to_cache(self, value, env, validate=True):
return bool(value) and ustr(value)
""" Html field. """
type = 'html'
- def convert_to_cache(self, value, env):
+ def convert_to_cache(self, value, env, validate=True):
return bool(value) and html_sanitize(value)
""" Convert a :class:`date` value into the format expected by the ORM. """
return value.strftime(DATE_FORMAT)
- def convert_to_cache(self, value, env):
+ def convert_to_cache(self, value, env, validate=True):
if not value:
return False
if isinstance(value, basestring):
""" Convert a :class:`datetime` value into the format expected by the ORM. """
return value.strftime(DATETIME_FORMAT)
- def convert_to_cache(self, value, env):
+ def convert_to_cache(self, value, env, validate=True):
if not value:
return False
if isinstance(value, basestring):
selection = selection(env[self.model_name])
return [value for value, _ in selection]
- def convert_to_cache(self, value, env):
+ def convert_to_cache(self, value, env, validate=True):
+ if not validate:
+ return value or False
if value in self.get_values(env):
return value
elif not value:
_column_size = property(attrgetter('size'))
- def convert_to_cache(self, value, env):
+ def convert_to_cache(self, value, env, validate=True):
if isinstance(value, BaseModel):
- if value._name in self.get_values(env) and len(value) <= 1:
+ if ((not validate or value._name in self.get_values(env))
+ and len(value) <= 1):
return value.with_env(env) or False
elif isinstance(value, basestring):
res_model, res_id = value.split(',')
""" Update the cached value of `self` for `records` with `value`. """
records._cache[self] = value
- def convert_to_cache(self, value, env):
+ def convert_to_cache(self, value, env, validate=True):
if isinstance(value, (NoneType, int)):
return env[self.comodel_name].browse(value)
if isinstance(value, BaseModel):
for record in records:
record._cache[self] = record[self.name] | value
- def convert_to_cache(self, value, env):
+ def convert_to_cache(self, value, env, validate=True):
if isinstance(value, BaseModel):
if value._name == self.comodel_name:
return value.with_env(env)
_sequence = None
_description = None
_needaction = False
+ _translate = True # set to False to disable translations export for this model
# dict of {field:method}, with method returning the (name_get of records, {id: fold})
# to include in the _read_group, if grouped on this field
# this field 'id' must override any other column or field
cls._add_field('id', fields.Id(automatic=True))
- add('display_name', fields.Char(string='Name',
- compute='_compute_display_name', inverse='_inverse_display_name',
- search='_search_display_name', automatic=True))
+ add('display_name', fields.Char(string='Display Name', automatic=True,
+ compute='_compute_display_name'))
if cls._log_access:
add('create_uid', fields.Many2one('res.users', string='Created by', automatic=True))
depends = dict(parent_class._depends)
for m, fs in cls._depends.iteritems():
- depends.setdefault(m, []).extend(fs)
+ depends[m] = depends.get(m, []) + fs
old_constraints = parent_class._constraints
new_constraints = cls._constraints
@api.depends(lambda self: (self._rec_name,) if self._rec_name else ())
def _compute_display_name(self):
- name = self._rec_name
- if name in self._fields:
- convert = self._fields[name].convert_to_display_name
- for record in self:
- record.display_name = convert(record[name])
- else:
- for record in self:
- record.display_name = "%s,%s" % (record._name, record.id)
-
- def _inverse_display_name(self):
- name = self._rec_name
- if name in self._fields and not self._fields[name].relational:
- for record in self:
- record[name] = record.display_name
- else:
- _logger.warning("Cannot inverse field display_name on %s", self._name)
-
- def _search_display_name(self, operator, value):
- name = self._rec_name
- if name in self._fields:
- return [(name, operator, value)]
- else:
- _logger.warning("Cannot search field display_name on %s", self._name)
- return [(0, '=', 1)]
+ for i, got_name in enumerate(self.name_get()):
+ self[i].display_name = got_name[1]
@api.multi
def name_get(self):
:return: list of pairs ``(id, text_repr)`` for all records
"""
result = []
- for record in self:
- try:
- result.append((record.id, record.display_name))
- except MissingError:
- pass
+ name = self._rec_name
+ if name in self._fields:
+ convert = self._fields[name].convert_to_display_name
+ for record in self:
+ result.append((record.id, convert(record[name])))
+ else:
+ for record in self:
+ result.append((record.id, "%s,%s" % (record._name, record.id)))
+
return result
@api.model
:rtype: tuple
:return: the :meth:`~.name_get` pair value of the created record
"""
- # Shortcut the inverse function of 'display_name' with self._rec_name.
- # This is useful when self._rec_name is a required field: in that case,
- # create() creates a record without the field, and inverse display_name
- # afterwards.
- field_name = self._rec_name if self._rec_name else 'display_name'
- record = self.create({field_name: name})
- return (record.id, record.display_name)
+ if self._rec_name:
+ record = self.create({self._rec_name: name})
+ return record.name_get()[0]
+ else:
+ _logger.warning("Cannot execute name_create, no _rec_name defined on %s", self._name)
+ return False
@api.model
def name_search(self, name='', args=None, operator='ilike', limit=100):
:return: list of pairs ``(id, text_repr)`` for all matching records.
"""
args = list(args or [])
- if not (name == '' and operator == 'ilike'):
- args += [('display_name', operator, name)]
+ if not self._rec_name:
+ _logger.warning("Cannot execute name_search, no _rec_name defined on %s", self._name)
+ elif not (name == '' and operator == 'ilike'):
+ args += [(self._rec_name, operator, name)]
return self.search(args, limit=limit).name_get()
def _name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=100, name_get_uid=None):
# for the name_get part to solve some access rights issues
args = list(args or [])
# optimize out the default criterion of ``ilike ''`` that matches everything
- if not (name == '' and operator == 'ilike'):
- args += [('display_name', operator, name)]
+ if not self._rec_name:
+ _logger.warning("Cannot execute name_search, no _rec_name defined on %s", self._name)
+ elif not (name == '' and operator == 'ilike'):
+ args += [(self._rec_name, operator, name)]
access_rights_uid = name_get_uid or user
ids = self._search(cr, user, args, limit=limit, context=context, access_rights_uid=access_rights_uid)
res = self.name_get(cr, access_rights_uid, ids, context)
if column_name in defaults:
default = field.convert_to_write(defaults[column_name])
- if default is not None:
- _logger.debug("Table '%s': setting default value of new column %s",
- self._table, column_name)
- ss = self._columns[column_name]._symbol_set
+ ss = self._columns[column_name]._symbol_set
+ store_default = ss[1](default)
+ if store_default is not None:
+ _logger.debug("Table '%s': setting default value of new column %s to %r",
+ self._table, column_name, default)
query = 'UPDATE "%s" SET "%s"=%s WHERE "%s" is NULL' % (
self._table, column_name, ss[0], column_name)
- cr.execute(query, (ss[1](default),))
+ cr.execute(query, (store_default,))
# this is a disgrace
cr.commit()
if field not in self._cache:
for values in result:
record = self.browse(values.pop('id'))
- record._cache.update(record._convert_to_cache(values))
+ record._cache.update(record._convert_to_cache(values, validate=False))
if field not in self._cache:
e = AccessError("No value found for %s.%s" % (self, field.name))
self._cache[field] = FailedValue(e)
# store result in cache for POST fields
for vals in result:
record = self.browse(vals['id'])
- record._cache.update(record._convert_to_cache(vals))
+ record._cache.update(record._convert_to_cache(vals, validate=False))
# determine the fields that must be processed now
fields_post = [f for f in field_names if not self._columns[f]._classic_write]
# store result in cache
for vals in result:
record = self.browse(vals.pop('id'))
- record._cache.update(record._convert_to_cache(vals))
+ record._cache.update(record._convert_to_cache(vals, validate=False))
# store failed values in cache for the records that could not be read
fetched = self.browse(ids)
if not self:
return True
- cr, uid, context = self.env.args
self._check_concurrency(self._ids)
self.check_access_rights('write')
context = dict(args[0] if args else self._context, **kwargs)
return self.with_env(self.env(context=context))
- def _convert_to_cache(self, values):
+ def _convert_to_cache(self, values, validate=True):
""" Convert the `values` dictionary into cached values. """
fields = self._fields
return {
- name: fields[name].convert_to_cache(value, self.env)
+ name: fields[name].convert_to_cache(value, self.env, validate=validate)
for name, value in values.iteritems()
if name in fields
}
return
if 'value' in method_res:
method_res['value'].pop('id', None)
- self.update(self._convert_to_cache(method_res['value']))
+ self.update(self._convert_to_cache(method_res['value'], validate=False))
if 'domain' in method_res:
result.setdefault('domain', {}).update(method_res['domain'])
if 'warning' in method_res:
'STORE_NAME', 'GET_ITER', 'FOR_ITER', 'LIST_APPEND', 'DELETE_NAME',
'JUMP_FORWARD', 'JUMP_IF_TRUE', 'JUMP_IF_FALSE', 'JUMP_ABSOLUTE',
'MAKE_FUNCTION', 'SLICE+0', 'SLICE+1', 'SLICE+2', 'SLICE+3', 'BREAK_LOOP',
- 'CONTINUE_LOOP', 'RAISE_VARARGS',
+ 'CONTINUE_LOOP', 'RAISE_VARARGS', 'YIELD_VALUE',
# New in Python 2.7 - http://bugs.python.org/issue4715 :
'JUMP_IF_FALSE_OR_POP', 'JUMP_IF_TRUE_OR_POP', 'POP_JUMP_IF_FALSE',
'POP_JUMP_IF_TRUE', 'SETUP_EXCEPT', 'END_FINALLY',
import config
import misc
-from misc import UpdateableStr
from misc import SKIPPED_ELEMENT_TYPES
import osutil
import openerp
_logger.error("Unable to find object %r", model)
continue
+ if not registry[model]._translate:
+ # explicitly disabled
+ continue
+
exists = registry[model].exists(cr, uid, res_id)
if not exists:
_logger.warning("Unable to find object %r with id %d", model, res_id)
_logger.error("name error in %s: %s", xml_name, str(exc))
continue
objmodel = registry.get(obj.model)
- if objmodel is None or field_name not in objmodel._columns:
+ if (objmodel is None or field_name not in objmodel._columns
+ or not objmodel._translate):
continue
field_def = objmodel._columns[field_name]