[MERGE] forward port branch saas-14 up to 557feb950c

This commit is contained in:
Christophe Simonis
2017-01-10 13:52:44 +01:00
52 changed files with 651 additions and 159 deletions
+21 -15
View File
@@ -350,6 +350,26 @@ class AccountChartTemplate(models.Model):
new_xml_id = str(company.id)+'_'+template_xmlid.name
return ir_model_data._update(model, template_xmlid.module, vals, xml_id=new_xml_id, store=True, noupdate=True, mode='init', res_id=False)
def _get_account_vals(self, company, account_template, code_acc, tax_template_ref):
""" This method generates a dictionnary of all the values for the account that will be created.
"""
self.ensure_one()
tax_ids = []
for tax in account_template.tax_ids:
tax_ids.append(tax_template_ref[tax.id])
val = {
'name': account_template.name,
'currency_id': account_template.currency_id and account_template.currency_id.id or False,
'code': code_acc,
'user_type_id': account_template.user_type_id and account_template.user_type_id.id or False,
'reconcile': account_template.reconcile,
'note': account_template.note,
'tax_ids': [(6, 0, tax_ids)],
'company_id': company.id,
'tag_ids': [(6, 0, [t.id for t in account_template.tag_ids])],
}
return val
@api.multi
def generate_account(self, tax_template_ref, acc_template_ref, code_digits, company):
""" This method for generating accounts from templates.
@@ -365,25 +385,11 @@ class AccountChartTemplate(models.Model):
account_tmpl_obj = self.env['account.account.template']
acc_template = account_tmpl_obj.search([('nocreate', '!=', True), ('chart_template_id', '=', self.id)], order='id')
for account_template in acc_template:
tax_ids = []
for tax in account_template.tax_ids:
tax_ids.append(tax_template_ref[tax.id])
code_main = account_template.code and len(account_template.code) or 0
code_acc = account_template.code or ''
if code_main > 0 and code_main <= code_digits:
code_acc = str(code_acc) + (str('0'*(code_digits-code_main)))
vals = {
'name': account_template.name,
'currency_id': account_template.currency_id and account_template.currency_id.id or False,
'code': code_acc,
'user_type_id': account_template.user_type_id and account_template.user_type_id.id or False,
'reconcile': account_template.reconcile,
'note': account_template.note,
'tax_ids': [(6, 0, tax_ids)],
'company_id': company.id,
'tag_ids': [(6, 0, [t.id for t in account_template.tag_ids])],
}
vals = self._get_account_vals(company, account_template, code_acc, tax_template_ref)
new_account = self.create_record_with_xmlid(company, account_template, 'account.account', vals)
acc_template_ref[account_template.id] = new_account
return acc_template_ref
+1 -6
View File
@@ -144,7 +144,7 @@
</div>
</div>
<h2>Invoicing</h2>
<div class="row mt16 o_settings_container">
<div class="row mt16 o_settings_container" id="invoicing_settings">
<div class="col-xs-12 col-md-6 o_setting_box">
<div class="o_setting_left_pane">
<field name="group_proforma_invoices"/>
@@ -176,11 +176,6 @@
<div class="text-muted">
Mail your invoices in one-click using <a href="https://www.docsaway.com/">Docsaway</a>
</div>
<div id="print_docsway_message" class="content-group" attrs="{'invisible': [('module_print_docsaway', '=', False)]}">
<div class="text-warning mt16 mb4">
Save this page and come back here to set up the feature.
</div>
</div>
</div>
</div>
</div>
+1 -1
View File
@@ -98,7 +98,7 @@
<th>Terms &amp; Conditions</th>
<td></td>
<td>
<a class="fa fa-arrow-right" t-attf-href="#{prepare_backend_url('sales_team.action_sale_config', 'form')}&amp;id=#{company_id.id}" target="_blank">
<a class="fa fa-arrow-right" t-attf-href="#{prepare_backend_url('sales_team.action_sale_config', 'form')}" target="_blank">
Change
</a>
</td>
@@ -66,7 +66,7 @@
<div class="o_setting_right_pane">
<label for="id" string="Cash Basis"/>
<div class="text-muted">
Declare taxes at payment date.
Journal used to declare taxes at payment date
</div>
<field name="tax_cash_basis_journal_id"/>
</div>
@@ -6,7 +6,7 @@
<field name="inherit_id" ref="base_setup.base_config_settings_view_form"/>
<field name="arch" type="xml">
<div name='auth_ldap_right_pane' position="inside">
<div>
<div attrs="{'invisible': [('module_auth_ldap','=',False)]}">
<button type="action" name="%(auth_ldap.action_ldap_installer)d" string="LDAP Server" icon="fa-arrow-right" class="btn-link"/>
</div>
</div>
+2 -2
View File
@@ -6,14 +6,14 @@
<field name="inherit_id" ref="base_setup.base_config_settings_view_form"/>
<field name="arch" type="xml">
<div id="msg_module_auth_oauth" position="replace">
<div class="content-group">
<div class="content-group" attrs="{'invisible': [('module_auth_oauth','=',False)]}">
<div class="mt16">
<button type="action" name="%(auth_oauth.action_oauth_provider)d" string="OAuth Providers" icon="fa-arrow-right" class="btn-link"/>
</div>
</div>
</div>
<div id="module_auth_oauth" position="after">
<div class="col-xs-12 col-md-6 o_setting_box">
<div class="col-xs-12 col-md-6 o_setting_box" attrs="{'invisible': [('module_auth_oauth','=',False)]}">
<div class="o_setting_left_pane">
<field name="auth_oauth_google_enabled"/>
</div>
@@ -28,6 +28,8 @@ var FormViewBarcodeHandler = common.AbstractField.extend(BarcodeHandlerMixin, {
this.__quantity_listener = _.bind(this._set_quantity_listener, this);
BarcodeHandlerMixin.init.apply(this, arguments);
this.process_barcode_mutex = new utils.Mutex();
return this._super.apply(this, arguments);
},
@@ -153,25 +155,35 @@ var FormViewBarcodeHandler = common.AbstractField.extend(BarcodeHandlerMixin, {
else if (this.form_view.get('actual_mode') === 'view')
this._display_no_edit_mode_warning();
else {
// Call hook method possibly implemented by subclass
this.pre_onchange_hook(barcode).then(function(proceed) {
if (proceed === true) {
// Wait for hypothetical ongoing onchange to finish
self.form_view.onchanges_mutex.exec(function() {
// A real onchange is triggered when a value actually changes (which can correspond
// to a widget's blur event per example). Commit the value of fields before
// programmatically triggering an onchange to be consistent with this.
var mutex_commit_value = new utils.Mutex();
_.each(self.form_view.fields, function(field) {
mutex_commit_value.exec(_.bind(field.commit_value, field));
});
return mutex_commit_value.def.then(function(){
// Trigger the barcode onchange
self.set_value(barcode);
});
});
var process_barcode = function () {
// this function can be passed to `Mutex.exec` in order to make sure
// that every ongoing onchanges in the form view are done
var form_onchanges_mutex = function () {
return self.form_view.onchanges_mutex.def;
}
});
// before setting the barcode field with the received barcode, we commit
// every fields of the form view and we wait for their hypothetical ongoing
// onchanges to finish
var commit_mutex = new utils.Mutex();
_.each(self.form_view.fields, function (field) {
commit_mutex.exec(function () {
return field.commit_value();
});
commit_mutex.exec(form_onchanges_mutex);
});
return commit_mutex.def.then(function () {
return self.pre_onchange_hook(barcode).then(function (proceed) {
if (proceed) {
self.set_value(barcode); // set the barcode field with the received one
return form_onchanges_mutex(); // wait for its onchange to finish
}
});
});
};
this.process_barcode_mutex.exec(process_barcode);
}
},
+8 -8
View File
@@ -7,32 +7,32 @@
<field name="arch" type="xml">
<xpath expr="//div[@name='base_gengo_right_pane']" position="inside">
<div name="base_gengo_right_pane" position="replace">
<label for="id" string="Public Key"/>
<div name="gengo_public_key">
<label for="id" string="Public Key" attrs="{'invisible': [('module_base_gengo','=',False)]}"/>
<div name="gengo_public_key" attrs="{'invisible': [('module_base_gengo','=',False)]}">
<div name="gengo_public_key">
<field name="gengo_public_key" nolabel="1" placeholder="Add Gengo login Public Key..."/>
</div>
</div>
<label for="id" string="Private Key"/>
<div name="gengo_private_key">
<label for="id" string="Private Key" attrs="{'invisible': [('module_base_gengo','=',False)]}"/>
<div name="gengo_private_key" attrs="{'invisible': [('module_base_gengo','=',False)]}">
<div name="gengo_private_key">
<field name="gengo_private_key" password="True" nolabel="1" placeholder="Add Gengo login Private Key..."/>
</div>
</div>
<div name="gengo_auto_approve">
<div name="gengo_auto_approve" attrs="{'invisible': [('module_base_gengo','=',False)]}">
<div name="gengo_auto_approve">
<field name="gengo_auto_approve"/>
<label for="id" string="Auto Approve Translation"/>
</div>
</div>
<div name="gengo_sandbox">
<div name="gengo_sandbox" attrs="{'invisible': [('module_base_gengo','=',False)]}">
<div name="gengo_sandbox">
<field name="gengo_sandbox"/>
<label for="id" string="Sandbox Mode"/>
</div>
</div>
<label for="id" string="Comment"/>
<div name="gengo_comment">
<label for="id" string="Comment" attrs="{'invisible': [('module_base_gengo','=',False)]}"/>
<div name="gengo_comment" attrs="{'invisible': [('module_base_gengo','=',False)]}">
<div name="gengo_comment">
<field name="gengo_comment" nolabel="1" placeholder="Add your comments here for translator...."/>
</div>
@@ -402,6 +402,20 @@ class GoogleCalendar(models.AbstractModel):
data_json = json.dumps(data)
return self.env['google.service']._do_request(url, data_json, headers, type='PUT')
def create_from_google(self, event, partner_id):
context_tmp = dict(self._context, NewMeeting=True)
res = self.with_context(context_tmp).update_from_google(False, event.GG.event, "create")
event.OE.event_id = res
meeting = self.env['calendar.event'].browse(res)
attendee_record = self.env['calendar.attendee'].search([('partner_id', '=', partner_id), ('event_id', '=', res)])
attendee_record.with_context(context_tmp).write({'oe_synchro_date': meeting.oe_update_date, 'google_internal_event_id': event.GG.event['id']})
if meeting.recurrency:
attendees = self.env['calendar.attendee'].sudo().search([('google_internal_event_id', '=ilike', '%s\_%%' % event.GG.event['id'])])
excluded_recurrent_event_ids = set(attendee.event_id for attendee in attendees)
for event in excluded_recurrent_event_ids:
event.write({'recurrent_id': meeting.id, 'recurrent_id_date': event.start, 'user_id': meeting.user_id.id})
return event
def update_from_google(self, event, single_event_dict, type):
""" Update an event in Odoo with information from google calendar
:param event : record od calendar.event to update
@@ -786,13 +800,8 @@ class GoogleCalendar(models.AbstractModel):
if isinstance(actToDo, NothingToDo):
continue
elif isinstance(actToDo, Create):
context_tmp = {'newMeeting': True}
if actSrc == 'GG':
res = recs.with_context(context_tmp).update_from_google(False, event.GG.event, "create")
event.OE.event_id = res
meeting = CalendarEvent.browse(res)
attendee_records = CalendarAttendee.search([('partner_id', '=', my_partner_id), ('event_id', '=', res)])
attendee_records.with_context(context_tmp).write({'oe_synchro_date': meeting.oe_update_date, 'google_internal_event_id': event.GG.event['id']})
self.create_from_google(event, my_partner_id)
elif actSrc == 'OE':
raise "Should be never here, creation for OE is done before update !"
#TODO Add to batch
@@ -817,6 +826,12 @@ class GoogleCalendar(models.AbstractModel):
main_ev = CalendarAttendee.with_context(context_novirtual).search([('google_internal_event_id', '=', event.GG.event['id'].rsplit('_', 1)[0])], limit=1)
event_to_synchronize[base_event][0][1].OE.event_id = main_ev.event_id.id
if event_to_synchronize[base_event][0][1].OE.event_id:
parent_event['id'] = "%s-%s" % (event_to_synchronize[base_event][0][1].OE.event_id, new_google_event_id)
res = self.update_from_google(parent_event, event.GG.event, "copy")
else:
self.create_from_google(event, my_partner_id)
parent_event['id'] = "%s-%s" % (event_to_synchronize[base_event][0][1].OE.event_id, new_google_event_id)
res = recs.update_from_google(parent_event, event.GG.event, "copy")
else:
@@ -1568,28 +1568,28 @@
<field name="code">4700</field>
<field name="reconcile" eval="True"/>
<field name="name">Hacienda Pública, deudora por IVA</field>
<field name="user_type_id" ref="account.data_account_type_current_liabilities"/>
<field name="user_type_id" ref="account.data_account_type_current_assets"/>
<field name="chart_template_id" ref="l10n_es.account_chart_template_common"/>
</record>
<record id="pgc_4708_child" model="account.account.template">
<field name="code">4708</field>
<field name="reconcile" eval="True"/>
<field name="name">Hacienda Pública, deudora por subvenciones concedidas</field>
<field name="user_type_id" ref="account.data_account_type_expenses"/>
<field name="user_type_id" ref="account.data_account_type_current_assets"/>
<field name="chart_template_id" ref="l10n_es.account_chart_template_common"/>
</record>
<record id="pgc_4709_child" model="account.account.template">
<field name="code">4709</field>
<field name="reconcile" eval="True"/>
<field name="name">Hacienda Pública, deudora por devolución de impuestos</field>
<field name="user_type_id" ref="account.data_account_type_expenses"/>
<field name="user_type_id" ref="account.data_account_type_current_assets"/>
<field name="chart_template_id" ref="l10n_es.account_chart_template_common"/>
</record>
<record id="pgc_471_child" model="account.account.template">
<field name="code">471</field>
<field name="reconcile" eval="False"/>
<field name="name">Organismos de la Seguridad Social, deudores</field>
<field name="user_type_id" ref="account.data_account_type_expenses"/>
<field name="user_type_id" ref="account.data_account_type_current_assets"/>
<field name="chart_template_id" ref="l10n_es.account_chart_template_common"/>
</record>
<record id="pgc_472_child" model="account.account.template">
@@ -1603,77 +1603,77 @@
<field name="code">473</field>
<field name="reconcile" eval="True"/>
<field name="name">Hacienda Pública, retenciones y pagos a cuenta</field>
<field name="user_type_id" ref="account.data_account_type_expenses"/>
<field name="user_type_id" ref="account.data_account_type_current_assets"/>
<field name="chart_template_id" ref="l10n_es.account_chart_template_common"/>
</record>
<record id="pgc_4740_child" model="account.account.template">
<field name="code">4740</field>
<field name="reconcile" eval="False"/>
<field name="name">Activos por diferencias temporarias deducibles</field>
<field name="user_type_id" ref="account.data_account_type_expenses"/>
<field name="user_type_id" ref="account.data_account_type_non_current_assets"/>
<field name="chart_template_id" ref="l10n_es.account_chart_template_common"/>
</record>
<record id="pgc_4742_child" model="account.account.template">
<field name="code">4742</field>
<field name="reconcile" eval="False"/>
<field name="name">Derechos por deducciones y bonificaciones pendientes de aplicar</field>
<field name="user_type_id" ref="account.data_account_type_expenses"/>
<field name="user_type_id" ref="account.data_account_type_non_current_assets"/>
<field name="chart_template_id" ref="l10n_es.account_chart_template_common"/>
</record>
<record id="pgc_4745_child" model="account.account.template">
<field name="code">4745</field>
<field name="reconcile" eval="False"/>
<field name="name">Crédito por pérdidas a compensar del ejercicio</field>
<field name="user_type_id" ref="account.data_account_type_expenses"/>
<field name="user_type_id" ref="account.data_account_type_non_current_assets"/>
<field name="chart_template_id" ref="l10n_es.account_chart_template_common"/>
</record>
<record id="pgc_4750_child" model="account.account.template">
<field name="code">4750</field>
<field name="reconcile" eval="True"/>
<field name="name">Hacienda Pública, acreedora por IVA</field>
<field name="user_type_id" ref="account.data_account_type_expenses"/>
<field name="user_type_id" ref="account.data_account_type_current_liabilities"/>
<field name="chart_template_id" ref="l10n_es.account_chart_template_common"/>
</record>
<record id="pgc_4751_child" model="account.account.template">
<field name="code">4751</field>
<field name="reconcile" eval="True"/>
<field name="name">Hacienda Pública, acreedora por retenciones practicadas</field>
<field name="user_type_id" ref="account.data_account_type_expenses"/>
<field name="user_type_id" ref="account.data_account_type_current_liabilities"/>
<field name="chart_template_id" ref="l10n_es.account_chart_template_common"/>
</record>
<record id="pgc_4752_child" model="account.account.template">
<field name="code">4752</field>
<field name="reconcile" eval="True"/>
<field name="name">Hacienda Pública, acreedora por impuesto sobre sociedades</field>
<field name="user_type_id" ref="account.data_account_type_expenses"/>
<field name="user_type_id" ref="account.data_account_type_current_liabilities"/>
<field name="chart_template_id" ref="l10n_es.account_chart_template_common"/>
</record>
<record id="pgc_4758_child" model="account.account.template">
<field name="code">4758</field>
<field name="reconcile" eval="True"/>
<field name="name">Hacienda Pública, acreedora por subvenciones a reintegrar</field>
<field name="user_type_id" ref="account.data_account_type_expenses"/>
<field name="user_type_id" ref="account.data_account_type_current_liabilities"/>
<field name="chart_template_id" ref="l10n_es.account_chart_template_common"/>
</record>
<record id="pgc_476_child" model="account.account.template">
<field name="code">476</field>
<field name="reconcile" eval="False"/>
<field name="name">Organismos de la Seguridad Social, acreedores</field>
<field name="user_type_id" ref="account.data_account_type_expenses"/>
<field name="user_type_id" ref="account.data_account_type_current_liabilities"/>
<field name="chart_template_id" ref="l10n_es.account_chart_template_common"/>
</record>
<record id="pgc_477_child" model="account.account.template">
<field name="code">477</field>
<field name="reconcile" eval="True"/>
<field name="name">Hacienda Pública. IVA repercutido</field>
<field name="user_type_id" ref="account.data_account_type_expenses"/>
<field name="user_type_id" ref="account.data_account_type_current_liabilities"/>
<field name="chart_template_id" ref="l10n_es.account_chart_template_common"/>
</record>
<record id="pgc_479_child" model="account.account.template">
<field name="code">479</field>
<field name="reconcile" eval="False"/>
<field name="name">Pasivos por diferencias temporarias imponibles</field>
<field name="user_type_id" ref="account.data_account_type_expenses"/>
<field name="user_type_id" ref="account.data_account_type_non_current_liabilities"/>
<field name="chart_template_id" ref="l10n_es.account_chart_template_common"/>
</record>
<record id="pgc_480_child" model="account.account.template">
+2
View File
@@ -40,5 +40,7 @@ With this module you will have:
"data/account_data.xml",
"data/account_tax_data.xml",
"data/account_chart_template_data.yml",
"data/fiscal_position_data.xml",
"views/res_config_view.xml",
],
}
@@ -0,0 +1,115 @@
<?xml version="1.0" ?>
<odoo>
<data noupdate="1">
<record id="account_fiscal_position_personasmoralesdelrgimengeneral0" model="account.fiscal.position">
<field name="name">Personas morales del régimen general</field>
<field name="note">Es para las sociedades mercantiles, asociaciones civiles de profesionales que:
- Realicen actividades lucrativas,
- Sociedades cooperativas de producción,
- Instituciones de crédito tales como bancos y casas de bolsa,
- Organismos descentralizados que comercialicen bienes o servicios, entre otras.
http://www.sat.gob.mx/6_406.html
última modificación :
16/febrero/2010, 12:56, información vigente.</field>
</record>
<record id="account_fiscal_position_personasmoralesconfinesnolucrativos0" model="account.fiscal.position">
<field name="name">Personas morales con fines no lucrativos</field>
<field name="note">Es para las personas morales cuya finalidad no es obtener una ganancia económica o lucro, por ejemplo:
- Instituciones de asistencia o de beneficencia
- Asociaciones o sociedades civiles con fines políticos o deportivos
- Asociaciones religiosas
- Asociaciones patronales; sindicatos obreros; cámaras de comercio e industria
- Asociaciones o sociedades civiles de enseñanza; de investigación científica o tecnológica
- Sociedades cooperativas de consumo o sociedades mutualistas.
- Colegios de profesionales
- Partidos políticos
- Asociaciones de padres de familia
- Asociaciones civiles de colonos o las que administren inmuebles en condominio
http://www.sat.gob.mx/6_404.html
última modificación :
16/febrero/2010, 12:57, información vigente.</field>
</record>
<record id="account_fiscal_position_asociacionesreligiosas0" model="account.fiscal.position">
<field name="name">Asociaciones Religiosas</field>
<field name="note">Son congregaciones que tienen como fin el ejercer culto a una divinidad; para efectos fiscales, se considerarán a las iglesias y demás agrupaciones que obtengan su correspondiente registro constitutivo ante la Secretaría de Gobernación, en los términos de la Ley de Asociaciones Religiosas y Culto Público, ya que es a través de este registro como se obtiene la personalidad jurídica.
http://www.sat.gob.mx/sitio_internet/asistencia_contribuyente/principiantes/permonl/68_1813.html
última modificación :
10/abril/2012, 18:15, información vigente.</field>
</record>
<record id="account_fiscal_position_personasmoralesdelrgimensimplificado0" model="account.fiscal.position">
<field name="name">Personas morales del régimen simplificado</field>
<field name="note">Deben tributar en el régimen simplificado las siguientes personas morales:
- Las dedicadas exclusivamente al autotransporte terrestre de carga o de pasajeros.
- Las que se dediquen exclusivamente a actividades:
1) Agrícolas
2) Ganaderas
3) Silvícolas
4) Pesqueras
- Las constituidas como empresas integradoras.
http://www.sat.gob.mx/6_405.html
última modificación :
24/agosto/2009, 12:03, información vigente
</field>
</record>
<record id="account_fiscal_position_personafisicaconactividadempresarial0" model="account.fiscal.position">
<field name="name">Persona física con actividad empresarial</field>
<field name="note">Es el régimen conforme al cual pueden pagar sus impuestos todas las personas físicas que se dediquen a las siguientes actividades
- Comerciales.
- Industriales, que consisten en la extracción, conservación o transformación de materias primas, acabado de productos y la elaboración de satisfactores;
- Agrícolas, que comprenden las actividades de siembra, cultivo, cosecha y la primera enajenación de los productos obtenidos, que no hayan sido objeto de transformación industrial;
- Ganaderas, que son las consistentes en la cría y engorda de ganado, aves de corral y animales, así como la primera enajenación de sus productos, que no hayan sido objeto de transformación industrial;
- De pesca, que incluyen la cría, cultivo, fomento y cuidado de la reproducción de toda clase de especies marinas y de agua dulce, incluida la acuacultura, así como la captura y extracción de las mismas y la primera enajenación de sus productos, que no hayan sido objeto de transformación industrial;
- Silvícolas, que son las de cultivo de los bosques o montes, así como la cría, conservación, restauración, fomento y aprovechamiento de la vegetación de los mismos y la primera enajenación de sus productos, que no hayan sido objeto de transformación industrial.
http://www.sat.gob.mx/sitio_internet/asistencia_contribuyente/principiantes/empresarial/11_428.html
última modificación :
12/septiembre/2005, 15:58, información vigente
</field>
</record>
<record id="account_fiscal_position_foreign" model="account.fiscal.position">
<field name="name">Foreign Customer</field>
</record>
<record id="account_fiscal_position0_purchase" model="account.fiscal.position.tax">
<field name="tax_src_id" ref="l10n_mx.1_tax14"/>
<field name="tax_dest_id" ref="l10n_mx.1_tax13"/>
<field name="position_id" ref="account_fiscal_position_foreign"/>
</record>
<record id="account_fiscal_position0_sale" model="account.fiscal.position.tax">
<field name="tax_src_id" ref="l10n_mx.1_tax12"/>
<field name="tax_dest_id" ref="l10n_mx.1_tax9"/>
<field name="position_id" ref="account_fiscal_position_foreign"/>
</record>
</data>
</odoo>
+1
View File
@@ -4,3 +4,4 @@
from . import chart_template
from . import account
from . import res_config
+9
View File
@@ -0,0 +1,9 @@
# -*- coding: utf-8 -*-
from odoo import fields, models
class AccountConfigSettings(models.TransientModel):
_inherit = 'res.config.settings'
module_l10n_mx_edi = fields.Boolean('Mexican Electronic Invoicing')
+23
View File
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="account_config_settings_account_mx_inherit" model="ir.ui.view">
<field name="name">account.config.settings.form.inherit</field>
<field name="model">account.config.settings</field>
<field name="inherit_id" ref="account.account_config_settings_view_form"/>
<field name="arch" type="xml">
<xpath expr=".//div[@id='invoicing_settings']" position="inside">
<div class="col-xs-12 col-md-6 o_setting_box">
<div class="o_setting_left_pane">
<field name="module_l10n_mx_edi" class="oe_inline" widget="upgrade_boolean"/>
</div>
<div class="o_setting_right_pane">
<label for="module_l10n_mx_edi"/>
<div class="text-muted">
Create your electronic invoices automatically (CFDI format)
</div>
</div>
</div>
</xpath>
</field>
</record>
</odoo>
+2 -2
View File
@@ -271,8 +271,8 @@ class MrpProduction(models.Model):
boms, lines = production.bom_id.explode(production.product_id, factor, picking_type=production.bom_id.picking_type_id)
production._generate_raw_moves(lines)
# Check for all draft moves whether they are mto or not
self._adjust_procure_method()
self.move_raw_ids.action_confirm()
production._adjust_procure_method()
production.move_raw_ids.action_confirm()
return True
def _generate_finished_moves(self):
+13 -1
View File
@@ -4,6 +4,9 @@ import json
import logging
import urlparse
import dateutil.parser
import pytz
from odoo import api, fields, models, _
from odoo.addons.payment.models.payment_acquirer import ValidationError
from odoo.addons.payment_paypal.controllers.main import PaypalController
@@ -193,7 +196,16 @@ class TxPaypal(models.Model):
}
if status in ['Completed', 'Processed']:
_logger.info('Validated Paypal payment for tx %s: set as done' % (self.reference))
res.update(state='done', date_validate=data.get('payment_date', fields.datetime.now()))
try:
# dateutil and pytz don't recognize abbreviations PDT/PST
tzinfos = {
'PST': -8 * 3600,
'PDT': -7 * 3600,
}
date_validate = dateutil.parser.parse(data.get('payment_date'), tzinfos=tzinfos).astimezone(pytz.utc)
except:
date_validate = fields.Datetime.now()
res.update(state='done', date_validate=date_validate)
return self.write(res)
elif status in ['Pending', 'Expired']:
_logger.info('Received notification for Paypal payment %s: set as pending' % (self.reference))
+1 -1
View File
@@ -187,4 +187,4 @@ class PaypalForm(PaypalCommon):
# check
self.assertEqual(tx.state, 'done', 'paypal: wrong state after receiving a valid pending notification')
self.assertEqual(tx.acquirer_reference, '08D73520KX778924N', 'paypal: wrong txn_id after receiving a valid pending notification')
self.assertEqual(tx.date_validate, '2013-11-18 03:21:19', 'paypal: wrong validation date')
self.assertEqual(tx.date_validate, '2013-11-18 11:21:19', 'paypal: wrong validation date')
@@ -1432,6 +1432,7 @@ var ReceiptScreenWidget = ScreenWidget.extend({
print_xml: function() {
var env = {
widget: this,
order: this.pos.get_order(),
receipt: this.pos.get_order().export_for_printing(),
paymentlines: this.pos.get_order().get_paymentlines()
};
+5 -1
View File
@@ -502,10 +502,14 @@ class SaleOrder(models.Model):
res = {}
currency = self.currency_id or self.company_id.currency_id
for line in self.order_line:
base_tax = 0
for tax in line.tax_id:
group = tax.tax_group_id
res.setdefault(group, 0.0)
res[group] += tax.compute_all(line.price_reduce, quantity=line.product_uom_qty)['taxes'][0]['amount']
amount = tax.compute_all(line.price_reduce + base_tax, quantity=line.product_uom_qty)['taxes'][0]['amount']
res[group] += amount
if tax.include_base_amount:
base_tax += amount
res = sorted(res.items(), key=lambda l: l[0].sequence)
res = map(lambda l: (l[0].name, l[1]), res)
return res
+1 -1
View File
@@ -44,7 +44,7 @@ class Quant(models.Model):
lot_id = fields.Many2one(
'stock.production.lot', 'Lot/Serial Number',
index=True, ondelete="restrict", readonly=True)
cost = fields.Float('Unit Cost')
cost = fields.Float('Unit Cost', group_operator='avg')
owner_id = fields.Many2one(
'res.partner', 'Owner',
index=True, readonly=True,
@@ -0,0 +1,19 @@
odoo.define("web.domainUtils", function (require) {
"use strict";
var pyeval = require("web.pyeval");
function domainToString(domain) {
if (_.isString(domain)) return domain;
return JSON.stringify(domain || []).replace(/false/g, "False").replace(/true/g, "True");
}
function stringToDomain(domain) {
if (!_.isString(domain)) return domain;
return pyeval.eval("domain", domain || "[]");
}
return {
domainToString: domainToString,
stringToDomain: stringToDomain,
};
});
+18 -1
View File
@@ -11,6 +11,7 @@ var Priority = require('web.Priority');
var ProgressBar = require('web.ProgressBar');
var Dialog = require('web.Dialog');
var DomainSelector = require("web.DomainSelector");
var DomainSelectorDialog = require("web.DomainSelectorDialog");
var common = require('web.form_common');
var formats = require('web.formats');
var framework = require('web.framework');
@@ -400,11 +401,19 @@ var FieldDomain = common.AbstractField.extend(common.ReinitializeFieldMixin).ext
e.preventDefault();
this._showSelection();
},
"click .o_form_field_domain_dialog_button": function (e) {
e.preventDefault();
this.openDomainDialog();
},
},
custom_events: {
"domain_changed": function (e) {
if (this.options.in_dialog) return;
this.set_value(this.domainSelector.getDomain(), true);
},
"domain_selected": function (e) {
this.set_value(e.data.domain);
},
},
init: function () {
this._super.apply(this, arguments);
@@ -412,6 +421,7 @@ var FieldDomain = common.AbstractField.extend(common.ReinitializeFieldMixin).ext
this.valid = true;
this.debug = session.debug;
this.options = _.defaults(this.options || {}, {
in_dialog: false,
model: undefined, // this option is mandatory !
fs_filters: {}, // Field selector filters (to only show a subset of available fields @see FieldSelector)
});
@@ -457,7 +467,7 @@ var FieldDomain = common.AbstractField.extend(common.ReinitializeFieldMixin).ext
this.domainSelector.destroy();
}
this.domainSelector = new DomainSelector(this, this.model, domain, {
readonly: this.get("effective_readonly"),
readonly: this.get("effective_readonly") || this.options.in_dialog,
fs_filters: this.options.fs_filters,
debugMode: session.debug,
});
@@ -492,6 +502,13 @@ var FieldDomain = common.AbstractField.extend(common.ReinitializeFieldMixin).ext
disable_multiple_selection: true,
}).open();
},
openDomainDialog: function () {
new DomainSelectorDialog(this, this.model, this.get("value"), {
readonly: this.get("effective_readonly"),
fs_filters: this.options.fs_filters,
debugMode: session.debug,
}).open();
},
});
var FieldDate = common.AbstractField.extend(common.ReinitializeFieldMixin, {
@@ -641,7 +641,8 @@ ListView.include(/** @lends instance.web.ListView# */{
if (saveInfo.created) {
return self.start_edition();
}
var record = self.records[next_record](saveInfo.record);
var options = { wraparound: !self.is_action_enabled('create') };
var record = self.records[next_record](saveInfo.record, options);
if (record === undefined) {
return self.start_edition();
}
@@ -466,7 +466,7 @@ var SearchView = View.extend({
*/
select_completion: function (e, ui) {
e.preventDefault();
if(ui.item.facet.values && ui.item.facet.values.length && String(ui.item.facet.values[0].value).trim() !== "") {
if(ui.item.facet && ui.item.facet.values && ui.item.facet.values.length && String(ui.item.facet.values[0].value).trim() !== "") {
this.query.add(ui.item.facet);
} else {
this.query.trigger('add');
@@ -3,9 +3,9 @@ odoo.define("web.DomainSelector", function (require) {
var core = require("web.core");
var datepicker = require("web.datepicker");
var domainUtils = require("web.domainUtils");
var formats = require ("web.formats");
var ModelFieldSelector = require("web.ModelFieldSelector");
var pyeval = require("web.pyeval");
var Widget = require("web.Widget");
var _t = core._t;
@@ -55,7 +55,7 @@ var DomainNode = Widget.extend({
},
/// A DomainNode needs a model and domain to work. It can also receives a set of options
/// @param model - a string with the model name
/// @param domain - an array of the prefix representation of the domain
/// @param domain - an array of the prefix representation of the domain (or a string which represents it)
/// @param options - an object with possible values:
/// - readonly, a boolean to indicate if the widget is readonly or not (default to true)
/// - fs_filters, an object with a series of filters to use for the field selector (see @FieldSelector)
@@ -109,7 +109,7 @@ var DomainTree = DomainNode.extend({
/// @see DomainTree._addFlattenedChildren
init: function (parent, model, domain, options) {
this._super.apply(this, arguments);
this._initialize(domain);
this._initialize(domainUtils.stringToDomain(domain));
},
/// @see DomainTree.init
_initialize: function (domain) {
@@ -265,8 +265,9 @@ var DomainSelector = DomainTree.extend({
/// the widget is re-rendered and notifies the parents. If not, a warning is shown to the
/// user and the input is ignored.
"change .o_domain_debug_input": function (e) {
var domain;
try {
var domain = pyeval.eval("domain", $(e.currentTarget).val() || "[]");
domain = domainUtils.stringToDomain($(e.currentTarget).val());
} catch (err) {
this.do_warn(_t("Syntax error"), _t("The domain you entered is not properly formed"));
return;
@@ -292,12 +293,7 @@ var DomainSelector = DomainTree.extend({
// Display technical domain if in debug mode
this.$debugInput = this.$(".o_domain_debug_input");
if (this.$debugInput.length) {
var strValue = this.getDomain() || "[]";
if (!_.isString(strValue)) { // TODO there is no "pyuneval" function
strValue = JSON.stringify(strValue);
strValue = strValue.replace(/false/g, "False").replace(/true/g, "True");
}
this.$debugInput.val(strValue);
this.$debugInput.val(domainUtils.domainToString(this.getDomain()));
}
},
});
@@ -328,6 +324,7 @@ var DomainLeaf = DomainNode.extend({
init: function (parent, model, domain, options) {
this._super.apply(this, arguments);
domain = domainUtils.stringToDomain(domain);
this.chain = domain[0][0];
this.operator = domain[0][1];
this.value = domain[0][2];
@@ -0,0 +1,48 @@
odoo.define("web.DomainSelectorDialog", function (require) {
"use strict";
var core = require("web.core");
var Dialog = require("web.Dialog");
var DomainSelector = require("web.DomainSelector");
var _t = core._t;
return Dialog.extend({
init: function (parent, model, domain, options) {
this.model = model;
this.options = _.extend({
readonly: true,
fs_filters: {},
debugMode: false,
}, options || {});
var buttons;
if (this.options.readonly) {
buttons = [
{text: _t("Close"), close: true},
];
} else {
buttons = [
{text: _t("Save"), classes: "btn-primary", close: true, click: function () {
this.trigger_up("domain_selected", {domain: this.domainSelector.getDomain()});
}},
{text: _t("Discard"), close: true},
];
}
this._super(parent, _.extend({}, {
title: _t("Domain"),
buttons: buttons,
}, options || {}));
this.domainSelector = new DomainSelector(this, model, domain, options);
},
start: function () {
this.$el.css("overflow", "visible").closest(".modal-dialog").css("height", "auto"); // This restores default modal height (bootstrap) and allows field selector to overflow
return $.when(
this._super.apply(this, arguments),
this.domainSelector.appendTo(this.$el)
);
},
});
});
@@ -24,7 +24,10 @@
&.o_domain_selector {
&.o_edit_mode {
position: relative;
padding-right: @o-domain-selector-panel-space;
> .o_domain_node_children_container {
padding-right: @o-domain-selector-panel-space;
}
}
> .o_domain_node_children_container {
@@ -91,7 +91,7 @@
// Title
.oe_title {
> h1, > h2, > h3 {
width: 100%; // Needed because inline-block when is a hx.o_row
width: 100%; // Needed because inline-block when is a hx.o_row
margin-top: 0;
margin-bottom: 0;
line-height: inherit;
@@ -276,6 +276,20 @@
padding: 0 1em;
}
}
.o_form_field_domain {
> .o_form_field_domain_panel {
margin-top: 8px;
}
&.o_inline_mode.o_edit_mode {
position: relative;
> .o_form_field_domain_panel {
.o-position-absolute(0, 0);
margin-top: 0;
}
}
}
}
// Overriden style when form view in modal
+10 -7
View File
@@ -670,13 +670,16 @@
t-att-maxlength="widget.field.size"/>
</t>
<div t-name="FieldDomain" class="o_form_field_domain">
<div t-name="FieldDomain" t-attf-class="o_form_field_domain#{widget.get('effective_readonly') ? '' : ' o_edit_mode'}#{widget.options.in_dialog ? '' : ' o_inline_mode'}">
<!-- domain selector will be instantiated here -->
<div class="mt8">
<div class="o_form_field_domain_panel">
<i class="fa fa-arrow-right"/>
<span><span class="o_domain_records_count"/> Records Selected</span>
<button class="btn btn-default btn-xs o_domain_show_selection_button" type="button">See selection</button>
<button class="btn btn-xs btn-default o_domain_show_selection_button" type="button">
<span class="o_domain_records_count"/> record(s)
</button>
<span class="text-warning o_domain_error_message hidden"><i class="fa fa-exclamation-triangle"/> Invalid domain</span>
<button t-if="widget.options.in_dialog &amp;&amp; !widget.get('effective_readonly')"
class="btn btn-sm btn-primary o_form_field_domain_dialog_button">Edit domain</button>
</div>
</div>
@@ -684,7 +687,7 @@
<div t-if="!widget.readonly &amp;&amp; !widget.noControlPanel" class="btn-group btn-group-sm pull-right o_domain_node_control_panel">
<button class="btn btn-default o_domain_delete_node_button"><i class="fa fa-minus"/></button>
<button class="btn btn-default o_domain_add_node_button"><i class="fa fa-plus"/></button>
<button class="btn btn-default o_domain_add_node_button" t-att-data-branch="1"><i class="fa fa-ellipsis-h"/></button>
<button class="btn btn-default o_domain_add_node_button" data-branch="1"><i class="fa fa-ellipsis-h"/></button>
</div>
</t>
<t t-name="DomainTree.OperatorSelector">
@@ -712,9 +715,9 @@
</t>
<t t-else="">
<div>
<t t-if="widget.children.length === 1">Match the records with the following rule:</t>
<t t-if="widget.children.length === 1">Match records with the following rule:</t>
<t t-else="">
<span>Match the records with</span>
<span>Match records with</span>
<t t-call="DomainTree.OperatorSelector"/>
<span>of the following rules:</span>
</t>
+2
View File
@@ -134,6 +134,7 @@
<script type="text/javascript" src="/web/static/lib/jquery.ba-bbq/jquery.ba-bbq.js"></script>
<script type="text/javascript" src="/web/static/src/js/framework/data_model.js"></script>
<script type="text/javascript" src="/web/static/src/js/framework/domain_utils.js"></script>
<script type="text/javascript" src="/web/static/src/js/framework/formats.js"></script>
<script type="text/javascript" src="/web/static/src/js/framework/view.js"></script>
<script type="text/javascript" src="/web/static/src/js/framework/pyeval.js"></script>
@@ -155,6 +156,7 @@
<script type="text/javascript" src="/web/static/src/js/widgets/debug_manager.js"></script>
<script type="text/javascript" src="/web/static/src/js/widgets/data_export.js"></script>
<script type="text/javascript" src="/web/static/src/js/widgets/date_picker.js"></script>
<script type="text/javascript" src="/web/static/src/js/widgets/domain_selector_dialog.js"></script>
<script type="text/javascript" src="/web/static/src/js/widgets/domain_selector.js"></script>
<script type="text/javascript" src="/web/static/src/js/widgets/loading.js"></script>
<script type="text/javascript" src="/web/static/src/js/widgets/model_field_selector.js"></script>
@@ -161,6 +161,11 @@ var FieldTextHtmlSimple = widget.extend({
return !this.get('value') || this.get('value') === "<p><br/></p>" || !this.get('value').match(/\S/);
},
commit_value: function () {
/* Switch to WYSIWYG mode if currently in code view */
if (session.debug) {
var layoutInfo = this.$textarea.data('layoutInfo');
$.summernote.pluginEvents.codeview(undefined, undefined, layoutInfo, false);
}
if (this.options['style-inline']) {
transcoder.class_to_style(this.$content);
transcoder.font_to_img(this.$content);
@@ -377,6 +382,11 @@ var FieldTextHtml = widget.extend({
this._dirty_flag = false;
return this.editor.save();
} else if (this._dirty_flag && this.editor && this.editor.buildingBlock) {
/* Switch to WYSIWYG mode if currently in code view */
if (session.debug) {
var layoutInfo = this.editor.rte.editable().data('layoutInfo');
$.summernote.pluginEvents.codeview(undefined, undefined, layoutInfo, false);
}
this.editor.buildingBlock.clean_for_save();
this.internal_set_value( this.$content.html() );
}
@@ -511,12 +511,32 @@ function prettify_html(html) {
}
return result;
}
$.summernote.pluginEvents.codeview = function (event, editor, layoutInfo) {
/*
* This override when clicking on the 'Code View' button has two aims:
*
* - have our own code view implementation for FieldTextHtml
* - add an 'enable' paramater to call the function directly and allow us to
* disable (false) or enable (true) the code view mode.
*/
$.summernote.pluginEvents.codeview = function (event, editor, layoutInfo, enable) {
if (layoutInfo === undefined) {
return;
}
if (layoutInfo.toolbar) {
// if editor inline (FieldTextHtmlSimple)
var is_activated = $.summernote.eventHandler.modules.codeview.isActivated(layoutInfo);
if (is_activated === enable) {
return;
}
return eventHandler.modules.codeview.toggle(layoutInfo);
} else {
// if editor iframe (FieldTextHtml)
var $editor = layoutInfo.editor();
var $textarea = $editor.prev('textarea');
if ($textarea.is('textarea') === enable) {
return;
}
if (!$textarea.length) {
// init and create texarea
+25 -18
View File
@@ -710,24 +710,19 @@ var KanbanView = View.extend({
var context = {};
context['default_' + this.group_by_field] = column.id;
var name = event.data.value;
this.dataset.name_create(name, context).then(function on_success (data) {
this.dataset.name_create(name).done(function(data) {
add_record(data[0]);
}, function on_fail (event) {
}).fail(function(error, event) {
event.preventDefault();
var popup = new form_common.SelectCreatePopup(this);
popup.select_element(
self.model,
{
title: _t("Create: "),
initial_view: "form",
disable_multiple_selection: true
},
[],
{"default_name": name}
);
popup.on("elements_selected", self, function(element_ids) {
add_record(element_ids[0]);
});
var dialog = new form_common.FormViewDialog(self, {
res_model: self.model,
context: _.extend({"default_name": name}, context),
title: _t("Create"),
disable_multiple_selection: true,
on_selected: function(element_ids) {
add_record(element_ids[0]);
}
}).open();
});
function add_record(id) {
@@ -817,8 +812,20 @@ function transform_qweb_template (node, fvg, many2manys) {
} else if (fields_registry.contains(ftype)) {
// do nothing, the kanban record will handle it
} else {
node.tag = qweb.prefix;
node.attrs[qweb.prefix + '-esc'] = 'record.' + node.attrs.name + '.value';
if (node.attrs.bold) {
node.tag = 'strong';
var children_node = {
attrs: {},
children: [],
tag: qweb.prefix,
};
children_node.attrs[qweb.prefix + '-esc'] = 'record.' + node.attrs.name + '.value';
node.children.push(children_node);
} else {
node.tag = qweb.prefix;
node.attrs[qweb.prefix + '-esc'] = 'record.' + node.attrs.name + '.value';
}
}
break;
case 'button':
@@ -65,7 +65,9 @@
}
.o_dropdown_kanban {
float: right;
position: absolute;
top: 5px;
right: 10px;
@media (max-width: @screen-xs-max) {
visibility: visible;
}
@@ -6,7 +6,7 @@
<field name="inherit_id" ref="website.website_config_settings_view_form"/>
<field name="arch" type="xml">
<div id="msg_contact_form_install_setting" position="after">
<div class="content-group mt16">
<div class="content-group mt16" attrs="{'invisible': [('module_website_crm', '=', False)]}">
<div class="row">
<label class="col-md-3 o_light_label" string="Sales Channel" for="crm_default_team_id"/>
<field name="crm_default_team_id" class="oe_inline"/>
+1 -1
View File
@@ -7,7 +7,7 @@
<field name="inherit_id" ref="website.website_config_settings_view_form"/>
<field name="arch" type="xml">
<div id="msg_module_website_livechat" position="replace">
<div class="content-group mt16">
<div class="content-group mt16" attrs="{'invisible': [('module_website_livechat', '=', False)]}">
<div class="row">
<label class="col-md-3 o_light_label" string="Channel" for="channel_id"/>
<field name="channel_id" class="oe_inline"/>
@@ -124,8 +124,11 @@ animation.registry.newsletter_popup = animation.Class.extend({
odoo.define('mass_mailing.unsubscribe', function (require) {
var ajax = require('web.ajax');
var core = require('web.core');
require('web_editor.base'); // wait for implicit dependencies to load
var _t = core._t;
if (!$('.o_unsubscribe_form').length) {
return $.Deferred().reject("DOM doesn't contain '.o_unsubscribe_form'");
}
@@ -148,10 +151,10 @@ odoo.define('mass_mailing.unsubscribe', function (require) {
ajax.jsonRpc('/mail/mailing/unsubscribe', 'call', {'opt_in_ids': checked_ids, 'opt_out_ids': unchecked_ids, 'email': email, 'mailing_id': mailing_id})
.then(function(result) {
$('.alert-info').html('Your changes has been saved.').removeClass('alert-info').addClass('alert-success');
$('.alert-info').html(_t('Your changes have been saved.')).removeClass('alert-info').addClass('alert-success');
})
.fail(function() {
$('.alert-info').html('You changes has not been saved, try again later.').removeClass('alert-info').addClass('alert-warning');
$('.alert-info').html(_t('Your changes have not been saved, try again later.')).removeClass('alert-info').addClass('alert-warning');
});
});
});
@@ -217,7 +217,7 @@ class website_account(website_account):
partner = request.env['res.users'].browse(request.uid).partner_id
invoices = request.env['account.invoice'].sudo().search_count([('partner_id', '=', partner.id), ('state', 'not in', ['draft', 'cancel'])])
if invoices:
if data.get('vat', partner.vat) != partner.vat:
if (data.get('vat', partner.vat) or False) != partner.vat:
error['vat'] = 'error'
error_message.append(_('Changing VAT number is not allowed once invoices have been issued for your account. Please contact us directly for this operation.'))
if data.get('name', partner.name) != partner.name:
@@ -6,7 +6,7 @@
<field name="inherit_id" ref="website_sale.website_config_settings_view_form_inherit_website_sale"/>
<field name="arch" type="xml">
<div id="msg_delivery_method_setting" position="replace">
<div class="content-group">
<div class="content-group" attrs="{'invisible': [('module_website_sale_delivery', '=', False)]}">
<div class="mt16">
<button type="action" name="%(delivery.action_delivery_carrier_form)d" string="Delivery Methods" class="btn-link" icon="fa-arrow-right"/>
</div>
+1 -1
View File
@@ -6,7 +6,7 @@
<field name="inherit_id" ref="website.website_config_settings_view_form"/>
<field name="arch" type="xml">
<div id="msg_slides_install_setting" position="replace">
<div class="content-group" groups="base.group_no_one">
<div class="content-group" groups="base.group_no_one" attrs="{'invisible': [('module_website_slides', '=', False)]}">
<div class="row mt16">
<label class="col-md-3 o_light_label" for="website_slide_google_app_key"/>
<field name="website_slide_google_app_key" class="oe_inline"/>
@@ -6,7 +6,7 @@
<field name="inherit_id" ref="website.website_config_settings_view_form"/>
<field name="arch" type="xml">
<div id="msg_twitter_roller_install_setting" position="replace">
<div class="content-group">
<div class="content-group" attrs="{'invisible': [('module_website_twitter', '=', False)]}">
<div class="mt16">
<div class="row">
<label class="col-md-3 o_light_label" string="API Key" for="twitter_api_key"/>
+1 -1
View File
@@ -54,7 +54,7 @@ Business objects
by Odoo based on their configuration
Data files
XML or CSV files declaring metadata (views or workflows), configuration
XML or CSV files declaring metadata (views or reports), configuration
data (modules parameterization), demonstration data and more
Web controllers
-1
View File
@@ -17,7 +17,6 @@ Index: addons/openacademy/__manifest__.py
@@ -31,6 +31,7 @@
'views/openacademy.xml',
'views/partner.xml',
'views/session_workflow.xml',
+ 'views/session_board.xml',
'reports.xml',
],
+1 -1
View File
@@ -6,9 +6,9 @@ Index: addons/openacademy/__manifest__.py
--- addons.orig/openacademy/__manifest__.py 2014-08-29 08:39:43.975536806 +0200
+++ addons/openacademy/__manifest__.py 2014-08-29 08:39:52.000000000 +0200
@@ -31,6 +31,7 @@
'templates.xml',
'views/openacademy.xml',
'views/partner.xml',
'views/session_workflow.xml',
+ 'reports.xml',
],
# only loaded in demonstration mode
+59
View File
@@ -738,6 +738,65 @@ you to make your alias easily configurable from the record's form view.
res = super(BusinessExpense, self).message_new(msg, custom_values=defaults)
return res
.. _reference/mixins/mail/activities:
Activities tracking
-------------------
Activities are actions users have to take on a document like making a phone call
or organizing a meeting. Activities come with the mail module as they are
integrated in the Chatter but are *not bundled with mail.thread*. Activities
are records of the ``mail.activity`` class, which have a type (``mail.activity.type``),
name, description, scheduled time (among others). Pending activities are visible
above the message history in the chatter widget.
You can integrate activities using the ``mail.activity.mixin`` class on your object
and the specific widgets to display them (via the field ``activity_ids``) in the form
view and kanban view of your records (``mail_activity`` and ``kanban_activity``
widgets, respectively).
.. admonition:: Example
Organizing a business trip is a tedious process and tracking needed activities
like ordering plane tickets or a cab for the airport could be useful. To do so,
we will add the activities mixin on our model and display the next planned activities
in the message history of our trip.
.. code-block:: python
class BusinessTrip(models.Model):
_name = 'business.trip'
_inherit = ['mail.thread', 'mail.activity.mixin']
_description = 'Business Trip'
name = fields.Char()
# [...]
We modify the form view of our trips to display their next activites:
.. code-block:: xml
<record id="businness_trip_form" model="ir.ui.view">
<field name="name">business.trip.form</field>
<field name="model">business.trip</field>
<field name="arch" type="xml">
<form string="Business Trip">
<!-- Your usual form view goes here -->
<div class="oe_chatter">
<field name="message_follower_ids" widget="mail_followers"/>
<field name="activity_ids" widget="mail_activity"/>
<field name="message_ids" widget="mail_thread"/>
</div>
</form>
</field>
</record>
You can find concrete examples of integration in the following models:
* ``crm.lead`` in the CRM (*crm*) Application
* ``sale.order`` in the Sales (*sale*) Application
* ``project.task`` in the Project (*poject*) Application
.. _reference/mixins/website:
+3
View File
@@ -1,4 +1,7 @@
#!/usr/bin/env python
# set server timezone in UTC before time module imported
__import__('os').environ['TZ'] = 'UTC'
__import__('pkg_resources').declare_namespace('odoo.addons')
import odoo
+5 -8
View File
@@ -27,15 +27,12 @@ multi_process = False
#----------------------------------------------------------
# libc UTC hack
#----------------------------------------------------------
# Make sure the OpenERP server runs in UTC. This is especially necessary
# under Windows as under Linux it seems the real import of time is
# sufficiently deferred so that setting the TZ environment variable
# in odoo.cli.server was working.
# Make sure the OpenERP server runs in UTC.
import os
os.environ['TZ'] = 'UTC' # Set the timezone...
import time # ... *then* import time.
del os
del time
os.environ['TZ'] = 'UTC' # Set the timezone
import time
if hasattr(time, 'tzset'):
time.tzset()
#----------------------------------------------------------
# Shortcuts
+44 -14
View File
@@ -123,6 +123,31 @@ def get_view_arch_from_file(filename, xmlid):
_logger.warning("Could not find view arch definition in file '%s' for xmlid '%s'", filename, xmlid)
return None
def add_text_before(node, text):
""" Add text before ``node`` in its XML tree. """
if text is None:
return
prev = node.getprevious()
if prev is not None:
prev.tail = (prev.tail or "") + text
else:
parent = node.getparent()
parent.text = (parent.text or "") + text
def add_text_inside(node, text):
""" Add text inside ``node``. """
if text is None:
return
if len(node):
node[-1].tail = (node[-1].tail or "") + text
else:
node.text = (node.text or "") + text
def remove_element(node):
""" Remove ``node`` but not its tail, from its XML tree. """
add_text_before(node, node.tail)
node.getparent().remove(node)
xpath_utils = etree.FunctionNamespace(None)
xpath_utils['hasclass'] = _hasclass
@@ -555,21 +580,26 @@ actual arch.
node.set(attribute, value)
elif attribute in node.attrib:
del node.attrib[attribute]
else:
sib = node.getnext()
elif pos == 'inside':
add_text_inside(node, spec.text)
for child in spec:
if pos == 'inside':
node.append(child)
elif pos == 'after':
if sib is None:
node.addnext(child)
node = child
else:
sib.addprevious(child)
elif pos == 'before':
node.addprevious(child)
else:
self.raise_view_error(_("Invalid position attribute: '%s'") % pos, inherit_id)
node.append(child)
elif pos == 'after':
# add a sentinel element right after node, insert content of
# spec before the sentinel, then remove the sentinel element
sentinel = E.sentinel()
node.addnext(sentinel)
add_text_before(sentinel, spec.text)
for child in spec:
sentinel.addprevious(child)
remove_element(sentinel)
elif pos == 'before':
add_text_before(node, spec.text)
for child in spec:
node.addprevious(child)
else:
self.raise_view_error(_("Invalid position attribute: '%s'") % pos, inherit_id)
else:
attrs = ''.join([
' %s="%s"' % (attr, spec.get(attr))
+60
View File
@@ -736,6 +736,66 @@ class TestViews(ViewCase):
),
string="Replacement title", version="7.0"))
def test_view_inheritance_text_inside(self):
""" Test view inheritance when adding elements and text. """
view1 = self.View.create({
'name': "alpha",
'model': 'ir.ui.view',
'arch': '<form string="F">(<div/>)</form>',
})
view2 = self.View.create({
'name': "beta",
'model': 'ir.ui.view',
'inherit_id': view1.id,
'arch': '<div position="inside">a<p/>b<p/>c</div>',
})
view = self.View.with_context(check_view_ids=view2.ids).fields_view_get(view1.id)
self.assertEqual(view['type'], 'form')
self.assertEqual(
view['arch'],
'<form string="F">(<div>a<p/>b<p/>c</div>)</form>',
)
def test_view_inheritance_text_after(self):
""" Test view inheritance when adding elements and text. """
view1 = self.View.create({
'name': "alpha",
'model': 'ir.ui.view',
'arch': '<form string="F">(<div/>)</form>',
})
view2 = self.View.create({
'name': "beta",
'model': 'ir.ui.view',
'inherit_id': view1.id,
'arch': '<div position="after">a<p/>b<p/>c</div>',
})
view = self.View.with_context(check_view_ids=view2.ids).fields_view_get(view1.id)
self.assertEqual(view['type'], 'form')
self.assertEqual(
view['arch'],
'<form string="F">(<div/>a<p/>b<p/>c)</form>',
)
def test_view_inheritance_text_before(self):
""" Test view inheritance when adding elements and text. """
view1 = self.View.create({
'name': "alpha",
'model': 'ir.ui.view',
'arch': '<form string="F">(<div/>)</form>',
})
view2 = self.View.create({
'name': "beta",
'model': 'ir.ui.view',
'inherit_id': view1.id,
'arch': '<div position="before">a<p/>b<p/>c</div>',
})
view = self.View.with_context(check_view_ids=view2.ids).fields_view_get(view1.id)
self.assertEqual(view['type'], 'form')
self.assertEqual(
view['arch'],
'<form string="F">(a<p/>b<p/>c<div/>)</form>',
)
def test_view_inheritance_divergent_models(self):
view1 = self.View.create({
'name': "bob",
+3 -2
View File
@@ -1247,8 +1247,9 @@ class BaseModel(object):
"""
field = E.field(name=self._rec_name_fallback())
div = E.div(field, {'class': "oe_kanban_card oe_kanban_global_click"})
kanban_box = E.t(div, {'t-name': "kanban-box"})
content_div = E.div(field, {'class': "o_kanban_card_content"})
card_div = E.div(content_div, {'t-attf-class': "oe_kanban_card oe_kanban_global_click"})
kanban_box = E.t(card_div, {'t-name': "kanban-box"})
templates = E.templates(kanban_box)
return E.kanban(templates, string=self._description)
+5 -3
View File
@@ -364,7 +364,7 @@ def load_modules(db, force_demo=False, status=None, update_module=False):
for (model,) in cr.fetchall():
if model in registry:
env[model]._check_removed_columns(log=True)
else:
elif _logger.isEnabledFor(logging.INFO): # more an info that a warning...
_logger.warning("Model %s is declared but cannot be loaded! (Perhaps a module was partially removed or renamed)", model)
# Cleanup orphan records
@@ -402,8 +402,10 @@ def load_modules(db, force_demo=False, status=None, update_module=False):
if update_module:
View = env['ir.ui.view']
for model in registry:
if not View._validate_custom_views(model):
_logger.warning('invalid custom view(s) for model %s', model)
try:
View._validate_custom_views(model)
except Exception as e:
_logger.warning('invalid custom view(s) for model %s: %s', model, tools.ustr(e))
if report.failures:
_logger.error('At least one test failed when loading the modules.')