[MERGE] with trunk
bzr revid: nco@tinyerp.com-20121210070332-6h5qnlcf4whjv7p5
This commit is contained in:
commit
480e116b40
|
@ -1,19 +0,0 @@
|
|||
#! /usr/bin/env python
|
||||
# -*- coding: UTF-8 -*-
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
if __name__ == "__main__":
|
||||
print '-' * 70
|
||||
print "DEPRECATED: you are starting the OpenERP server with its old path,"
|
||||
print "please use the new executable (available in the parent directory)."
|
||||
print '-' * 70
|
||||
|
||||
# Change to the parent directory ...
|
||||
os.chdir(os.path.normpath(os.path.dirname(__file__)))
|
||||
os.chdir('..')
|
||||
# ... and execute the new executable.
|
||||
os.execv('openerp-server', sys.argv)
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
|
@ -23,6 +23,7 @@ Depends:
|
|||
python-libxslt1,
|
||||
python-lxml,
|
||||
python-mako,
|
||||
python-mock,
|
||||
python-openid,
|
||||
python-psutil,
|
||||
python-psycopg2,
|
||||
|
@ -33,6 +34,7 @@ Depends:
|
|||
python-reportlab,
|
||||
python-simplejson,
|
||||
python-tz,
|
||||
python-unittest2,
|
||||
python-vatnumber,
|
||||
python-vobject,
|
||||
python-webdav,
|
||||
|
|
|
@ -40,7 +40,6 @@ import service
|
|||
import sql_db
|
||||
import test
|
||||
import tools
|
||||
import wizard
|
||||
import workflow
|
||||
# backward compatilbility
|
||||
# TODO: This is for the web addons, can be removed later.
|
||||
|
|
|
@ -7,14 +7,14 @@ msgstr ""
|
|||
"Project-Id-Version: OpenERP Server 5.0.4\n"
|
||||
"Report-Msgid-Bugs-To: support@openerp.com\n"
|
||||
"POT-Creation-Date: 2012-12-03 16:01+0000\n"
|
||||
"PO-Revision-Date: 2012-08-20 15:29+0000\n"
|
||||
"Last-Translator: OpenERP Administrators <Unknown>\n"
|
||||
"PO-Revision-Date: 2012-12-09 23:26+0000\n"
|
||||
"Last-Translator: lambdasoftware <development@lambdasoftware.net>\n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Launchpad-Export-Date: 2012-12-04 05:00+0000\n"
|
||||
"X-Generator: Launchpad (build 16335)\n"
|
||||
"X-Launchpad-Export-Date: 2012-12-10 04:36+0000\n"
|
||||
"X-Generator: Launchpad (build 16341)\n"
|
||||
|
||||
#. module: base
|
||||
#: model:ir.module.module,description:base.module_account_check_writing
|
||||
|
@ -24,6 +24,10 @@ msgid ""
|
|||
"================================================\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"\n"
|
||||
"Módulo para la emisión e impresión de cheques\n"
|
||||
"================================================\n"
|
||||
" "
|
||||
|
||||
#. module: base
|
||||
#: model:res.country,name:base.sh
|
||||
|
@ -59,7 +63,7 @@ msgstr "Estructura de la vista"
|
|||
#. module: base
|
||||
#: model:ir.module.module,summary:base.module_sale_stock
|
||||
msgid "Quotation, Sale Orders, Delivery & Invoicing Control"
|
||||
msgstr ""
|
||||
msgstr "Presupuesto, órdenes de venta, entrega y control de facturación"
|
||||
|
||||
#. module: base
|
||||
#: selection:ir.sequence,implementation:0
|
||||
|
@ -88,7 +92,7 @@ msgstr ""
|
|||
#. module: base
|
||||
#: model:ir.module.module,summary:base.module_point_of_sale
|
||||
msgid "Touchscreen Interface for Shops"
|
||||
msgstr ""
|
||||
msgstr "Interfaz de pantalla táctil para tiendas"
|
||||
|
||||
#. module: base
|
||||
#: model:ir.module.module,shortdesc:base.module_l10n_in_hr_payroll
|
||||
|
@ -140,7 +144,7 @@ msgstr ""
|
|||
#. module: base
|
||||
#: help:res.partner,employee:0
|
||||
msgid "Check this box if this contact is an Employee."
|
||||
msgstr ""
|
||||
msgstr "Marque si el contacto es un empleado"
|
||||
|
||||
#. module: base
|
||||
#: help:ir.model.fields,domain:0
|
||||
|
@ -171,7 +175,7 @@ msgstr "Ventana destino"
|
|||
#. module: base
|
||||
#: field:ir.actions.report.xml,report_rml:0
|
||||
msgid "Main Report File Path"
|
||||
msgstr ""
|
||||
msgstr "Ruta de archivo del informe principal"
|
||||
|
||||
#. module: base
|
||||
#: model:ir.module.module,shortdesc:base.module_sale_analytic_plans
|
||||
|
@ -269,7 +273,7 @@ msgstr "creado."
|
|||
#. module: base
|
||||
#: field:ir.actions.report.xml,report_xsl:0
|
||||
msgid "XSL Path"
|
||||
msgstr ""
|
||||
msgstr "Ruta XSL"
|
||||
|
||||
#. module: base
|
||||
#: model:ir.module.module,shortdesc:base.module_l10n_tr
|
||||
|
@ -295,7 +299,7 @@ msgstr "Inuktitut / ᐃᓄᒃᑎᑐᑦ"
|
|||
#. module: base
|
||||
#: model:res.groups,name:base.group_multi_currency
|
||||
msgid "Multi Currencies"
|
||||
msgstr ""
|
||||
msgstr "Multidivisas"
|
||||
|
||||
#. module: base
|
||||
#: model:ir.module.module,description:base.module_l10n_cl
|
||||
|
@ -319,6 +323,7 @@ msgid ""
|
|||
"The internal user that is in charge of communicating with this contact if "
|
||||
"any."
|
||||
msgstr ""
|
||||
"El usuario interno encargado de comunicarse con este contacto si lo hubiese."
|
||||
|
||||
#. module: base
|
||||
#: view:res.partner:0
|
||||
|
@ -531,12 +536,12 @@ msgstr ""
|
|||
#. module: base
|
||||
#: field:ir.model.relation,name:0
|
||||
msgid "Relation Name"
|
||||
msgstr ""
|
||||
msgstr "Nombre de relación"
|
||||
|
||||
#. module: base
|
||||
#: view:ir.rule:0
|
||||
msgid "Create Access Right"
|
||||
msgstr ""
|
||||
msgstr "Otorge derecho de acceso"
|
||||
|
||||
#. module: base
|
||||
#: model:res.country,name:base.tv
|
||||
|
@ -576,7 +581,7 @@ msgstr ""
|
|||
#. module: base
|
||||
#: view:workflow.transition:0
|
||||
msgid "Workflow Transition"
|
||||
msgstr ""
|
||||
msgstr "Transición del flujo de trabajo"
|
||||
|
||||
#. module: base
|
||||
#: model:res.country,name:base.gf
|
||||
|
@ -699,7 +704,7 @@ msgstr "Colombia"
|
|||
#. module: base
|
||||
#: model:res.partner.title,name:base.res_partner_title_mister
|
||||
msgid "Mister"
|
||||
msgstr ""
|
||||
msgstr "Señor"
|
||||
|
||||
#. module: base
|
||||
#: help:res.country,code:0
|
||||
|
@ -728,7 +733,7 @@ msgstr "Sin traducir"
|
|||
#. module: base
|
||||
#: view:ir.mail_server:0
|
||||
msgid "Outgoing Mail Server"
|
||||
msgstr ""
|
||||
msgstr "Servidor de correo saliente"
|
||||
|
||||
#. module: base
|
||||
#: help:ir.actions.act_window,context:0
|
||||
|
@ -922,7 +927,7 @@ msgstr "Sueco / svenska"
|
|||
#: field:base.language.export,name:0
|
||||
#: field:ir.attachment,datas_fname:0
|
||||
msgid "File Name"
|
||||
msgstr ""
|
||||
msgstr "Nombre del archivo"
|
||||
|
||||
#. module: base
|
||||
#: model:res.country,name:base.rs
|
||||
|
@ -1019,7 +1024,7 @@ msgstr "Preferencias de email"
|
|||
#: code:addons/base/ir/ir_fields.py:196
|
||||
#, python-format
|
||||
msgid "'%s' does not seem to be a valid date for field '%%(field)s'"
|
||||
msgstr ""
|
||||
msgstr "'%s' no parece ser una fecha valida para el campo '%%(field)s'"
|
||||
|
||||
#. module: base
|
||||
#: view:res.partner:0
|
||||
|
@ -1151,7 +1156,7 @@ msgstr "Usuarios google"
|
|||
#. module: base
|
||||
#: model:ir.module.module,shortdesc:base.module_fleet
|
||||
msgid "Fleet Management"
|
||||
msgstr ""
|
||||
msgstr "Gestión de flotas"
|
||||
|
||||
#. module: base
|
||||
#: help:ir.server.object.lines,value:0
|
||||
|
@ -2716,7 +2721,7 @@ msgstr "Nombre de acceso rápido"
|
|||
#. module: base
|
||||
#: field:res.partner,contact_address:0
|
||||
msgid "Complete Address"
|
||||
msgstr ""
|
||||
msgstr "Dirección completa"
|
||||
|
||||
#. module: base
|
||||
#: help:ir.actions.act_window,limit:0
|
||||
|
@ -2788,7 +2793,7 @@ msgstr "ID de registro"
|
|||
#. module: base
|
||||
#: view:ir.filters:0
|
||||
msgid "My Filters"
|
||||
msgstr ""
|
||||
msgstr "Mis filtros"
|
||||
|
||||
#. module: base
|
||||
#: field:ir.actions.server,email:0
|
||||
|
@ -2874,7 +2879,7 @@ msgstr "Responsable"
|
|||
#: code:addons/base/ir/ir_model.py:718
|
||||
#, python-format
|
||||
msgid "Sorry, you are not allowed to access this document."
|
||||
msgstr ""
|
||||
msgstr "Lo siento, no está autorizado para acceder a este documento."
|
||||
|
||||
#. module: base
|
||||
#: model:res.country,name:base.py
|
||||
|
@ -2889,7 +2894,7 @@ msgstr "Fiji"
|
|||
#. module: base
|
||||
#: view:ir.actions.report.xml:0
|
||||
msgid "Report Xml"
|
||||
msgstr ""
|
||||
msgstr "Informe Xml"
|
||||
|
||||
#. module: base
|
||||
#: model:ir.module.module,description:base.module_purchase
|
||||
|
@ -2960,7 +2965,7 @@ msgstr "Hededado"
|
|||
#: code:addons/base/ir/ir_fields.py:147
|
||||
#, python-format
|
||||
msgid "yes"
|
||||
msgstr ""
|
||||
msgstr "Sí"
|
||||
|
||||
#. module: base
|
||||
#: field:ir.model.fields,serialization_field_id:0
|
||||
|
@ -3051,7 +3056,7 @@ msgstr ""
|
|||
#. module: base
|
||||
#: model:ir.module.module,shortdesc:base.module_web_linkedin
|
||||
msgid "LinkedIn Integration"
|
||||
msgstr ""
|
||||
msgstr "Integración con LinkedIn"
|
||||
|
||||
#. module: base
|
||||
#: code:addons/orm.py:2021
|
||||
|
@ -3365,7 +3370,7 @@ msgstr "Calendario"
|
|||
#. module: base
|
||||
#: model:ir.module.category,name:base.module_category_knowledge_management
|
||||
msgid "Knowledge"
|
||||
msgstr ""
|
||||
msgstr "Conocimiento"
|
||||
|
||||
#. module: base
|
||||
#: field:workflow.activity,signal_send:0
|
||||
|
@ -3530,7 +3535,7 @@ msgstr "workflow.actividad"
|
|||
#. module: base
|
||||
#: view:base.language.export:0
|
||||
msgid "Export Complete"
|
||||
msgstr ""
|
||||
msgstr "Exportación completada"
|
||||
|
||||
#. module: base
|
||||
#: help:ir.ui.view_sc,res_id:0
|
||||
|
@ -3559,7 +3564,7 @@ msgstr "Finlandés / Suomi"
|
|||
#. module: base
|
||||
#: view:ir.config_parameter:0
|
||||
msgid "System Properties"
|
||||
msgstr ""
|
||||
msgstr "Propiedades de sistema"
|
||||
|
||||
#. module: base
|
||||
#: field:ir.sequence,prefix:0
|
||||
|
@ -3616,7 +3621,7 @@ msgstr ""
|
|||
#. module: base
|
||||
#: field:base.language.export,modules:0
|
||||
msgid "Modules To Export"
|
||||
msgstr ""
|
||||
msgstr "Módulos a exportar"
|
||||
|
||||
#. module: base
|
||||
#: model:res.country,name:base.mt
|
||||
|
@ -3719,7 +3724,7 @@ msgstr "Antártida"
|
|||
#. module: base
|
||||
#: view:res.partner:0
|
||||
msgid "Persons"
|
||||
msgstr ""
|
||||
msgstr "Personas"
|
||||
|
||||
#. module: base
|
||||
#: view:base.language.import:0
|
||||
|
@ -3898,7 +3903,7 @@ msgstr "Tayiko / اردو"
|
|||
#: code:addons/orm.py:3870
|
||||
#, python-format
|
||||
msgid "Access Denied"
|
||||
msgstr ""
|
||||
msgstr "Acceso denegado"
|
||||
|
||||
#. module: base
|
||||
#: field:res.company,name:0
|
||||
|
@ -3991,7 +3996,7 @@ msgstr "%x - Representación apropiada de fecha."
|
|||
#. module: base
|
||||
#: view:res.partner:0
|
||||
msgid "Tag"
|
||||
msgstr ""
|
||||
msgstr "Etiqueta"
|
||||
|
||||
#. module: base
|
||||
#: view:res.lang:0
|
||||
|
@ -4043,7 +4048,7 @@ msgstr ""
|
|||
#. module: base
|
||||
#: model:res.country,name:base.sk
|
||||
msgid "Slovakia"
|
||||
msgstr ""
|
||||
msgstr "Eslovaquia"
|
||||
|
||||
#. module: base
|
||||
#: model:res.country,name:base.nr
|
||||
|
@ -4594,7 +4599,7 @@ msgstr ""
|
|||
#. module: base
|
||||
#: field:ir.module.module,latest_version:0
|
||||
msgid "Installed Version"
|
||||
msgstr ""
|
||||
msgstr "Versión instalada"
|
||||
|
||||
#. module: base
|
||||
#: field:ir.module.module,license:0
|
||||
|
@ -4890,7 +4895,7 @@ msgstr "Flujos"
|
|||
#. module: base
|
||||
#: model:ir.ui.menu,name:base.next_id_73
|
||||
msgid "Purchase"
|
||||
msgstr ""
|
||||
msgstr "Compra"
|
||||
|
||||
#. module: base
|
||||
#: selection:base.language.install,lang:0
|
||||
|
@ -5049,7 +5054,7 @@ msgstr "Establecer a NULL"
|
|||
#. module: base
|
||||
#: view:res.users:0
|
||||
msgid "Save"
|
||||
msgstr ""
|
||||
msgstr "Guardar"
|
||||
|
||||
#. module: base
|
||||
#: field:ir.actions.report.xml,report_xml:0
|
||||
|
@ -5243,7 +5248,7 @@ msgstr "Tasas"
|
|||
#. module: base
|
||||
#: model:ir.module.module,shortdesc:base.module_email_template
|
||||
msgid "Email Templates"
|
||||
msgstr ""
|
||||
msgstr "Plantillas de correo electrónico"
|
||||
|
||||
#. module: base
|
||||
#: model:res.country,name:base.sy
|
||||
|
@ -5392,7 +5397,7 @@ msgstr ""
|
|||
#. module: base
|
||||
#: view:ir.rule:0
|
||||
msgid "Write Access Right"
|
||||
msgstr ""
|
||||
msgstr "Permiso de escritura"
|
||||
|
||||
#. module: base
|
||||
#: model:ir.actions.act_window,help:base.action_res_groups
|
||||
|
@ -5427,7 +5432,7 @@ msgstr "Historial"
|
|||
#. module: base
|
||||
#: model:res.country,name:base.im
|
||||
msgid "Isle of Man"
|
||||
msgstr ""
|
||||
msgstr "Isla de Man"
|
||||
|
||||
#. module: base
|
||||
#: help:ir.actions.client,res_model:0
|
||||
|
@ -6104,7 +6109,7 @@ msgstr "Nombre del recurso"
|
|||
#. module: base
|
||||
#: field:res.partner,is_company:0
|
||||
msgid "Is a Company"
|
||||
msgstr ""
|
||||
msgstr "¿Es una empresa?"
|
||||
|
||||
#. module: base
|
||||
#: selection:ir.cron,interval_type:0
|
||||
|
@ -6184,7 +6189,7 @@ msgstr ""
|
|||
#. module: base
|
||||
#: sql_constraint:ir.filters:0
|
||||
msgid "Filter names must be unique"
|
||||
msgstr ""
|
||||
msgstr "Los nombres de filtro deben ser únicos"
|
||||
|
||||
#. module: base
|
||||
#: help:multi_company.default,object_id:0
|
||||
|
@ -6199,7 +6204,7 @@ msgstr ""
|
|||
#. module: base
|
||||
#: field:ir.filters,is_default:0
|
||||
msgid "Default filter"
|
||||
msgstr ""
|
||||
msgstr "Filtro por defecto"
|
||||
|
||||
#. module: base
|
||||
#: report:ir.module.reference:0
|
||||
|
@ -6927,7 +6932,7 @@ msgstr ""
|
|||
#. module: base
|
||||
#: model:ir.module.module,shortdesc:base.module_auth_anonymous
|
||||
msgid "Anonymous"
|
||||
msgstr ""
|
||||
msgstr "Anónimo"
|
||||
|
||||
#. module: base
|
||||
#: model:ir.module.module,description:base.module_base_import
|
||||
|
@ -7434,7 +7439,7 @@ msgstr ""
|
|||
#. module: base
|
||||
#: model:res.partner.category,name:base.res_partner_category_14
|
||||
msgid "Manufacturer"
|
||||
msgstr ""
|
||||
msgstr "Fabricante"
|
||||
|
||||
#. module: base
|
||||
#: help:res.users,company_id:0
|
||||
|
@ -8050,7 +8055,7 @@ msgstr "Francés (CH) / Français (CH)"
|
|||
#. module: base
|
||||
#: model:res.partner.category,name:base.res_partner_category_13
|
||||
msgid "Distributor"
|
||||
msgstr ""
|
||||
msgstr "Distribuidor"
|
||||
|
||||
#. module: base
|
||||
#: help:ir.actions.server,subject:0
|
||||
|
@ -15506,7 +15511,7 @@ msgstr ""
|
|||
#. module: base
|
||||
#: view:res.partner:0
|
||||
msgid "Internal Notes"
|
||||
msgstr ""
|
||||
msgstr "Notas internas"
|
||||
|
||||
#. module: base
|
||||
#: selection:res.partner.address,type:0
|
||||
|
@ -15548,7 +15553,7 @@ msgstr "Empresas: "
|
|||
#. module: base
|
||||
#: view:res.partner:0
|
||||
msgid "Is a Company?"
|
||||
msgstr ""
|
||||
msgstr "¿Es una empresa?"
|
||||
|
||||
#. module: base
|
||||
#: code:addons/base/res/res_company.py:159
|
||||
|
@ -15570,7 +15575,7 @@ msgstr "Crear objeto"
|
|||
#. module: base
|
||||
#: model:res.country,name:base.ss
|
||||
msgid "South Sudan"
|
||||
msgstr ""
|
||||
msgstr "Sudán del Sur"
|
||||
|
||||
#. module: base
|
||||
#: field:ir.filters,context:0
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -7,14 +7,14 @@ msgstr ""
|
|||
"Project-Id-Version: OpenERP Server 5.0.4\n"
|
||||
"Report-Msgid-Bugs-To: support@openerp.com\n"
|
||||
"POT-Creation-Date: 2012-12-03 16:01+0000\n"
|
||||
"PO-Revision-Date: 2012-12-03 11:15+0000\n"
|
||||
"PO-Revision-Date: 2012-12-06 10:07+0000\n"
|
||||
"Last-Translator: Quentin THEURET <Unknown>\n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Launchpad-Export-Date: 2012-12-04 04:55+0000\n"
|
||||
"X-Generator: Launchpad (build 16335)\n"
|
||||
"X-Launchpad-Export-Date: 2012-12-07 04:34+0000\n"
|
||||
"X-Generator: Launchpad (build 16341)\n"
|
||||
|
||||
#. module: base
|
||||
#: model:ir.module.module,description:base.module_account_check_writing
|
||||
|
@ -26,7 +26,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"\n"
|
||||
"Module pour l'écriture et l'impression de chèques.\n"
|
||||
"================================================\n"
|
||||
"==================================================\n"
|
||||
" "
|
||||
|
||||
#. module: base
|
||||
|
@ -129,10 +129,10 @@ msgid ""
|
|||
msgstr ""
|
||||
"\n"
|
||||
"Ce module ajoute des fabricants et des attributs sur le formulaire produit.\n"
|
||||
"==========================================================\n"
|
||||
"===========================================================================\n"
|
||||
"\n"
|
||||
"Vous pouvez maintenant définir pour un produit:\n"
|
||||
"-------------------------------------------------------------------\n"
|
||||
"-----------------------------------------------\n"
|
||||
" * un fabricant\n"
|
||||
" * le nom du produit chez le fabricant\n"
|
||||
" * le code produit chez le fabricant\n"
|
||||
|
@ -153,7 +153,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"\n"
|
||||
"Ce module ajoute les utilisateurs Google dans res.user.\n"
|
||||
"========================================\n"
|
||||
"=======================================================\n"
|
||||
|
||||
#. module: base
|
||||
#: help:res.partner,employee:0
|
||||
|
@ -212,7 +212,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"\n"
|
||||
"Génère vos factures à partir des dépenses et des feuilles de temps.\n"
|
||||
"=====================================================\n"
|
||||
"===================================================================\n"
|
||||
"\n"
|
||||
"Module pour générer les factures basées sur les coûts (RH, dépenses…).\n"
|
||||
"\n"
|
||||
|
@ -253,8 +253,8 @@ msgid ""
|
|||
"* Opportunities by Stage (graph)\n"
|
||||
msgstr ""
|
||||
"\n"
|
||||
"Le module générique de Gestion de la Relation Client d'OpenERP\n"
|
||||
"===================================================\n"
|
||||
"Le module générique de Gestion de la Relation Client d'OpenERP (CRM)\n"
|
||||
"====================================================================\n"
|
||||
"Cette application permet à un groupe de personnes de gérer intelligemment et "
|
||||
"efficacement les pistes, les opportunités, les réunions et les appels "
|
||||
"téléphoniques.\n"
|
||||
|
@ -277,8 +277,8 @@ msgstr ""
|
|||
"automatiquement à l'équipe appropriée et s'assurer que les correspondances "
|
||||
"futures se fassent au bon endroit.\n"
|
||||
"\n"
|
||||
"Le tableau de bord GRC inclura :\n"
|
||||
"--------------------------------------------\n"
|
||||
"Le tableau de bord CRM inclura :\n"
|
||||
"--------------------------------\n"
|
||||
"* Le revenu planifié par étape et par utilisateur (graphique)\n"
|
||||
"* Les opportunités par étape (graphique)\n"
|
||||
|
||||
|
@ -364,7 +364,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"\n"
|
||||
"Comptabilité chilienne et mise en place des taxes pour le Chili\n"
|
||||
"================================================\n"
|
||||
"===============================================================\n"
|
||||
"Plan comptable chilien et taxes en accord avec la législation en vigueur.\n"
|
||||
"\n"
|
||||
" "
|
||||
|
@ -418,10 +418,10 @@ msgid ""
|
|||
msgstr ""
|
||||
"\n"
|
||||
"Ajoute des informations de date additionnelles au commandes de vente.\n"
|
||||
"==========================================================\n"
|
||||
"=====================================================================\n"
|
||||
"\n"
|
||||
"Vous pouvez ajouter les dates suivantes à un bon de commande:\n"
|
||||
"-----------------------------------------------------------\n"
|
||||
"-------------------------------------------------------------\n"
|
||||
" * Date de la demande\n"
|
||||
" * Date de confirmation\n"
|
||||
" * Date Effective\n"
|
||||
|
@ -486,7 +486,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"\n"
|
||||
"Installateur invisible pour la base de connaissances\n"
|
||||
"==========================================\n"
|
||||
"====================================================\n"
|
||||
"\n"
|
||||
"Rend disponible la configuration de l’application des connaissances depuis "
|
||||
"l'endroit où vous\n"
|
||||
|
@ -514,7 +514,7 @@ msgstr ""
|
|||
"Permet d'ajouter des méthodes de livraison dans les commandes de vente et "
|
||||
"dans les livraisons.\n"
|
||||
"============================================================================="
|
||||
"\n"
|
||||
"=================\n"
|
||||
"\n"
|
||||
"Vous pouvez définir vos propres transporteurs et grilles de prix de "
|
||||
"livraison. Quand vous créez \n"
|
||||
|
@ -701,7 +701,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"\n"
|
||||
"Module de définition des comptes analytiques.\n"
|
||||
"========================================\n"
|
||||
"=============================================\n"
|
||||
"\n"
|
||||
"Dans OpenERP, les comptes analytiques sont liés à des comptes financiers "
|
||||
"mais sont traités\n"
|
||||
|
@ -734,14 +734,14 @@ msgid ""
|
|||
msgstr ""
|
||||
"\n"
|
||||
"Gestion et organisation d'événements\n"
|
||||
"=================================\n"
|
||||
"====================================\n"
|
||||
"\n"
|
||||
"Le module 'event' vous permet d'organiser efficacement des événements et "
|
||||
"toutes les tâches liées : planification, suivi des inscriptions,\n"
|
||||
"des présences, etc.\n"
|
||||
"\n"
|
||||
"Fonctionnalités clés\n"
|
||||
"----------------------------\n"
|
||||
"--------------------\n"
|
||||
"* Gestion des événements et des inscriptions\n"
|
||||
"* Utilisation d'e-mails pour automatiquement confirmer et envoyer des "
|
||||
"accusés de réception pour chaque inscription à un événement\n"
|
||||
|
@ -941,10 +941,10 @@ msgid ""
|
|||
msgstr ""
|
||||
"\n"
|
||||
"Comptabilité et gestion financière\n"
|
||||
"================================\n"
|
||||
"==================================\n"
|
||||
"\n"
|
||||
"Le module de comptabilité et de finance couvre :\n"
|
||||
"---------------------------------------------------------------------\n"
|
||||
"------------------------------------------------\n"
|
||||
" * Comptabilité générale\n"
|
||||
" * Comptabilité de coût / comptabilité analytique\n"
|
||||
" * Comptabilité de tiers\n"
|
||||
|
@ -955,8 +955,7 @@ msgstr ""
|
|||
" * Lettrages par partenaire\n"
|
||||
"\n"
|
||||
"Il créé également un tableau de bord pour les comptables qui inclut :\n"
|
||||
"-----------------------------------------------------------------------------"
|
||||
"------------------\n"
|
||||
"---------------------------------------------------------------------\n"
|
||||
" * Une liste des factures clients à approuver\n"
|
||||
" * L'analyse de la société\n"
|
||||
" * Un graphe de la trésorerie\n"
|
||||
|
@ -1008,7 +1007,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"\n"
|
||||
"Module LinkedIn pour OpenERP Web.\n"
|
||||
"===============================\n"
|
||||
"=================================\n"
|
||||
"Ce module fournit l'intégration de LinkedIn avec OpenERP.\n"
|
||||
" "
|
||||
|
||||
|
@ -1153,6 +1152,33 @@ msgid ""
|
|||
"also possible in order to automatically create a meeting when a holiday "
|
||||
"request is accepted by setting up a type of meeting in Leave Type.\n"
|
||||
msgstr ""
|
||||
"\n"
|
||||
"Gère les absences et les demandes de congés\n"
|
||||
"===========================================\n"
|
||||
"\n"
|
||||
"Cette application contrôle la programmation des congés de votre société. Il "
|
||||
"permet aux employés de faire des demandes de congés. Ensuite, les managers "
|
||||
"peuvent examiner les demandes et les approuver ou les rejeter. De cette "
|
||||
"façon, vous pouvez contrôler le planning de l'ensemble des congés de votre "
|
||||
"société ou de votre département.\n"
|
||||
"\n"
|
||||
"Vous pouvez configurer plusieurs sortes d'absences (maladie, congés payés, "
|
||||
"congés sans soldes…) et allouer rapidement ces absences à un employé ou un "
|
||||
"département en utilisant les demandes de congés. Un employée peut aussi "
|
||||
"faire une demande de congés pour plus de jours en ajoutant une allocation. "
|
||||
"Cela va augmenter le total des jours disponibles pour ce type de congés (si "
|
||||
"la demande est accepté).\n"
|
||||
"\n"
|
||||
"Vous pouvez suivre les absences de différentes façons grâce aux rapports "
|
||||
"suivants : \n"
|
||||
"\n"
|
||||
"* Résumé des absences\n"
|
||||
"* Absences par département\n"
|
||||
"* Analyse des absences\n"
|
||||
"\n"
|
||||
"Une synchronisation avec les agendas internes (Réunions du module CRM) est "
|
||||
"aussi possible dans le but de créer automatiquement une réunion quand une "
|
||||
"demande de congés est accepté en ajoutant une type de réunion Absence.\n"
|
||||
|
||||
#. module: base
|
||||
#: selection:base.language.install,lang:0
|
||||
|
@ -1257,6 +1283,13 @@ msgid ""
|
|||
" * the Tax Code Chart for Luxembourg\n"
|
||||
" * the main taxes used in Luxembourg"
|
||||
msgstr ""
|
||||
"\n"
|
||||
"Ceci est le module de base pour gérer le plan de comptes pour le Luxembourg\n"
|
||||
"===========================================================================\n"
|
||||
"\n"
|
||||
" * le plan de compte KLUWER\n"
|
||||
" * le plan de codes de taxes pour le Luxembourg\n"
|
||||
" * les taxes principales utilisées au Luxembourg"
|
||||
|
||||
#. module: base
|
||||
#: model:res.country,name:base.om
|
||||
|
@ -1281,7 +1314,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"\n"
|
||||
"Module de pointage des heures pour les employés.\n"
|
||||
"==========================================\n"
|
||||
"================================================\n"
|
||||
"\n"
|
||||
"Comptabilise les temps de présence des employés sur la base des\n"
|
||||
"pointages (Entrée / Sorties) réalisés.\n"
|
||||
|
@ -1725,7 +1758,7 @@ msgstr ""
|
|||
"dans la configuration du serveur.\n"
|
||||
"\n"
|
||||
"Paramètres de configuration du serveur :\n"
|
||||
"-------------------------------\n"
|
||||
"----------------------------------------\n"
|
||||
"[webdav]:\n"
|
||||
"+++++++++ \n"
|
||||
" * enable = True ; Sert le webdav à travers les serveurs http(s)\n"
|
||||
|
@ -1801,7 +1834,7 @@ msgstr ""
|
|||
"Ce module ajoute un menu pour les réclamations et les fonctionnalités pour "
|
||||
"votre portail si les modules claim et portal sont installés.\n"
|
||||
"============================================================================="
|
||||
"=======================================\n"
|
||||
"==========================================================\n"
|
||||
" "
|
||||
|
||||
#. module: base
|
||||
|
@ -1895,7 +1928,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"\n"
|
||||
"Le module de base pour gérer les repas.\n"
|
||||
"====================================\n"
|
||||
"=======================================\n"
|
||||
"\n"
|
||||
"Des sociétés commandent des sandwiches, des pizzas et d'autres repas, à des "
|
||||
"fournisseurs, pour leurs employés pour leur offrir plus de commodités.\n"
|
||||
|
@ -2114,7 +2147,7 @@ msgstr ""
|
|||
"Ce module fournit le plan comptable standard pour l'Autriche qui est basé "
|
||||
"sur le modèle BMF.gv.at.\n"
|
||||
"============================================================================="
|
||||
"======================== \n"
|
||||
"===================== \n"
|
||||
"Veuillez garder à l'esprit que vous devez le revoir et l'adapter avec votre "
|
||||
"comptable avant de l'utiliser dans un environnement de production.\n"
|
||||
|
||||
|
@ -2137,7 +2170,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"\n"
|
||||
"Droits d'accès à la comptabilité\n"
|
||||
"==========================\n"
|
||||
"================================\n"
|
||||
"Cela donne à l'administrateur les droits d'accès à toutes les "
|
||||
"fonctionnalités comptables telles que les mouvements comptables et le plan "
|
||||
"de comptes.\n"
|
||||
|
@ -2234,7 +2267,7 @@ msgstr ""
|
|||
"Ce module ajoute les outils de partage générique pour votre base de données "
|
||||
"OpenERP actuelle.\n"
|
||||
"============================================================================="
|
||||
"============\n"
|
||||
"================\n"
|
||||
"\n"
|
||||
"Il ajoute spécifiquement un bouton 'Partager' qui est disponible dans le "
|
||||
"client web pour\n"
|
||||
|
@ -2342,7 +2375,8 @@ msgstr ""
|
|||
"\n"
|
||||
"Synchronisation les entrées dans les tâches avec les entrées dans les "
|
||||
"feuilles de temps.\n"
|
||||
"=========================================================================\n"
|
||||
"============================================================================="
|
||||
"===========\n"
|
||||
"\n"
|
||||
"Ce module vous permet de transférer les entrées sous les tâches définies "
|
||||
"pour la gestion de\n"
|
||||
|
@ -2421,7 +2455,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"\n"
|
||||
"Module de base pour gérer le plan comptable pour l'Équateur dans OpenERP.\n"
|
||||
"==============================================================\n"
|
||||
"=========================================================================\n"
|
||||
"\n"
|
||||
"Module pour la comptabilité équatorienne\n"
|
||||
" "
|
||||
|
@ -2472,13 +2506,13 @@ msgid ""
|
|||
msgstr ""
|
||||
"\n"
|
||||
"Gestion des devis et des commandes de vente\n"
|
||||
"=========================================\n"
|
||||
"===========================================\n"
|
||||
"\n"
|
||||
"Ce module fait le lien entre les ventes et les applications de gestion des "
|
||||
"entrepôts.\n"
|
||||
"\n"
|
||||
"Préférences\n"
|
||||
"-------------------\n"
|
||||
"-----------\n"
|
||||
"* Expédition : Choix de la livraison en une fois ou une livraison partielle\n"
|
||||
"* Facturation : Choisir comment les factures vont être payées\n"
|
||||
"* Incoterms : International Commercial terms\n"
|
||||
|
@ -2677,6 +2711,29 @@ msgid ""
|
|||
"* Maximal difference between timesheet and attendances\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"\n"
|
||||
"Enregistrer et valider facilement les feuilles de temps et les présences\n"
|
||||
"========================================================================\n"
|
||||
"\n"
|
||||
"Cette application fournie un nouvel écran vous permettant de gérer à la fois "
|
||||
"les présences (entrées/sorties) et l'enregistrement du travail (feuille de "
|
||||
"temps) par période. Les saisies dans la feuille de temps sont faites par les "
|
||||
"employés chaque jour. À la fin de la période définie, les employés valident "
|
||||
"leurs feuilles de temps et le responsable doit approuver les saisies de son "
|
||||
"équipe. Les périodes sont définis dans le formulaire de la société et vous "
|
||||
"pouvez les paramétrer sur une base mensuelle ou hebdomadaire.\n"
|
||||
"\n"
|
||||
"Le processus complet de la validation des feuilles de temps est :\n"
|
||||
"-----------------------------------------------------------------\n"
|
||||
"* Feuille en brouillon\n"
|
||||
"* Confirmation de la feuille à la fin de la période par les employées\n"
|
||||
"* Validation par le responsable du projet\n"
|
||||
"\n"
|
||||
"La validation peut être paramétrée dans la société :\n"
|
||||
"----------------------------------------------------\n"
|
||||
"* Durée de la période (jour, semaine, mois)\n"
|
||||
"* Différence maximale entre la feuille de temps et les présences\n"
|
||||
" "
|
||||
|
||||
#. module: base
|
||||
#: code:addons/base/ir/ir_fields.py:342
|
||||
|
@ -2844,7 +2901,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"\n"
|
||||
"Gestion de Support (Helpdesk).\n"
|
||||
"====================\n"
|
||||
"==============================\n"
|
||||
"\n"
|
||||
"Les dossiers et le traitement des réclamations sont bien suivis et tracés "
|
||||
"avec le Helpdesk.\n"
|
||||
|
@ -2932,11 +2989,25 @@ msgid ""
|
|||
" * Date\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"\n"
|
||||
"Définir des valeurs par défaut pour les comptes analytiques.\n"
|
||||
"============================================================\n"
|
||||
"\n"
|
||||
"Permet de sélectionner automatiquement les comptes analytiques selon des "
|
||||
"critères :\n"
|
||||
"-----------------------------------------------------------------------------"
|
||||
"------\n"
|
||||
" * Article\n"
|
||||
" * Partenaire\n"
|
||||
" * Utilisateur\n"
|
||||
" * Société\n"
|
||||
" * Date\n"
|
||||
" "
|
||||
|
||||
#. module: base
|
||||
#: field:res.company,rml_header1:0
|
||||
msgid "Company Slogan"
|
||||
msgstr ""
|
||||
msgstr "Slogan de la société"
|
||||
|
||||
#. module: base
|
||||
#: model:res.country,name:base.bb
|
||||
|
@ -2960,7 +3031,7 @@ msgstr ""
|
|||
#. module: base
|
||||
#: model:ir.module.module,shortdesc:base.module_auth_oauth_signup
|
||||
msgid "Signup with OAuth2 Authentication"
|
||||
msgstr ""
|
||||
msgstr "Se connecter avec l'authentification OAuth2"
|
||||
|
||||
#. module: base
|
||||
#: selection:ir.model,state:0
|
||||
|
@ -3021,6 +3092,10 @@ msgid ""
|
|||
"==================================\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"\n"
|
||||
"États-Unis d'Amérique - Plan comptable.\n"
|
||||
"=======================================\n"
|
||||
" "
|
||||
|
||||
#. module: base
|
||||
#: field:ir.actions.act_url,target:0
|
||||
|
@ -3055,7 +3130,7 @@ msgstr "Nom du raccourci"
|
|||
#. module: base
|
||||
#: field:res.partner,contact_address:0
|
||||
msgid "Complete Address"
|
||||
msgstr ""
|
||||
msgstr "Adresse complète"
|
||||
|
||||
#. module: base
|
||||
#: help:ir.actions.act_window,limit:0
|
||||
|
@ -3127,7 +3202,7 @@ msgstr "ID de l’enregistrement"
|
|||
#. module: base
|
||||
#: view:ir.filters:0
|
||||
msgid "My Filters"
|
||||
msgstr ""
|
||||
msgstr "Mes filtres"
|
||||
|
||||
#. module: base
|
||||
#: field:ir.actions.server,email:0
|
||||
|
@ -3141,12 +3216,16 @@ msgid ""
|
|||
"Module to attach a google document to any model.\n"
|
||||
"================================================\n"
|
||||
msgstr ""
|
||||
"\n"
|
||||
"Module pour attacher des documents Google à n'importe quel objet.\n"
|
||||
"=================================================================\n"
|
||||
|
||||
#. module: base
|
||||
#: code:addons/base/ir/ir_fields.py:334
|
||||
#, python-format
|
||||
msgid "Found multiple matches for field '%%(field)s' (%d matches)"
|
||||
msgstr ""
|
||||
"Plusieurs résultats ont été trouvés pour le champ '%%(field)s' (%d résultats)"
|
||||
|
||||
#. module: base
|
||||
#: selection:base.language.install,lang:0
|
||||
|
@ -3180,7 +3259,7 @@ msgstr "Arguments envoyés au client avec la vue"
|
|||
#. module: base
|
||||
#: model:ir.module.module,summary:base.module_contacts
|
||||
msgid "Contacts, People and Companies"
|
||||
msgstr ""
|
||||
msgstr "Contacts, personnes et sociétés"
|
||||
|
||||
#. module: base
|
||||
#: model:res.country,name:base.tt
|
||||
|
@ -3213,7 +3292,7 @@ msgstr "Responsable"
|
|||
#: code:addons/base/ir/ir_model.py:718
|
||||
#, python-format
|
||||
msgid "Sorry, you are not allowed to access this document."
|
||||
msgstr ""
|
||||
msgstr "Désolé, vous n'êtes pas autorisé à accéder à ce document."
|
||||
|
||||
#. module: base
|
||||
#: model:res.country,name:base.py
|
||||
|
@ -3329,7 +3408,7 @@ msgstr ""
|
|||
#: code:addons/base/ir/ir_fields.py:175
|
||||
#, python-format
|
||||
msgid "'%s' does not seem to be an integer for field '%%(field)s'"
|
||||
msgstr ""
|
||||
msgstr "'%s' ne semble pas être un entier pour le champ '%%(field)s'"
|
||||
|
||||
#. module: base
|
||||
#: model:ir.module.category,description:base.module_category_report_designer
|
||||
|
@ -3390,7 +3469,7 @@ msgstr ""
|
|||
#. module: base
|
||||
#: model:ir.module.module,shortdesc:base.module_web_linkedin
|
||||
msgid "LinkedIn Integration"
|
||||
msgstr ""
|
||||
msgstr "Intégration avec LinkedIn"
|
||||
|
||||
#. module: base
|
||||
#: code:addons/orm.py:2021
|
||||
|
@ -3439,6 +3518,8 @@ msgid ""
|
|||
"the user will have an access to the sales configuration as well as statistic "
|
||||
"reports."
|
||||
msgstr ""
|
||||
"l'utilisateur aura un accès à la configuration des ventes ainsi qu'aux "
|
||||
"rapports statistiques."
|
||||
|
||||
#. module: base
|
||||
#: model:res.country,name:base.nz
|
||||
|
@ -3585,7 +3666,7 @@ msgstr "Arménie"
|
|||
#. module: base
|
||||
#: model:ir.module.module,summary:base.module_hr_evaluation
|
||||
msgid "Periodical Evaluations, Appraisals, Surveys"
|
||||
msgstr ""
|
||||
msgstr "Évaluations périodiques, appréciations, sondages"
|
||||
|
||||
#. module: base
|
||||
#: model:ir.actions.act_window,name:base.ir_property_form
|
||||
|
@ -3625,6 +3706,31 @@ msgid ""
|
|||
" payslip interface, but not in the payslip report\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"\n"
|
||||
"Règles pour la paie française.\n"
|
||||
"==============================\n"
|
||||
"\n"
|
||||
" - Configuration du module hr_payroll pour la localisation française\n"
|
||||
" - Toutes les principales règles de contribution pour la fiche de paie "
|
||||
"française, pour les cadres et les non-cadres\n"
|
||||
" - Nouveau rapport des feuilles de paie\n"
|
||||
"\n"
|
||||
"Reste à faire :\n"
|
||||
"---------------\n"
|
||||
" - Intégration avec le module de gestion des congés pour la déduction des "
|
||||
"congés et la gestion des indemnités\n"
|
||||
" - Intégration avec le module hr_payroll_account pour la création des "
|
||||
"lignes d'écritures \n"
|
||||
" depuis la feuille de paie\n"
|
||||
" - Continuer l'intégration des contributions. Seules les principales "
|
||||
"contributions sont \n"
|
||||
" actuellement implémentées.\n"
|
||||
" - Retravailler le rapport sous webkit\n"
|
||||
" - Les lignes de paie avec appears_in_payslip = False doivent apparaître "
|
||||
"dans\n"
|
||||
" l'interface des fiches de paie, mais pas dans le rapport des fiches de "
|
||||
"paie.\n"
|
||||
" "
|
||||
|
||||
#. module: base
|
||||
#: model:res.country,name:base.se
|
||||
|
@ -3634,7 +3740,7 @@ msgstr "Suède"
|
|||
#. module: base
|
||||
#: field:ir.actions.report.xml,report_file:0
|
||||
msgid "Report File"
|
||||
msgstr ""
|
||||
msgstr "Fichier de rapport"
|
||||
|
||||
#. module: base
|
||||
#: selection:ir.actions.act_window.view,view_mode:0
|
||||
|
@ -3667,7 +3773,7 @@ msgstr ""
|
|||
#: code:addons/orm.py:3839
|
||||
#, python-format
|
||||
msgid "Missing document(s)"
|
||||
msgstr ""
|
||||
msgstr "Document(s) manquant(s)"
|
||||
|
||||
#. module: base
|
||||
#: model:ir.model,name:base.model_res_partner_bank_type
|
||||
|
@ -3682,6 +3788,8 @@ msgid ""
|
|||
"For more details about translating OpenERP in your language, please refer to "
|
||||
"the"
|
||||
msgstr ""
|
||||
"Pour plus de détails concernant la traduction d'OpenERP dans votre langue, "
|
||||
"veuillez vous référer à"
|
||||
|
||||
#. module: base
|
||||
#: field:res.partner,image:0
|
||||
|
@ -3807,6 +3915,28 @@ msgid ""
|
|||
"database,\n"
|
||||
" but in the servers rootpad like /server/bin/filestore.\n"
|
||||
msgstr ""
|
||||
"\n"
|
||||
"Ceci est un système complet de gestion de documents.\n"
|
||||
"=====================================================\n"
|
||||
"\n"
|
||||
" * Authentification des utilisateurs\n"
|
||||
" * Indexation des documents ; les fichiers .pptx et .docx ne sont pas "
|
||||
"supportés sous Windows.\n"
|
||||
" * Tableau de bord des documents qui inclut :\n"
|
||||
" * Nouveaux fichiers (liste)\n"
|
||||
" * Fichiers par type de ressources (graphique)\n"
|
||||
" * Fichiers par partenaire (graphique)\n"
|
||||
" * Taille des fichiers par mois (graphique)\n"
|
||||
"\n"
|
||||
"ATTENTION :\n"
|
||||
"-----------\n"
|
||||
"\n"
|
||||
" - Quand vous installez ce module en production dans une société qui a "
|
||||
"déjà des fichiers PDF\n"
|
||||
" enregistrés dans la base de données, ils seront tous perdus.\n"
|
||||
" - Après l'installation de ce module les PDF ne seront plus enregistrés "
|
||||
"dans la base de données,\n"
|
||||
" mais à la racine du serveur dans /server/bin/filestore.\n"
|
||||
|
||||
#. module: base
|
||||
#: help:res.currency,name:0
|
||||
|
@ -3898,7 +4028,7 @@ msgstr "Finlandais / Suomi"
|
|||
#. module: base
|
||||
#: view:ir.config_parameter:0
|
||||
msgid "System Properties"
|
||||
msgstr ""
|
||||
msgstr "Propriétés système"
|
||||
|
||||
#. module: base
|
||||
#: field:ir.sequence,prefix:0
|
||||
|
@ -3955,7 +4085,7 @@ msgstr "Personnel"
|
|||
#. module: base
|
||||
#: field:base.language.export,modules:0
|
||||
msgid "Modules To Export"
|
||||
msgstr ""
|
||||
msgstr "Modules à exporter"
|
||||
|
||||
#. module: base
|
||||
#: model:res.country,name:base.mt
|
||||
|
@ -3968,6 +4098,8 @@ msgstr "Malte"
|
|||
msgid ""
|
||||
"Only users with the following access level are currently allowed to do that"
|
||||
msgstr ""
|
||||
"Seuls les utilisateurs avec les niveaux d'accès suivants sont actuellement "
|
||||
"autorisé à faire cela"
|
||||
|
||||
#. module: base
|
||||
#: field:ir.actions.server,fields_lines:0
|
||||
|
@ -4128,7 +4260,7 @@ msgstr "De droite à gauche"
|
|||
#. module: base
|
||||
#: model:res.country,name:base.sx
|
||||
msgid "Sint Maarten (Dutch part)"
|
||||
msgstr ""
|
||||
msgstr "Saint-Martin (partie néerlandaise)"
|
||||
|
||||
#. module: base
|
||||
#: view:ir.actions.act_window:0
|
||||
|
|
|
@ -7,14 +7,14 @@ msgstr ""
|
|||
"Project-Id-Version: OpenERP Server 5.0.4\n"
|
||||
"Report-Msgid-Bugs-To: support@openerp.com\n"
|
||||
"POT-Creation-Date: 2012-12-03 16:01+0000\n"
|
||||
"PO-Revision-Date: 2012-11-24 21:10+0000\n"
|
||||
"PO-Revision-Date: 2012-12-09 19:36+0000\n"
|
||||
"Last-Translator: Goran Kliska <gkliska@gmail.com>\n"
|
||||
"Language-Team: openerp-translators\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Launchpad-Export-Date: 2012-12-04 04:59+0000\n"
|
||||
"X-Generator: Launchpad (build 16335)\n"
|
||||
"X-Launchpad-Export-Date: 2012-12-10 04:36+0000\n"
|
||||
"X-Generator: Launchpad (build 16341)\n"
|
||||
"Language: hr\n"
|
||||
|
||||
#. module: base
|
||||
|
@ -532,7 +532,7 @@ msgstr "Naziv relacije"
|
|||
#. module: base
|
||||
#: view:ir.rule:0
|
||||
msgid "Create Access Right"
|
||||
msgstr ""
|
||||
msgstr "Kreiraj pravo pristupa"
|
||||
|
||||
#. module: base
|
||||
#: model:res.country,name:base.tv
|
||||
|
@ -582,7 +582,7 @@ msgstr "Francuska Guyana"
|
|||
#. module: base
|
||||
#: model:ir.module.module,summary:base.module_hr
|
||||
msgid "Jobs, Departments, Employees Details"
|
||||
msgstr ""
|
||||
msgstr "Radna mjesta, odjeli, podaci o radnicima"
|
||||
|
||||
#. module: base
|
||||
#: model:ir.module.module,description:base.module_analytic
|
||||
|
@ -695,7 +695,7 @@ msgstr "Kolumbija"
|
|||
#. module: base
|
||||
#: model:res.partner.title,name:base.res_partner_title_mister
|
||||
msgid "Mister"
|
||||
msgstr ""
|
||||
msgstr "Gospodin"
|
||||
|
||||
#. module: base
|
||||
#: help:res.country,code:0
|
||||
|
@ -828,6 +828,11 @@ msgid ""
|
|||
"This module provides the Integration of the LinkedIn with OpenERP.\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"\n"
|
||||
"OpenERP Web LinkedIn modul.\n"
|
||||
"============================\n"
|
||||
"Integracija LinkedIn-a sa OpenERP-om.\n"
|
||||
" "
|
||||
|
||||
#. module: base
|
||||
#: help:ir.actions.act_window,src_model:0
|
||||
|
@ -1132,7 +1137,7 @@ msgstr "Google korisnici"
|
|||
#. module: base
|
||||
#: model:ir.module.module,shortdesc:base.module_fleet
|
||||
msgid "Fleet Management"
|
||||
msgstr ""
|
||||
msgstr "Upravljanje flotom (automobili)"
|
||||
|
||||
#. module: base
|
||||
#: help:ir.server.object.lines,value:0
|
||||
|
@ -1234,7 +1239,7 @@ msgstr "Zadnja verzija"
|
|||
#. module: base
|
||||
#: view:ir.rule:0
|
||||
msgid "Delete Access Right"
|
||||
msgstr ""
|
||||
msgstr "Prava brisanja"
|
||||
|
||||
#. module: base
|
||||
#: code:addons/base/ir/ir_mail_server.py:213
|
||||
|
@ -1304,7 +1309,7 @@ msgstr "Vidljivo"
|
|||
#. module: base
|
||||
#: model:ir.actions.client,name:base.action_client_base_menu
|
||||
msgid "Open Settings Menu"
|
||||
msgstr ""
|
||||
msgstr "Otvori izbornik postavki"
|
||||
|
||||
#. module: base
|
||||
#: selection:base.language.install,lang:0
|
||||
|
@ -1427,7 +1432,7 @@ msgstr "Haiti"
|
|||
#. module: base
|
||||
#: model:ir.module.module,shortdesc:base.module_l10n_fr_hr_payroll
|
||||
msgid "French Payroll"
|
||||
msgstr ""
|
||||
msgstr "Francuska plaća"
|
||||
|
||||
#. module: base
|
||||
#: view:ir.ui.view:0
|
||||
|
@ -2571,7 +2576,7 @@ msgstr "Grčka / Ελληνικά"
|
|||
#. module: base
|
||||
#: field:res.company,custom_footer:0
|
||||
msgid "Custom Footer"
|
||||
msgstr ""
|
||||
msgstr "Prilagođeno podnožje"
|
||||
|
||||
#. module: base
|
||||
#: model:ir.module.module,shortdesc:base.module_sale_crm
|
||||
|
@ -2879,7 +2884,7 @@ msgstr "Naslijeđeno"
|
|||
#: code:addons/base/ir/ir_fields.py:147
|
||||
#, python-format
|
||||
msgid "yes"
|
||||
msgstr ""
|
||||
msgstr "da"
|
||||
|
||||
#. module: base
|
||||
#: field:ir.model.fields,serialization_field_id:0
|
||||
|
@ -14826,7 +14831,7 @@ msgstr ""
|
|||
#. module: base
|
||||
#: field:ir.sequence,implementation:0
|
||||
msgid "Implementation"
|
||||
msgstr ""
|
||||
msgstr "Implementacija"
|
||||
|
||||
#. module: base
|
||||
#: model:ir.module.module,shortdesc:base.module_l10n_ve
|
||||
|
@ -14931,7 +14936,7 @@ msgstr "Ažuriranje sustava"
|
|||
#: field:ir.actions.report.xml,report_sxw_content:0
|
||||
#: field:ir.actions.report.xml,report_sxw_content_data:0
|
||||
msgid "SXW Content"
|
||||
msgstr ""
|
||||
msgstr "SXW Sadržaj"
|
||||
|
||||
#. module: base
|
||||
#: help:ir.sequence,prefix:0
|
||||
|
@ -14946,7 +14951,7 @@ msgstr "Sejšeli"
|
|||
#. module: base
|
||||
#: model:res.partner.category,name:base.res_partner_category_4
|
||||
msgid "Gold"
|
||||
msgstr ""
|
||||
msgstr "Zlatni"
|
||||
|
||||
#. module: base
|
||||
#: code:addons/base/res/res_company.py:159
|
||||
|
@ -14973,7 +14978,7 @@ msgstr "Opći podaci"
|
|||
#. module: base
|
||||
#: field:ir.model.data,complete_name:0
|
||||
msgid "Complete ID"
|
||||
msgstr ""
|
||||
msgstr "Puni ID"
|
||||
|
||||
#. module: base
|
||||
#: model:res.country,name:base.tc
|
||||
|
@ -14985,7 +14990,7 @@ msgstr "Turks and Caicos Islands"
|
|||
msgid ""
|
||||
"Tax Identification Number. Check the box if this contact is subjected to "
|
||||
"taxes. Used by the some of the legal statements."
|
||||
msgstr ""
|
||||
msgstr "Porezni broj (OIB) ."
|
||||
|
||||
#. module: base
|
||||
#: field:res.partner.bank,partner_id:0
|
||||
|
@ -15070,7 +15075,7 @@ msgstr ""
|
|||
#. module: base
|
||||
#: view:res.partner:0
|
||||
msgid "Internal Notes"
|
||||
msgstr ""
|
||||
msgstr "Interne bilješke"
|
||||
|
||||
#. module: base
|
||||
#: selection:res.partner.address,type:0
|
||||
|
@ -15086,7 +15091,7 @@ msgstr "Corp."
|
|||
#. module: base
|
||||
#: model:ir.module.module,shortdesc:base.module_purchase_requisition
|
||||
msgid "Purchase Requisitions"
|
||||
msgstr ""
|
||||
msgstr "Natječaji u nabavi"
|
||||
|
||||
#. module: base
|
||||
#: selection:ir.actions.act_window,target:0
|
||||
|
@ -15112,7 +15117,7 @@ msgstr "Partneri: "
|
|||
#. module: base
|
||||
#: view:res.partner:0
|
||||
msgid "Is a Company?"
|
||||
msgstr ""
|
||||
msgstr "Je tvrtka="
|
||||
|
||||
#. module: base
|
||||
#: code:addons/base/res/res_company.py:159
|
||||
|
@ -15134,7 +15139,7 @@ msgstr "Kreiraj objekt"
|
|||
#. module: base
|
||||
#: model:res.country,name:base.ss
|
||||
msgid "South Sudan"
|
||||
msgstr ""
|
||||
msgstr "Južni Sudan"
|
||||
|
||||
#. module: base
|
||||
#: field:ir.filters,context:0
|
||||
|
|
|
@ -7,14 +7,14 @@ msgstr ""
|
|||
"Project-Id-Version: OpenERP Server 5.0.4\n"
|
||||
"Report-Msgid-Bugs-To: support@openerp.com\n"
|
||||
"POT-Creation-Date: 2012-12-03 16:01+0000\n"
|
||||
"PO-Revision-Date: 2012-12-01 13:29+0000\n"
|
||||
"Last-Translator: Sergio Corato <Unknown>\n"
|
||||
"PO-Revision-Date: 2012-12-09 12:05+0000\n"
|
||||
"Last-Translator: Davide Corio - agilebg.com <davide.corio@agilebg.com>\n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Launchpad-Export-Date: 2012-12-04 04:57+0000\n"
|
||||
"X-Generator: Launchpad (build 16335)\n"
|
||||
"X-Launchpad-Export-Date: 2012-12-10 04:36+0000\n"
|
||||
"X-Generator: Launchpad (build 16341)\n"
|
||||
|
||||
#. module: base
|
||||
#: model:ir.module.module,description:base.module_account_check_writing
|
||||
|
@ -92,7 +92,7 @@ msgstr ""
|
|||
#. module: base
|
||||
#: model:ir.module.module,summary:base.module_point_of_sale
|
||||
msgid "Touchscreen Interface for Shops"
|
||||
msgstr "Interfaccia Touchscreen per Negozzi"
|
||||
msgstr "Interfaccia Touchscreen per Negozi"
|
||||
|
||||
#. module: base
|
||||
#: model:ir.module.module,shortdesc:base.module_l10n_in_hr_payroll
|
||||
|
@ -140,6 +140,9 @@ msgid ""
|
|||
"The module adds google user in res user.\n"
|
||||
"========================================\n"
|
||||
msgstr ""
|
||||
"\n"
|
||||
"Aggiunge lo username Google a res.user.\n"
|
||||
"========================================\n"
|
||||
|
||||
#. module: base
|
||||
#: help:res.partner,employee:0
|
||||
|
@ -322,7 +325,7 @@ msgstr "Gestione Vendite"
|
|||
msgid ""
|
||||
"The internal user that is in charge of communicating with this contact if "
|
||||
"any."
|
||||
msgstr ""
|
||||
msgstr "L'utente responsabile delle comunicazioni con questo contatto."
|
||||
|
||||
#. module: base
|
||||
#: view:res.partner:0
|
||||
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -7,14 +7,14 @@ msgstr ""
|
|||
"Project-Id-Version: OpenERP Server 5.0.4\n"
|
||||
"Report-Msgid-Bugs-To: support@openerp.com\n"
|
||||
"POT-Creation-Date: 2012-12-03 16:01+0000\n"
|
||||
"PO-Revision-Date: 2012-12-01 11:25+0000\n"
|
||||
"Last-Translator: Chertykov Denis <chertykov@gmail.com>\n"
|
||||
"PO-Revision-Date: 2012-12-05 09:43+0000\n"
|
||||
"Last-Translator: Denis Karataev <dskarataev@gmail.com>\n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Launchpad-Export-Date: 2012-12-04 04:59+0000\n"
|
||||
"X-Generator: Launchpad (build 16335)\n"
|
||||
"X-Launchpad-Export-Date: 2012-12-06 04:38+0000\n"
|
||||
"X-Generator: Launchpad (build 16341)\n"
|
||||
|
||||
#. module: base
|
||||
#: model:ir.module.module,description:base.module_account_check_writing
|
||||
|
@ -1638,7 +1638,7 @@ msgstr "Настройка нижнего колонтитула отчета"
|
|||
#. module: base
|
||||
#: field:ir.translation,comments:0
|
||||
msgid "Translation comments"
|
||||
msgstr ""
|
||||
msgstr "Комментарии перевода"
|
||||
|
||||
#. module: base
|
||||
#: model:ir.module.module,description:base.module_lunch
|
||||
|
@ -5663,6 +5663,9 @@ msgid ""
|
|||
" <p>You should try others search criteria.</p>\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"<p><b>Модули не найдены!</b></p>\n"
|
||||
" <p>Попробуйте поискать по другим критериям.</p>\n"
|
||||
" "
|
||||
|
||||
#. module: base
|
||||
#: model:ir.model,name:base.model_ir_module_module
|
||||
|
@ -7490,6 +7493,11 @@ msgid ""
|
|||
"use the same timezone that is otherwise used to pick and render date and "
|
||||
"time values: your computer's timezone."
|
||||
msgstr ""
|
||||
"Часовой пояс партнера, используемый для вывода правильной даты и времени "
|
||||
"внутри распечатываемых отчетов. Важно установить значение этого поля. Вы "
|
||||
"должны использовать тот же часовой пояс который есть, в противном случае "
|
||||
"значения даты и времени будут вычисляться на основе часового пояса "
|
||||
"установленного на компьютере."
|
||||
|
||||
#. module: base
|
||||
#: model:ir.module.module,shortdesc:base.module_account_analytic_default
|
||||
|
@ -8407,6 +8415,8 @@ msgid ""
|
|||
"Translation features are unavailable until you install an extra OpenERP "
|
||||
"translation."
|
||||
msgstr ""
|
||||
"Возможности перевода не доступны до тех пор, пока вы не установите "
|
||||
"расширенный OpenERP перевод."
|
||||
|
||||
#. module: base
|
||||
#: model:ir.module.module,description:base.module_l10n_nl
|
||||
|
|
|
@ -7,14 +7,14 @@ msgstr ""
|
|||
"Project-Id-Version: OpenERP Server 5.0.4\n"
|
||||
"Report-Msgid-Bugs-To: support@openerp.com\n"
|
||||
"POT-Creation-Date: 2012-12-03 16:01+0000\n"
|
||||
"PO-Revision-Date: 2012-12-04 02:43+0000\n"
|
||||
"PO-Revision-Date: 2012-12-06 18:17+0000\n"
|
||||
"Last-Translator: ccdos <ccdos@163.com>\n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Launchpad-Export-Date: 2012-12-04 05:03+0000\n"
|
||||
"X-Generator: Launchpad (build 16335)\n"
|
||||
"X-Launchpad-Export-Date: 2012-12-07 04:34+0000\n"
|
||||
"X-Generator: Launchpad (build 16341)\n"
|
||||
|
||||
#. module: base
|
||||
#: model:ir.module.module,description:base.module_account_check_writing
|
||||
|
@ -88,7 +88,7 @@ msgstr "帮助您管理项目、跟踪任务、生成计划等"
|
|||
#. module: base
|
||||
#: model:ir.module.module,summary:base.module_point_of_sale
|
||||
msgid "Touchscreen Interface for Shops"
|
||||
msgstr "商店的触摸屏接口"
|
||||
msgstr "触摸屏接口的POS 系统"
|
||||
|
||||
#. module: base
|
||||
#: model:ir.module.module,shortdesc:base.module_l10n_in_hr_payroll
|
||||
|
@ -896,6 +896,14 @@ msgid ""
|
|||
" </p>\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"<p class=\"oe_view_nocontent_create\">\n"
|
||||
" 点击此处在你的地址本中增加一个新的联系人。\n"
|
||||
" </p><p>\n"
|
||||
" Openerp可以帮助你轻松的跟踪所有与客户相关的活动:\n"
|
||||
" 例如讨论,商业机会的历史记录,\n"
|
||||
" 文档,等等。\n"
|
||||
" </p>\n"
|
||||
" "
|
||||
|
||||
#. module: base
|
||||
#: model:ir.module.module,description:base.module_web_linkedin
|
||||
|
@ -2645,6 +2653,18 @@ msgid ""
|
|||
" * Date\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"\n"
|
||||
"为你的分析帐目设定缺省值。\n"
|
||||
"==============================================\n"
|
||||
"\n"
|
||||
"基于以下规则时,允许自动选择分析帐目:\n"
|
||||
"---------------------------------------------------------------------\n"
|
||||
" * 产品\n"
|
||||
" * 合作伙伴\n"
|
||||
" * 用户\n"
|
||||
" * 公司\n"
|
||||
" * 日期\n"
|
||||
" "
|
||||
|
||||
#. module: base
|
||||
#: field:res.company,rml_header1:0
|
||||
|
@ -4230,7 +4250,7 @@ msgstr "共享任意文档"
|
|||
#. module: base
|
||||
#: model:ir.module.module,summary:base.module_crm
|
||||
msgid "Leads, Opportunities, Phone Calls"
|
||||
msgstr "线索,上级,电话"
|
||||
msgstr "线索,商机,电话"
|
||||
|
||||
#. module: base
|
||||
#: view:res.lang:0
|
||||
|
@ -7666,6 +7686,10 @@ msgid ""
|
|||
"==========================================\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"\n"
|
||||
"CRM 线索和商机的待办事项\n"
|
||||
"==========================================\n"
|
||||
" "
|
||||
|
||||
#. module: base
|
||||
#: view:ir.mail_server:0
|
||||
|
@ -9849,7 +9873,7 @@ msgstr "将要安装的模块"
|
|||
#: model:ir.module.module,shortdesc:base.module_base
|
||||
#: field:res.currency,base:0
|
||||
msgid "Base"
|
||||
msgstr "本位币"
|
||||
msgstr "base 模块"
|
||||
|
||||
#. module: base
|
||||
#: field:ir.model.data,model:0
|
||||
|
|
|
@ -42,7 +42,7 @@ class actions(osv.osv):
|
|||
_order = 'name'
|
||||
_columns = {
|
||||
'name': fields.char('Name', size=64, required=True),
|
||||
'type': fields.char('Action Type', required=True, size=32,readonly=True),
|
||||
'type': fields.char('Action Type', required=True, size=32),
|
||||
'usage': fields.char('Action Usage', size=32),
|
||||
'help': fields.text('Action description',
|
||||
help='Optional help text for the users with a description of the target view, such as its usage and purpose.',
|
||||
|
|
|
@ -502,6 +502,19 @@
|
|||
<menuitem id="menu_ir_actions_todo" name="Configuration Wizards" parent="menu_custom" sequence="20" groups="base.group_no_one"/>
|
||||
<menuitem id="menu_ir_actions_todo_form" action="act_ir_actions_todo_form" parent="menu_ir_actions_todo"/>
|
||||
|
||||
<record id="action_run_ir_action_todo" model="ir.actions.server">
|
||||
<field name="name">Run Remaining Action Todo</field>
|
||||
<field name="condition">True</field>
|
||||
<field name="type">ir.actions.server</field>
|
||||
<field name="model_id" ref="model_res_config"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">
|
||||
config = self.next(cr, uid, [], context=context) or {}
|
||||
if config.get('type') not in ('ir.actions.act_window_close',):
|
||||
action = config
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
</data>
|
||||
</openerp>
|
||||
|
|
|
@ -31,7 +31,6 @@ import netsvc
|
|||
import openerp
|
||||
import pooler
|
||||
import tools
|
||||
from openerp.cron import WAKE_UP_NOW
|
||||
from osv import fields, osv
|
||||
from tools import DEFAULT_SERVER_DATETIME_FORMAT
|
||||
from tools.safe_eval import safe_eval as eval
|
||||
|
@ -142,130 +141,6 @@ class ir_cron(osv.osv):
|
|||
except Exception, e:
|
||||
self._handle_callback_exception(cr, uid, model_name, method_name, args, job_id, e)
|
||||
|
||||
def _run_job(self, cr, job, now):
|
||||
""" Run a given job taking care of the repetition.
|
||||
|
||||
The cursor has a lock on the job (aquired by _run_jobs_multithread()) and this
|
||||
method is run in a worker thread (spawned by _run_jobs_multithread())).
|
||||
|
||||
:param job: job to be run (as a dictionary).
|
||||
:param now: timestamp (result of datetime.now(), no need to call it multiple time).
|
||||
|
||||
"""
|
||||
try:
|
||||
nextcall = datetime.strptime(job['nextcall'], DEFAULT_SERVER_DATETIME_FORMAT)
|
||||
numbercall = job['numbercall']
|
||||
|
||||
ok = False
|
||||
while nextcall < now and numbercall:
|
||||
if numbercall > 0:
|
||||
numbercall -= 1
|
||||
if not ok or job['doall']:
|
||||
self._callback(cr, job['user_id'], job['model'], job['function'], job['args'], job['id'])
|
||||
if numbercall:
|
||||
nextcall += _intervalTypes[job['interval_type']](job['interval_number'])
|
||||
ok = True
|
||||
addsql = ''
|
||||
if not numbercall:
|
||||
addsql = ', active=False'
|
||||
cr.execute("UPDATE ir_cron SET nextcall=%s, numbercall=%s"+addsql+" WHERE id=%s",
|
||||
(nextcall.strftime(DEFAULT_SERVER_DATETIME_FORMAT), numbercall, job['id']))
|
||||
|
||||
if numbercall:
|
||||
# Reschedule our own main cron thread if necessary.
|
||||
# This is really needed if this job runs longer than its rescheduling period.
|
||||
nextcall = calendar.timegm(nextcall.timetuple())
|
||||
openerp.cron.schedule_wakeup(nextcall, cr.dbname)
|
||||
finally:
|
||||
cr.commit()
|
||||
cr.close()
|
||||
openerp.cron.release_thread_slot()
|
||||
|
||||
def _run_jobs_multithread(self):
|
||||
# TODO remove 'check' argument from addons/base_action_rule/base_action_rule.py
|
||||
""" Process the cron jobs by spawning worker threads.
|
||||
|
||||
This selects in database all the jobs that should be processed. It then
|
||||
tries to lock each of them and, if it succeeds, spawns a thread to run
|
||||
the cron job (if it doesn't succeed, it means the job was already
|
||||
locked to be taken care of by another thread).
|
||||
|
||||
The cursor used to lock the job in database is given to the worker
|
||||
thread (which has to close it itself).
|
||||
|
||||
"""
|
||||
db = self.pool.db
|
||||
cr = db.cursor()
|
||||
db_name = db.dbname
|
||||
try:
|
||||
jobs = {} # mapping job ids to jobs for all jobs being processed.
|
||||
now = datetime.now()
|
||||
# Careful to compare timestamps with 'UTC' - everything is UTC as of v6.1.
|
||||
cr.execute("""SELECT * FROM ir_cron
|
||||
WHERE numbercall != 0
|
||||
AND active AND nextcall <= (now() at time zone 'UTC')
|
||||
ORDER BY priority""")
|
||||
for job in cr.dictfetchall():
|
||||
if not openerp.cron.get_thread_slots():
|
||||
break
|
||||
jobs[job['id']] = job
|
||||
|
||||
task_cr = db.cursor()
|
||||
try:
|
||||
# Try to grab an exclusive lock on the job row from within the task transaction
|
||||
acquired_lock = False
|
||||
task_cr.execute("""SELECT *
|
||||
FROM ir_cron
|
||||
WHERE id=%s
|
||||
FOR UPDATE NOWAIT""",
|
||||
(job['id'],), log_exceptions=False)
|
||||
acquired_lock = True
|
||||
except psycopg2.OperationalError, e:
|
||||
if e.pgcode == '55P03':
|
||||
# Class 55: Object not in prerequisite state; 55P03: lock_not_available
|
||||
_logger.debug('Another process/thread is already busy executing job `%s`, skipping it.', job['name'])
|
||||
continue
|
||||
else:
|
||||
# Unexpected OperationalError
|
||||
raise
|
||||
finally:
|
||||
if not acquired_lock:
|
||||
# we're exiting due to an exception while acquiring the lot
|
||||
task_cr.close()
|
||||
|
||||
# Got the lock on the job row, now spawn a thread to execute it in the transaction with the lock
|
||||
task_thread = threading.Thread(target=self._run_job, name=job['name'], args=(task_cr, job, now))
|
||||
# force non-daemon task threads (the runner thread must be daemon, and this property is inherited by default)
|
||||
task_thread.setDaemon(False)
|
||||
openerp.cron.take_thread_slot()
|
||||
task_thread.start()
|
||||
_logger.debug('Cron execution thread for job `%s` spawned', job['name'])
|
||||
|
||||
# Find next earliest job ignoring currently processed jobs (by this and other cron threads)
|
||||
find_next_time_query = """SELECT min(nextcall) AS min_next_call
|
||||
FROM ir_cron WHERE numbercall != 0 AND active"""
|
||||
if jobs:
|
||||
cr.execute(find_next_time_query + " AND id NOT IN %s", (tuple(jobs.keys()),))
|
||||
else:
|
||||
cr.execute(find_next_time_query)
|
||||
next_call = cr.dictfetchone()['min_next_call']
|
||||
|
||||
if next_call:
|
||||
next_call = calendar.timegm(time.strptime(next_call, DEFAULT_SERVER_DATETIME_FORMAT))
|
||||
else:
|
||||
# no matching cron job found in database, re-schedule arbitrarily in 1 day,
|
||||
# this delay will likely be modified when running jobs complete their tasks
|
||||
next_call = time.time() + (24*3600)
|
||||
|
||||
openerp.cron.schedule_wakeup(next_call, db_name)
|
||||
|
||||
except Exception, ex:
|
||||
_logger.warning('Exception in cron:', exc_info=True)
|
||||
|
||||
finally:
|
||||
cr.commit()
|
||||
cr.close()
|
||||
|
||||
def _process_job(self, cr, job):
|
||||
""" Run a given job taking care of the repetition.
|
||||
|
||||
|
@ -365,19 +240,6 @@ class ir_cron(osv.osv):
|
|||
|
||||
return False
|
||||
|
||||
def update_running_cron(self, cr):
|
||||
""" Schedule as soon as possible a wake-up for this database. """
|
||||
# Verify whether the server is already started and thus whether we need to commit
|
||||
# immediately our changes and restart the cron agent in order to apply the change
|
||||
# immediately. The commit() is needed because as soon as the cron is (re)started it
|
||||
# will query the database with its own cursor, possibly before the end of the
|
||||
# current transaction.
|
||||
# This commit() is not an issue in most cases, but we must absolutely avoid it
|
||||
# when the server is only starting or loading modules (hence the test on pool._init).
|
||||
if not self.pool._init:
|
||||
cr.commit()
|
||||
openerp.cron.schedule_wakeup(WAKE_UP_NOW, self.pool.db.dbname)
|
||||
|
||||
def _try_lock(self, cr, uid, ids, context=None):
|
||||
"""Try to grab a dummy exclusive write-lock to the rows with the given ids,
|
||||
to make sure a following write() or unlink() will not block due
|
||||
|
@ -393,20 +255,16 @@ class ir_cron(osv.osv):
|
|||
|
||||
def create(self, cr, uid, vals, context=None):
|
||||
res = super(ir_cron, self).create(cr, uid, vals, context=context)
|
||||
self.update_running_cron(cr)
|
||||
return res
|
||||
|
||||
def write(self, cr, uid, ids, vals, context=None):
|
||||
self._try_lock(cr, uid, ids, context)
|
||||
res = super(ir_cron, self).write(cr, uid, ids, vals, context=context)
|
||||
self.update_running_cron(cr)
|
||||
return res
|
||||
|
||||
def unlink(self, cr, uid, ids, context=None):
|
||||
self._try_lock(cr, uid, ids, context)
|
||||
res = super(ir_cron, self).unlink(cr, uid, ids, context=context)
|
||||
self.update_running_cron(cr)
|
||||
return res
|
||||
ir_cron()
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||
|
|
|
@ -209,6 +209,7 @@ class ir_model(osv.osv):
|
|||
else:
|
||||
x_name = a._columns.keys()[0]
|
||||
x_custom_model._rec_name = x_name
|
||||
a._rec_name = x_name
|
||||
|
||||
class ir_model_fields(osv.osv):
|
||||
_name = 'ir.model.fields'
|
||||
|
|
|
@ -44,7 +44,7 @@
|
|||
<field name="selection" attrs="{'required': [('ttype','in',['selection','reference'])], 'readonly': [('ttype','not in',['selection','reference'])]}"/>
|
||||
<field name="size" attrs="{'required': [('ttype','in',['char','reference'])], 'readonly': [('ttype','not in',['char','reference'])]}"/>
|
||||
<field name="domain" attrs="{'readonly': [('relation','=','')]}"/>
|
||||
<field name="serialization_field_id" attrs="{'readonly': [('state','=','base')]}" domain="[('ttype','=','serialized'), ('model_id', '=', model_id)]"/>
|
||||
<field name="serialization_field_id" attrs="{'readonly': [('state','=','base')]}" domain="[('ttype','=','serialized'), ('model_id', '=', parent.model)]"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="required"/>
|
||||
|
|
|
@ -43,7 +43,7 @@ class ir_ui_menu(osv.osv):
|
|||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.cache_lock = threading.RLock()
|
||||
self.clear_cache()
|
||||
self._cache = {}
|
||||
r = super(ir_ui_menu, self).__init__(*args, **kwargs)
|
||||
self.pool.get('ir.model.access').register_cache_clearing_method(self._name, 'clear_cache')
|
||||
return r
|
||||
|
@ -51,6 +51,10 @@ class ir_ui_menu(osv.osv):
|
|||
def clear_cache(self):
|
||||
with self.cache_lock:
|
||||
# radical but this doesn't frequently happen
|
||||
if self._cache:
|
||||
# Normally this is done by openerp.tools.ormcache
|
||||
# but since we do not use it, set it by ourself.
|
||||
self.pool._any_cache_cleared = True
|
||||
self._cache = {}
|
||||
|
||||
def _filter_visible_menus(self, cr, uid, ids, context=None):
|
||||
|
|
|
@ -30,6 +30,7 @@ import re
|
|||
import urllib
|
||||
import zipimport
|
||||
|
||||
import openerp
|
||||
from openerp import modules, pooler, release, tools, addons
|
||||
from openerp.modules.db import create_categories
|
||||
from openerp.tools.parse_version import parse_version
|
||||
|
@ -385,6 +386,8 @@ class module(osv.osv):
|
|||
# Mark them to be installed.
|
||||
if to_install_ids:
|
||||
self.button_install(cr, uid, to_install_ids, context=context)
|
||||
|
||||
openerp.modules.registry.RegistryManager.signal_registry_change(cr.dbname)
|
||||
return dict(ACTION_DICT, name=_('Install'))
|
||||
|
||||
def button_immediate_install(self, cr, uid, ids, context=None):
|
||||
|
|
|
@ -258,7 +258,7 @@
|
|||
</group>
|
||||
<group>
|
||||
<field name="customer"/>
|
||||
<field name="supplier" invisible="not context.get('default_supplier')"/>
|
||||
<field name="supplier"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="ref"/>
|
||||
|
|
|
@ -50,20 +50,25 @@ class test_ir_values(common.TransactionCase):
|
|||
|
||||
# Create some action bindings for a non-existing model.
|
||||
|
||||
act_id_1 = self.ref('base.act_values_form_action')
|
||||
act_id_2 = self.ref('base.act_values_form_defaults')
|
||||
act_id_3 = self.ref('base.action_res_company_form')
|
||||
act_id_4 = self.ref('base.action_res_company_tree')
|
||||
|
||||
ir_values = self.registry('ir.values')
|
||||
ir_values.set(self.cr, self.uid, 'action', 'tree_but_open', 'OnDblClick Action', ['unexisting_model'], 'ir.actions.act_window,10', isobject=True)
|
||||
ir_values.set(self.cr, self.uid, 'action', 'tree_but_open', 'OnDblClick Action 2', ['unexisting_model'], 'ir.actions.act_window,11', isobject=True)
|
||||
ir_values.set(self.cr, self.uid, 'action', 'client_action_multi', 'Side Wizard', ['unexisting_model'], 'ir.actions.act_window,12', isobject=True)
|
||||
ir_values.set(self.cr, self.uid, 'action', 'tree_but_open', 'OnDblClick Action', ['unexisting_model'], 'ir.actions.act_window,%d' % act_id_1, isobject=True)
|
||||
ir_values.set(self.cr, self.uid, 'action', 'tree_but_open', 'OnDblClick Action 2', ['unexisting_model'], 'ir.actions.act_window,%d' % act_id_2, isobject=True)
|
||||
ir_values.set(self.cr, self.uid, 'action', 'client_action_multi', 'Side Wizard', ['unexisting_model'], 'ir.actions.act_window,%d' % act_id_3, isobject=True)
|
||||
report_ids = self.registry('ir.actions.report.xml').search(self.cr, self.uid, [], {})
|
||||
reports = self.registry('ir.actions.report.xml').browse(self.cr, self.uid, report_ids, {})
|
||||
report_id = [report.id for report in reports if not report.groups_id][0] # assume at least one
|
||||
ir_values.set(self.cr, self.uid, 'action', 'client_print_multi', 'Nice Report', ['unexisting_model'], 'ir.actions.report.xml,%s'%report_id, isobject=True)
|
||||
ir_values.set(self.cr, self.uid, 'action', 'client_action_relate', 'Related Stuff', ['unexisting_model'], 'ir.actions.act_window,14', isobject=True)
|
||||
report_id = [report.id for report in reports if not report.groups_id][0] # assume at least one
|
||||
ir_values.set(self.cr, self.uid, 'action', 'client_print_multi', 'Nice Report', ['unexisting_model'], 'ir.actions.report.xml,%d' % report_id, isobject=True)
|
||||
ir_values.set(self.cr, self.uid, 'action', 'client_action_relate', 'Related Stuff', ['unexisting_model'], 'ir.actions.act_window,%d' % act_id_4, isobject=True)
|
||||
|
||||
# Replace one action binding to set a new name.
|
||||
|
||||
ir_values = self.registry('ir.values')
|
||||
ir_values.set(self.cr, self.uid, 'action', 'tree_but_open', 'OnDblClick Action New', ['unexisting_model'], 'ir.actions.act_window,10', isobject=True)
|
||||
ir_values.set(self.cr, self.uid, 'action', 'tree_but_open', 'OnDblClick Action New', ['unexisting_model'], 'ir.actions.act_window,%d' % act_id_1, isobject=True)
|
||||
|
||||
# Retrieve the action bindings and check they're correct
|
||||
|
||||
|
@ -73,17 +78,17 @@ class test_ir_values(common.TransactionCase):
|
|||
#first action
|
||||
assert len(actions[0]) == 3, "Malformed action definition"
|
||||
assert actions[0][1] == 'OnDblClick Action 2', 'Bound action does not match definition'
|
||||
assert isinstance(actions[0][2], dict) and actions[0][2]['id'] == 11, 'Bound action does not match definition'
|
||||
assert isinstance(actions[0][2], dict) and actions[0][2]['id'] == act_id_2, 'Bound action does not match definition'
|
||||
#second action - this ones comes last because it was re-created with a different name
|
||||
assert len(actions[1]) == 3, "Malformed action definition"
|
||||
assert actions[1][1] == 'OnDblClick Action New', 'Re-Registering an action should replace it'
|
||||
assert isinstance(actions[1][2], dict) and actions[1][2]['id'] == 10, 'Bound action does not match definition'
|
||||
assert isinstance(actions[1][2], dict) and actions[1][2]['id'] == act_id_1, 'Bound action does not match definition'
|
||||
|
||||
actions = ir_values.get(self.cr, self.uid, 'action', 'client_action_multi', ['unexisting_model'])
|
||||
assert len(actions) == 1, "Mismatching number of bound actions"
|
||||
assert len(actions[0]) == 3, "Malformed action definition"
|
||||
assert actions[0][1] == 'Side Wizard', 'Bound action does not match definition'
|
||||
assert isinstance(actions[0][2], dict) and actions[0][2]['id'] == 12, 'Bound action does not match definition'
|
||||
assert isinstance(actions[0][2], dict) and actions[0][2]['id'] == act_id_3, 'Bound action does not match definition'
|
||||
|
||||
actions = ir_values.get(self.cr, self.uid, 'action', 'client_print_multi', ['unexisting_model'])
|
||||
assert len(actions) == 1, "Mismatching number of bound actions"
|
||||
|
@ -95,7 +100,7 @@ class test_ir_values(common.TransactionCase):
|
|||
assert len(actions) == 1, "Mismatching number of bound actions"
|
||||
assert len(actions[0]) == 3, "Malformed action definition"
|
||||
assert actions[0][1] == 'Related Stuff', 'Bound action does not match definition'
|
||||
assert isinstance(actions[0][2], dict) and actions[0][2]['id'] == 14, 'Bound action does not match definition'
|
||||
assert isinstance(actions[0][2], dict) and actions[0][2]['id'] == act_id_4, 'Bound action does not match definition'
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
|
|
@ -2,6 +2,8 @@ import logging
|
|||
import sys
|
||||
|
||||
import openerp
|
||||
from openerp import tools
|
||||
from openerp.modules import module
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -32,8 +34,25 @@ import server
|
|||
|
||||
def main():
|
||||
args = sys.argv[1:]
|
||||
|
||||
# The only shared option is '--addons-path=' needed to discover additional
|
||||
# commands from modules
|
||||
if len(args) > 1 and args[0].startswith('--addons-path=') and not args[1].startswith("-"):
|
||||
tools.config.parse_config([args[0]])
|
||||
args = args[1:]
|
||||
|
||||
# Default legacy command
|
||||
command = "server"
|
||||
|
||||
# Subcommand discovery
|
||||
if len(args) and not args[0].startswith("-"):
|
||||
for m in module.get_modules():
|
||||
m = 'openerp.addons.' + m
|
||||
__import__(m)
|
||||
#try:
|
||||
#except Exception, e:
|
||||
# raise
|
||||
# print e
|
||||
command = args[0]
|
||||
args = args[1:]
|
||||
|
||||
|
|
|
@ -35,10 +35,6 @@ must be used.
|
|||
|
||||
import deprecation
|
||||
|
||||
# Maximum number of threads processing concurrently cron jobs.
|
||||
max_cron_threads = 4 # Actually the default value here is meaningless,
|
||||
# look at tools.config for the default value.
|
||||
|
||||
# Paths to search for OpenERP addons.
|
||||
addons_paths = []
|
||||
|
||||
|
|
212
openerp/cron.py
212
openerp/cron.py
|
@ -1,212 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Management Solution
|
||||
# Copyright (C) 2004-2011 OpenERP SA (<http://www.openerp.com>)
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
""" Cron jobs scheduling
|
||||
|
||||
Cron jobs are defined in the ir_cron table/model. This module deals with all
|
||||
cron jobs, for all databases of a single OpenERP server instance.
|
||||
|
||||
It defines a single master thread that will spawn (a bounded number of)
|
||||
threads to process individual cron jobs.
|
||||
|
||||
The thread runs forever, checking every 60 seconds for new
|
||||
'database wake-ups'. It maintains a heapq of database wake-ups. At each
|
||||
wake-up, it will call ir_cron._run_jobs_multithread() for the given database. _run_jobs_multithread
|
||||
will check the jobs defined in the ir_cron table and spawn accordingly threads
|
||||
to process them.
|
||||
|
||||
This module's behavior depends on the following configuration variable:
|
||||
openerp.conf.max_cron_threads.
|
||||
|
||||
"""
|
||||
|
||||
import heapq
|
||||
import logging
|
||||
import threading
|
||||
import time
|
||||
|
||||
import openerp
|
||||
import tools
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
# Heapq of database wake-ups. Note that 'database wake-up' meaning is in
|
||||
# the context of the cron management. This is not originally about loading
|
||||
# a database, although having the database name in the queue will
|
||||
# cause it to be loaded when the schedule time is reached, even if it was
|
||||
# unloaded in the mean time. Normally a database's wake-up is cancelled by
|
||||
# the RegistryManager when the database is unloaded - so this should not
|
||||
# cause it to be reloaded.
|
||||
#
|
||||
# TODO: perhaps in the future we could consider a flag on ir.cron jobs
|
||||
# that would cause database wake-up even if the database has not been
|
||||
# loaded yet or was already unloaded (e.g. 'force_db_wakeup' or something)
|
||||
#
|
||||
# Each element is a triple (timestamp, database-name, boolean). The boolean
|
||||
# specifies if the wake-up is canceled (so a wake-up can be canceled without
|
||||
# relying on the heapq implementation detail; no need to remove the job from
|
||||
# the heapq).
|
||||
_wakeups = []
|
||||
|
||||
# Mapping of database names to the wake-up defined in the heapq,
|
||||
# so that we can cancel the wake-up without messing with the heapq
|
||||
# invariant: lookup the wake-up by database-name, then set
|
||||
# its third element to True.
|
||||
_wakeup_by_db = {}
|
||||
|
||||
# Re-entrant lock to protect the above _wakeups and _wakeup_by_db variables.
|
||||
# We could use a simple (non-reentrant) lock if the runner function below
|
||||
# was more fine-grained, but we are fine with the loop owning the lock
|
||||
# while spawning a few threads.
|
||||
_wakeups_lock = threading.RLock()
|
||||
|
||||
# Maximum number of threads allowed to process cron jobs concurrently. This
|
||||
# variable is set by start_master_thread using openerp.conf.max_cron_threads.
|
||||
_thread_slots = None
|
||||
|
||||
# A (non re-entrant) lock to protect the above _thread_slots variable.
|
||||
_thread_slots_lock = threading.Lock()
|
||||
|
||||
# Sleep duration limits - must not loop too quickly, but can't sleep too long
|
||||
# either, because a new job might be inserted in ir_cron with a much sooner
|
||||
# execution date than current known ones. We won't see it until we wake!
|
||||
MAX_SLEEP = 60 # 1 min
|
||||
MIN_SLEEP = 1 # 1 sec
|
||||
|
||||
# Dummy wake-up timestamp that can be used to force a database wake-up asap
|
||||
WAKE_UP_NOW = 1
|
||||
|
||||
def get_thread_slots():
|
||||
""" Return the number of available thread slots """
|
||||
return _thread_slots
|
||||
|
||||
|
||||
def release_thread_slot():
|
||||
""" Increment the number of available thread slots """
|
||||
global _thread_slots
|
||||
with _thread_slots_lock:
|
||||
_thread_slots += 1
|
||||
|
||||
|
||||
def take_thread_slot():
|
||||
""" Decrement the number of available thread slots """
|
||||
global _thread_slots
|
||||
with _thread_slots_lock:
|
||||
_thread_slots -= 1
|
||||
|
||||
|
||||
def cancel(db_name):
|
||||
""" Cancel the next wake-up of a given database, if any.
|
||||
|
||||
:param db_name: database name for which the wake-up is canceled.
|
||||
|
||||
"""
|
||||
_logger.debug("Cancel next wake-up for database '%s'.", db_name)
|
||||
with _wakeups_lock:
|
||||
if db_name in _wakeup_by_db:
|
||||
_wakeup_by_db[db_name][2] = True
|
||||
|
||||
|
||||
def cancel_all():
|
||||
""" Cancel all database wake-ups. """
|
||||
_logger.debug("Cancel all database wake-ups")
|
||||
global _wakeups
|
||||
global _wakeup_by_db
|
||||
with _wakeups_lock:
|
||||
_wakeups = []
|
||||
_wakeup_by_db = {}
|
||||
|
||||
|
||||
def schedule_wakeup(timestamp, db_name):
|
||||
""" Schedule a new wake-up for a database.
|
||||
|
||||
If an earlier wake-up is already defined, the new wake-up is discarded.
|
||||
If another wake-up is defined, that wake-up is discarded and the new one
|
||||
is scheduled.
|
||||
|
||||
:param db_name: database name for which a new wake-up is scheduled.
|
||||
:param timestamp: when the wake-up is scheduled.
|
||||
|
||||
"""
|
||||
if not timestamp:
|
||||
return
|
||||
with _wakeups_lock:
|
||||
if db_name in _wakeup_by_db:
|
||||
task = _wakeup_by_db[db_name]
|
||||
if not task[2] and timestamp > task[0]:
|
||||
# existing wakeup is valid and occurs earlier than new one
|
||||
return
|
||||
task[2] = True # cancel existing task
|
||||
task = [timestamp, db_name, False]
|
||||
heapq.heappush(_wakeups, task)
|
||||
_wakeup_by_db[db_name] = task
|
||||
_logger.debug("Wake-up scheduled for database '%s' @ %s", db_name,
|
||||
'NOW' if timestamp == WAKE_UP_NOW else timestamp)
|
||||
|
||||
def runner():
|
||||
"""Neverending function (intended to be run in a dedicated thread) that
|
||||
checks every 60 seconds the next database wake-up. TODO: make configurable
|
||||
"""
|
||||
while True:
|
||||
runner_body()
|
||||
|
||||
def runner_body():
|
||||
with _wakeups_lock:
|
||||
while _wakeups and _wakeups[0][0] < time.time() and get_thread_slots():
|
||||
task = heapq.heappop(_wakeups)
|
||||
timestamp, db_name, canceled = task
|
||||
if canceled:
|
||||
continue
|
||||
del _wakeup_by_db[db_name]
|
||||
registry = openerp.pooler.get_pool(db_name)
|
||||
if not registry._init:
|
||||
_logger.debug("Database '%s' wake-up! Firing multi-threaded cron job processing", db_name)
|
||||
registry['ir.cron']._run_jobs_multithread()
|
||||
amount = MAX_SLEEP
|
||||
with _wakeups_lock:
|
||||
# Sleep less than MAX_SLEEP if the next known wake-up will happen before that.
|
||||
if _wakeups and get_thread_slots():
|
||||
amount = min(MAX_SLEEP, max(MIN_SLEEP, _wakeups[0][0] - time.time()))
|
||||
_logger.debug("Going to sleep for %ss", amount)
|
||||
time.sleep(amount)
|
||||
|
||||
def start_master_thread():
|
||||
""" Start the above runner function in a daemon thread.
|
||||
|
||||
The thread is a typical daemon thread: it will never quit and must be
|
||||
terminated when the main process exits - with no consequence (the processing
|
||||
threads it spawns are not marked daemon).
|
||||
|
||||
"""
|
||||
global _thread_slots
|
||||
_thread_slots = openerp.conf.max_cron_threads
|
||||
db_maxconn = tools.config['db_maxconn']
|
||||
if _thread_slots >= tools.config.get('db_maxconn', 64):
|
||||
_logger.warning("Connection pool size (%s) is set lower than max number of cron threads (%s), "
|
||||
"this may cause trouble if you reach that number of parallel cron tasks.",
|
||||
db_maxconn, _thread_slots)
|
||||
t = threading.Thread(target=runner, name="openerp.cron.master_thread")
|
||||
t.setDaemon(True)
|
||||
t.start()
|
||||
_logger.debug("Master cron daemon started!")
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
|
@ -433,8 +433,9 @@ def get_modules():
|
|||
return name
|
||||
|
||||
def is_really_module(name):
|
||||
name = opj(dir, name)
|
||||
return os.path.isdir(name) or zipfile.is_zipfile(name)
|
||||
manifest_name = opj(dir, name, '__openerp__.py')
|
||||
zipfile_name = opj(dir, name)
|
||||
return os.path.isfile(manifest_name) or zipfile.is_zipfile(zipfile_name)
|
||||
return map(clean, filter(is_really_module, os.listdir(dir)))
|
||||
|
||||
plist = []
|
||||
|
|
|
@ -28,7 +28,6 @@ import threading
|
|||
|
||||
import openerp.sql_db
|
||||
import openerp.osv.orm
|
||||
import openerp.cron
|
||||
import openerp.tools
|
||||
import openerp.modules.db
|
||||
import openerp.tools.config
|
||||
|
@ -58,6 +57,21 @@ class Registry(object):
|
|||
self.db_name = db_name
|
||||
self.db = openerp.sql_db.db_connect(db_name)
|
||||
|
||||
# In monoprocess cron jobs flag (pooljobs)
|
||||
self.cron = False
|
||||
|
||||
# Inter-process signaling (used only when openerp.multi_process is True):
|
||||
# The `base_registry_signaling` sequence indicates the whole registry
|
||||
# must be reloaded.
|
||||
# The `base_cache_signaling sequence` indicates all caches must be
|
||||
# invalidated (i.e. cleared).
|
||||
self.base_registry_signaling_sequence = 1
|
||||
self.base_cache_signaling_sequence = 1
|
||||
|
||||
# Flag indicating if at least one model cache has been cleared.
|
||||
# Useful only in a multi-process context.
|
||||
self._any_cache_cleared = False
|
||||
|
||||
cr = self.db.cursor()
|
||||
has_unaccent = openerp.modules.db.has_unaccent(cr)
|
||||
if openerp.tools.config['unaccent'] and not has_unaccent:
|
||||
|
@ -112,7 +126,7 @@ class Registry(object):
|
|||
monitor the ir.cron model for future jobs. See openerp.cron for
|
||||
details.
|
||||
"""
|
||||
openerp.cron.schedule_wakeup(openerp.cron.WAKE_UP_NOW, self.db.dbname)
|
||||
self.cron = True
|
||||
|
||||
def clear_caches(self):
|
||||
""" Clear the caches
|
||||
|
@ -121,6 +135,36 @@ class Registry(object):
|
|||
"""
|
||||
for model in self.models.itervalues():
|
||||
model.clear_caches()
|
||||
# Special case for ir_ui_menu which does not use openerp.tools.ormcache.
|
||||
ir_ui_menu = self.models.get('ir.ui.menu')
|
||||
if ir_ui_menu:
|
||||
ir_ui_menu.clear_cache()
|
||||
|
||||
|
||||
# Useful only in a multi-process context.
|
||||
def reset_any_cache_cleared(self):
|
||||
self._any_cache_cleared = False
|
||||
|
||||
# Useful only in a multi-process context.
|
||||
def any_cache_cleared(self):
|
||||
return self._any_cache_cleared
|
||||
|
||||
@classmethod
|
||||
def setup_multi_process_signaling(cls, cr):
|
||||
if not openerp.multi_process:
|
||||
return
|
||||
|
||||
# Inter-process signaling:
|
||||
# The `base_registry_signaling` sequence indicates the whole registry
|
||||
# must be reloaded.
|
||||
# The `base_cache_signaling sequence` indicates all caches must be
|
||||
# invalidated (i.e. cleared).
|
||||
cr.execute("""SELECT sequence_name FROM information_schema.sequences WHERE sequence_name='base_registry_signaling'""")
|
||||
if not cr.fetchall():
|
||||
cr.execute("""CREATE SEQUENCE base_registry_signaling INCREMENT BY 1 START WITH 1""")
|
||||
cr.execute("""SELECT nextval('base_registry_signaling')""")
|
||||
cr.execute("""CREATE SEQUENCE base_cache_signaling INCREMENT BY 1 START WITH 1""")
|
||||
cr.execute("""SELECT nextval('base_cache_signaling')""")
|
||||
|
||||
@contextmanager
|
||||
def cursor(self, auto_commit=True):
|
||||
|
@ -182,6 +226,7 @@ class RegistryManager(object):
|
|||
|
||||
cr = registry.db.cursor()
|
||||
try:
|
||||
Registry.setup_multi_process_signaling(cr)
|
||||
registry.do_parent_store(cr)
|
||||
registry.get('ir.actions.report.xml').register_all(cr)
|
||||
cr.commit()
|
||||
|
@ -195,20 +240,11 @@ class RegistryManager(object):
|
|||
|
||||
@classmethod
|
||||
def delete(cls, db_name):
|
||||
"""Delete the registry linked to a given database.
|
||||
|
||||
This also cleans the associated caches. For good measure this also
|
||||
cancels the associated cron job. But please note that the cron job can
|
||||
be running and take some time before ending, and that you should not
|
||||
remove a registry if it can still be used by some thread. So it might
|
||||
be necessary to call yourself openerp.cron.Agent.cancel(db_name) and
|
||||
and join (i.e. wait for) the thread.
|
||||
"""
|
||||
"""Delete the registry linked to a given database. """
|
||||
with cls.registries_lock:
|
||||
if db_name in cls.registries:
|
||||
cls.registries[db_name].clear_caches()
|
||||
del cls.registries[db_name]
|
||||
openerp.cron.cancel(db_name)
|
||||
|
||||
@classmethod
|
||||
def delete_all(cls):
|
||||
|
@ -232,5 +268,71 @@ class RegistryManager(object):
|
|||
if db_name in cls.registries:
|
||||
cls.registries[db_name].clear_caches()
|
||||
|
||||
@classmethod
|
||||
def check_registry_signaling(cls, db_name):
|
||||
if openerp.multi_process and db_name in cls.registries:
|
||||
registry = cls.get(db_name, pooljobs=False)
|
||||
cr = registry.db.cursor()
|
||||
try:
|
||||
cr.execute("""
|
||||
SELECT base_registry_signaling.last_value,
|
||||
base_cache_signaling.last_value
|
||||
FROM base_registry_signaling, base_cache_signaling""")
|
||||
r, c = cr.fetchone()
|
||||
# Check if the model registry must be reloaded (e.g. after the
|
||||
# database has been updated by another process).
|
||||
if registry.base_registry_signaling_sequence != r:
|
||||
_logger.info("Reloading the model registry after database signaling.")
|
||||
# Don't run the cron in the Gunicorn worker.
|
||||
registry = cls.new(db_name, pooljobs=False)
|
||||
registry.base_registry_signaling_sequence = r
|
||||
# Check if the model caches must be invalidated (e.g. after a write
|
||||
# occured on another process). Don't clear right after a registry
|
||||
# has been reload.
|
||||
elif registry.base_cache_signaling_sequence != c:
|
||||
_logger.info("Invalidating all model caches after database signaling.")
|
||||
registry.base_cache_signaling_sequence = c
|
||||
registry.clear_caches()
|
||||
registry.reset_any_cache_cleared()
|
||||
# One possible reason caches have been invalidated is the
|
||||
# use of decimal_precision.write(), in which case we need
|
||||
# to refresh fields.float columns.
|
||||
for model in registry.models.values():
|
||||
for column in model._columns.values():
|
||||
if hasattr(column, 'digits_change'):
|
||||
column.digits_change(cr)
|
||||
finally:
|
||||
cr.close()
|
||||
|
||||
@classmethod
|
||||
def signal_caches_change(cls, db_name):
|
||||
if openerp.multi_process and db_name in cls.registries:
|
||||
# Check the registries if any cache has been cleared and signal it
|
||||
# through the database to other processes.
|
||||
registry = cls.get(db_name, pooljobs=False)
|
||||
if registry.any_cache_cleared():
|
||||
_logger.info("At least one model cache has been cleared, signaling through the database.")
|
||||
cr = registry.db.cursor()
|
||||
r = 1
|
||||
try:
|
||||
cr.execute("select nextval('base_cache_signaling')")
|
||||
r = cr.fetchone()[0]
|
||||
finally:
|
||||
cr.close()
|
||||
registry.base_cache_signaling_sequence = r
|
||||
registry.reset_any_cache_cleared()
|
||||
|
||||
@classmethod
|
||||
def signal_registry_change(cls, db_name):
|
||||
if openerp.multi_process and db_name in cls.registries:
|
||||
registry = cls.get(db_name, pooljobs=False)
|
||||
cr = registry.db.cursor()
|
||||
r = 1
|
||||
try:
|
||||
cr.execute("select nextval('base_registry_signaling')")
|
||||
r = cr.fetchone()[0]
|
||||
finally:
|
||||
cr.close()
|
||||
registry.base_registry_signaling_sequence = r
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||
|
|
|
@ -40,23 +40,6 @@ import openerp
|
|||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
def close_socket(sock):
|
||||
""" Closes a socket instance cleanly
|
||||
|
||||
:param sock: the network socket to close
|
||||
:type sock: socket.socket
|
||||
"""
|
||||
try:
|
||||
sock.shutdown(socket.SHUT_RDWR)
|
||||
except socket.error, e:
|
||||
# On OSX, socket shutdowns both sides if any side closes it
|
||||
# causing an error 57 'Socket is not connected' on shutdown
|
||||
# of the other side (or something), see
|
||||
# http://bugs.python.org/issue4397
|
||||
# note: stdlib fixed test, not behavior
|
||||
if e.errno != errno.ENOTCONN or platform.system() != 'Darwin':
|
||||
raise
|
||||
sock.close()
|
||||
|
||||
|
||||
#.apidoc title: Common Services: netsvc
|
||||
|
@ -67,9 +50,9 @@ def abort_response(dummy_1, description, dummy_2, details):
|
|||
raise openerp.osv.osv.except_osv(description, details)
|
||||
|
||||
class Service(object):
|
||||
""" Base class for *Local* services
|
||||
|
||||
Functionality here is trusted, no authentication.
|
||||
""" Base class for Local services
|
||||
Functionality here is trusted, no authentication.
|
||||
Workflow engine and reports subclass this.
|
||||
"""
|
||||
_services = {}
|
||||
def __init__(self, name):
|
||||
|
@ -145,7 +128,6 @@ class ColoredFormatter(DBFormatter):
|
|||
record.levelname = COLOR_PATTERN % (30 + fg_color, 40 + bg_color, record.levelname)
|
||||
return DBFormatter.format(self, record)
|
||||
|
||||
|
||||
def init_logger():
|
||||
from tools.translate import resetlocale
|
||||
resetlocale()
|
||||
|
@ -246,85 +228,6 @@ def init_alternative_logger():
|
|||
logger.addHandler(handler)
|
||||
logger.setLevel(logging.ERROR)
|
||||
|
||||
class Server:
|
||||
""" Generic interface for all servers with an event loop etc.
|
||||
Override this to impement http, net-rpc etc. servers.
|
||||
|
||||
Servers here must have threaded behaviour. start() must not block,
|
||||
there is no run().
|
||||
"""
|
||||
__is_started = False
|
||||
__servers = []
|
||||
__starter_threads = []
|
||||
|
||||
# we don't want blocking server calls (think select()) to
|
||||
# wait forever and possibly prevent exiting the process,
|
||||
# but instead we want a form of polling/busy_wait pattern, where
|
||||
# _server_timeout should be used as the default timeout for
|
||||
# all I/O blocking operations
|
||||
_busywait_timeout = 0.5
|
||||
|
||||
def __init__(self):
|
||||
Server.__servers.append(self)
|
||||
if Server.__is_started:
|
||||
# raise Exception('All instances of servers must be inited before the startAll()')
|
||||
# Since the startAll() won't be called again, allow this server to
|
||||
# init and then start it after 1sec (hopefully). Register that
|
||||
# timer thread in a list, so that we can abort the start if quitAll
|
||||
# is called in the meantime
|
||||
t = threading.Timer(1.0, self._late_start)
|
||||
t.name = 'Late start timer for %s' % str(self.__class__)
|
||||
Server.__starter_threads.append(t)
|
||||
t.start()
|
||||
|
||||
def start(self):
|
||||
_logger.debug("called stub Server.start")
|
||||
|
||||
def _late_start(self):
|
||||
self.start()
|
||||
for thr in Server.__starter_threads:
|
||||
if thr.finished.is_set():
|
||||
Server.__starter_threads.remove(thr)
|
||||
|
||||
def stop(self):
|
||||
_logger.debug("called stub Server.stop")
|
||||
|
||||
def stats(self):
|
||||
""" This function should return statistics about the server """
|
||||
return "%s: No statistics" % str(self.__class__)
|
||||
|
||||
@classmethod
|
||||
def startAll(cls):
|
||||
if cls.__is_started:
|
||||
return
|
||||
_logger.info("Starting %d services" % len(cls.__servers))
|
||||
for srv in cls.__servers:
|
||||
srv.start()
|
||||
cls.__is_started = True
|
||||
|
||||
@classmethod
|
||||
def quitAll(cls):
|
||||
if not cls.__is_started:
|
||||
return
|
||||
_logger.info("Stopping %d services" % len(cls.__servers))
|
||||
for thr in cls.__starter_threads:
|
||||
if not thr.finished.is_set():
|
||||
thr.cancel()
|
||||
cls.__starter_threads.remove(thr)
|
||||
|
||||
for srv in cls.__servers:
|
||||
srv.stop()
|
||||
cls.__is_started = False
|
||||
|
||||
@classmethod
|
||||
def allStats(cls):
|
||||
res = ["Servers %s" % ('stopped', 'started')[cls.__is_started]]
|
||||
res.extend(srv.stats() for srv in cls.__servers)
|
||||
return '\n'.join(res)
|
||||
|
||||
def _close_socket(self):
|
||||
close_socket(self.socket)
|
||||
|
||||
def replace_request_password(args):
|
||||
# password is always 3rd argument in a request, we replace it in RPC logs
|
||||
# so it's easier to forward logs for diagnostics/debugging purposes...
|
||||
|
|
|
@ -163,7 +163,7 @@ class boolean(_column):
|
|||
_logger.debug(
|
||||
"required=True is deprecated: making a boolean field"
|
||||
" `required` has no effect, as NULL values are "
|
||||
"automatically turned into False.")
|
||||
"automatically turned into False. args: %r",args)
|
||||
|
||||
class integer(_column):
|
||||
_type = 'integer'
|
||||
|
|
|
@ -916,7 +916,16 @@ class BaseModel(object):
|
|||
else:
|
||||
new.extend(cls.__dict__.get(s, []))
|
||||
nattr[s] = new
|
||||
|
||||
# Keep links to non-inherited constraints, e.g. useful when exporting translations
|
||||
nattr['_local_constraints'] = cls.__dict__.get('_constraints', [])
|
||||
nattr['_local_sql_constraints'] = cls.__dict__.get('_sql_constraints', [])
|
||||
|
||||
cls = type(name, (cls, parent_class), dict(nattr, _register=False))
|
||||
else:
|
||||
cls._local_constraints = getattr(cls, '_constraints', [])
|
||||
cls._local_sql_constraints = getattr(cls, '_sql_constraints', [])
|
||||
|
||||
if not getattr(cls, '_original_module', None):
|
||||
cls._original_module = cls._module
|
||||
obj = object.__new__(cls)
|
||||
|
@ -1515,6 +1524,8 @@ class BaseModel(object):
|
|||
error_msgs = []
|
||||
for constraint in self._constraints:
|
||||
fun, msg, fields = constraint
|
||||
# We don't pass around the context here: validation code
|
||||
# must always yield the same results.
|
||||
if not fun(self, cr, uid, ids):
|
||||
# Check presence of __call__ directly instead of using
|
||||
# callable() because it will be deprecated as of Python 3.0
|
||||
|
@ -2498,6 +2509,7 @@ class BaseModel(object):
|
|||
try:
|
||||
getattr(self, '_ormcache')
|
||||
self._ormcache = {}
|
||||
self.pool._any_cache_cleared = True
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
|
@ -3223,7 +3235,7 @@ class BaseModel(object):
|
|||
|
||||
|
||||
def _create_table(self, cr):
|
||||
cr.execute('CREATE TABLE "%s" (id SERIAL NOT NULL, PRIMARY KEY(id)) WITHOUT OIDS' % (self._table,))
|
||||
cr.execute('CREATE TABLE "%s" (id SERIAL NOT NULL, PRIMARY KEY(id))' % (self._table,))
|
||||
cr.execute(("COMMENT ON TABLE \"%s\" IS %%s" % self._table), (self._description,))
|
||||
_schema.debug("Table '%s': created", self._table)
|
||||
|
||||
|
@ -3307,7 +3319,7 @@ class BaseModel(object):
|
|||
raise except_orm('Programming Error', ('Many2Many destination model does not exist: `%s`') % (f._obj,))
|
||||
dest_model = self.pool.get(f._obj)
|
||||
ref = dest_model._table
|
||||
cr.execute('CREATE TABLE "%s" ("%s" INTEGER NOT NULL, "%s" INTEGER NOT NULL, UNIQUE("%s","%s")) WITH OIDS' % (m2m_tbl, col1, col2, col1, col2))
|
||||
cr.execute('CREATE TABLE "%s" ("%s" INTEGER NOT NULL, "%s" INTEGER NOT NULL, UNIQUE("%s","%s"))' % (m2m_tbl, col1, col2, col1, col2))
|
||||
# create foreign key references with ondelete=cascade, unless the targets are SQL views
|
||||
cr.execute("SELECT relkind FROM pg_class WHERE relkind IN ('v') AND relname=%s", (ref,))
|
||||
if not cr.fetchall():
|
||||
|
|
|
@ -23,6 +23,8 @@
|
|||
|
||||
from functools import wraps
|
||||
import logging
|
||||
import threading
|
||||
|
||||
from psycopg2 import IntegrityError, errorcodes
|
||||
|
||||
import orm
|
||||
|
@ -168,6 +170,7 @@ class object_proxy(object):
|
|||
|
||||
@check
|
||||
def execute(self, db, uid, obj, method, *args, **kw):
|
||||
threading.currentThread().dbname = db
|
||||
cr = pooler.get_db(db).cursor()
|
||||
try:
|
||||
try:
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
#
|
||||
# OpenERP, Open Source Management Solution
|
||||
# Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
|
||||
# Copyright (C) 2010-2012 OpenERP SA (<http://www.openerp.com>)
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
|
@ -15,7 +16,7 @@
|
|||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
|
@ -23,17 +24,16 @@ import logging
|
|||
import threading
|
||||
import time
|
||||
|
||||
import http_server
|
||||
import cron
|
||||
import netrpc_server
|
||||
import web_services
|
||||
import websrv_lib
|
||||
import web_services
|
||||
import wsgi_server
|
||||
|
||||
import openerp.cron
|
||||
import openerp.modules
|
||||
import openerp.netsvc
|
||||
import openerp.osv
|
||||
import openerp.tools
|
||||
import openerp.service.wsgi_server
|
||||
|
||||
#.apidoc title: RPC Services
|
||||
|
||||
|
@ -61,6 +61,7 @@ Maybe you forgot to add those addons in your addons_path configuration."""
|
|||
_logger.exception('Failed to load server-wide module `%s`.%s', m, msg)
|
||||
|
||||
start_internal_done = False
|
||||
main_thread_id = threading.currentThread().ident
|
||||
|
||||
def start_internal():
|
||||
global start_internal_done
|
||||
|
@ -68,38 +69,32 @@ def start_internal():
|
|||
return
|
||||
openerp.netsvc.init_logger()
|
||||
openerp.modules.loading.open_openerp_namespace()
|
||||
|
||||
# Instantiate local services (this is a legacy design).
|
||||
openerp.osv.osv.start_object_proxy()
|
||||
# Export (for RPC) services.
|
||||
web_services.start_web_services()
|
||||
web_services.start_service()
|
||||
|
||||
load_server_wide_modules()
|
||||
start_internal_done = True
|
||||
|
||||
def start_services():
|
||||
""" Start all services including http, netrpc and cron """
|
||||
start_internal()
|
||||
|
||||
# Initialize the HTTP stack.
|
||||
netrpc_server.init_servers()
|
||||
|
||||
# Start the main cron thread.
|
||||
if openerp.conf.max_cron_threads:
|
||||
openerp.cron.start_master_thread()
|
||||
|
||||
# Start the top-level servers threads (normally HTTP, HTTPS, and NETRPC).
|
||||
openerp.netsvc.Server.startAll()
|
||||
|
||||
# Initialize the NETRPC server.
|
||||
netrpc_server.start_service()
|
||||
# Start the WSGI server.
|
||||
openerp.service.wsgi_server.start_server()
|
||||
wsgi_server.start_service()
|
||||
# Start the main cron thread.
|
||||
cron.start_service()
|
||||
|
||||
def stop_services():
|
||||
""" Stop all services. """
|
||||
# stop scheduling new jobs; we will have to wait for the jobs to complete below
|
||||
openerp.cron.cancel_all()
|
||||
# stop services
|
||||
cron.stop_service()
|
||||
netrpc_server.stop_service()
|
||||
wsgi_server.stop_service()
|
||||
|
||||
openerp.netsvc.Server.quitAll()
|
||||
openerp.service.wsgi_server.stop_server()
|
||||
config = openerp.tools.config
|
||||
_logger.info("Initiating shutdown")
|
||||
_logger.info("Hit CTRL-C again or send a second signal to force the shutdown.")
|
||||
logging.shutdown()
|
||||
|
@ -107,8 +102,9 @@ def stop_services():
|
|||
# Manually join() all threads before calling sys.exit() to allow a second signal
|
||||
# to trigger _force_quit() in case some non-daemon threads won't exit cleanly.
|
||||
# threading.Thread.join() should not mask signals (at least in python 2.5).
|
||||
me = threading.currentThread()
|
||||
for thread in threading.enumerate():
|
||||
if thread != threading.currentThread() and not thread.isDaemon():
|
||||
if thread != me and not thread.isDaemon() and thread.ident != main_thread_id:
|
||||
while thread.isAlive():
|
||||
# Need a busyloop here as thread.join() masks signals
|
||||
# and would prevent the forced shutdown.
|
||||
|
@ -119,10 +115,9 @@ def stop_services():
|
|||
|
||||
def start_services_workers():
|
||||
import openerp.service.workers
|
||||
openerp.multi_process = True # Nah!
|
||||
openerp.multi_process = True
|
||||
|
||||
openerp.service.workers.Multicorn(openerp.service.wsgi_server.application).run()
|
||||
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Management Solution
|
||||
# Copyright (C) 2004-2011 OpenERP SA (<http://www.openerp.com>)
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
""" Cron jobs scheduling
|
||||
|
||||
Cron jobs are defined in the ir_cron table/model. This module deals with all
|
||||
cron jobs, for all databases of a single OpenERP server instance.
|
||||
|
||||
"""
|
||||
|
||||
import logging
|
||||
import threading
|
||||
import time
|
||||
|
||||
import openerp
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
SLEEP_INTERVAL = 60 # 1 min
|
||||
|
||||
def cron_runner(number):
|
||||
while True:
|
||||
time.sleep(SLEEP_INTERVAL + number) # Steve Reich timing style
|
||||
registries = openerp.modules.registry.RegistryManager.registries
|
||||
_logger.debug('cron%d polling for jobs', number)
|
||||
for db_name, registry in registries.items():
|
||||
while True and registry.cron:
|
||||
# acquired = openerp.addons.base.ir.ir_cron.ir_cron._acquire_job(db_name)
|
||||
# TODO why isnt openerp.addons.base defined ?
|
||||
import sys
|
||||
base = sys.modules['addons.base']
|
||||
acquired = base.ir.ir_cron.ir_cron._acquire_job(db_name)
|
||||
if not acquired:
|
||||
break
|
||||
|
||||
def start_service():
|
||||
""" Start the above runner function in a daemon thread.
|
||||
|
||||
The thread is a typical daemon thread: it will never quit and must be
|
||||
terminated when the main process exits - with no consequence (the processing
|
||||
threads it spawns are not marked daemon).
|
||||
|
||||
"""
|
||||
for i in range(openerp.tools.config['max_cron_threads']):
|
||||
def target():
|
||||
cron_runner(i)
|
||||
t = threading.Thread(target=target, name="openerp.service.cron.cron%d" % i)
|
||||
t.setDaemon(True)
|
||||
t.start()
|
||||
_logger.debug("cron%d started!" % i)
|
||||
|
||||
def stop_service():
|
||||
pass
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
|
@ -176,6 +176,4 @@ class OpenERPAuthProvider(AuthProvider):
|
|||
self.auth_tries += 1
|
||||
raise AuthRequiredExc(atype='Basic', realm=self.realm)
|
||||
|
||||
#eof
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||
|
|
|
@ -38,6 +38,103 @@ import openerp.tools as tools
|
|||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
def close_socket(sock):
|
||||
""" Closes a socket instance cleanly
|
||||
|
||||
:param sock: the network socket to close
|
||||
:type sock: socket.socket
|
||||
"""
|
||||
try:
|
||||
sock.shutdown(socket.SHUT_RDWR)
|
||||
except socket.error, e:
|
||||
# On OSX, socket shutdowns both sides if any side closes it
|
||||
# causing an error 57 'Socket is not connected' on shutdown
|
||||
# of the other side (or something), see
|
||||
# http://bugs.python.org/issue4397
|
||||
# note: stdlib fixed test, not behavior
|
||||
if e.errno != errno.ENOTCONN or platform.system() not in ['Darwin', 'Windows']:
|
||||
raise
|
||||
sock.close()
|
||||
|
||||
class Server:
|
||||
""" Generic interface for all servers with an event loop etc.
|
||||
Override this to impement http, net-rpc etc. servers.
|
||||
|
||||
Servers here must have threaded behaviour. start() must not block,
|
||||
there is no run().
|
||||
"""
|
||||
__is_started = False
|
||||
__servers = []
|
||||
__starter_threads = []
|
||||
|
||||
# we don't want blocking server calls (think select()) to
|
||||
# wait forever and possibly prevent exiting the process,
|
||||
# but instead we want a form of polling/busy_wait pattern, where
|
||||
# _server_timeout should be used as the default timeout for
|
||||
# all I/O blocking operations
|
||||
_busywait_timeout = 0.5
|
||||
|
||||
def __init__(self):
|
||||
Server.__servers.append(self)
|
||||
if Server.__is_started:
|
||||
# raise Exception('All instances of servers must be inited before the startAll()')
|
||||
# Since the startAll() won't be called again, allow this server to
|
||||
# init and then start it after 1sec (hopefully). Register that
|
||||
# timer thread in a list, so that we can abort the start if quitAll
|
||||
# is called in the meantime
|
||||
t = threading.Timer(1.0, self._late_start)
|
||||
t.name = 'Late start timer for %s' % str(self.__class__)
|
||||
Server.__starter_threads.append(t)
|
||||
t.start()
|
||||
|
||||
def start(self):
|
||||
_logger.debug("called stub Server.start")
|
||||
|
||||
def _late_start(self):
|
||||
self.start()
|
||||
for thr in Server.__starter_threads:
|
||||
if thr.finished.is_set():
|
||||
Server.__starter_threads.remove(thr)
|
||||
|
||||
def stop(self):
|
||||
_logger.debug("called stub Server.stop")
|
||||
|
||||
def stats(self):
|
||||
""" This function should return statistics about the server """
|
||||
return "%s: No statistics" % str(self.__class__)
|
||||
|
||||
@classmethod
|
||||
def startAll(cls):
|
||||
if cls.__is_started:
|
||||
return
|
||||
_logger.info("Starting %d services" % len(cls.__servers))
|
||||
for srv in cls.__servers:
|
||||
srv.start()
|
||||
cls.__is_started = True
|
||||
|
||||
@classmethod
|
||||
def quitAll(cls):
|
||||
if not cls.__is_started:
|
||||
return
|
||||
_logger.info("Stopping %d services" % len(cls.__servers))
|
||||
for thr in cls.__starter_threads:
|
||||
if not thr.finished.is_set():
|
||||
thr.cancel()
|
||||
cls.__starter_threads.remove(thr)
|
||||
|
||||
for srv in cls.__servers:
|
||||
srv.stop()
|
||||
cls.__is_started = False
|
||||
|
||||
@classmethod
|
||||
def allStats(cls):
|
||||
res = ["Servers %s" % ('stopped', 'started')[cls.__is_started]]
|
||||
res.extend(srv.stats() for srv in cls.__servers)
|
||||
return '\n'.join(res)
|
||||
|
||||
def _close_socket(self):
|
||||
close_socket(self.socket)
|
||||
|
||||
class TinySocketClientThread(threading.Thread):
|
||||
def __init__(self, sock, threads):
|
||||
spn = sock and sock.getpeername()
|
||||
|
@ -99,10 +196,10 @@ def netrpc_handle_exception_legacy(e):
|
|||
return 'AccessDenied ' + str(e)
|
||||
return openerp.tools.exception_to_unicode(e)
|
||||
|
||||
class TinySocketServerThread(threading.Thread,netsvc.Server):
|
||||
class TinySocketServerThread(threading.Thread,Server):
|
||||
def __init__(self, interface, port, secure=False):
|
||||
threading.Thread.__init__(self, name="NetRPCDaemon-%d"%port)
|
||||
netsvc.Server.__init__(self)
|
||||
Server.__init__(self)
|
||||
self.__port = port
|
||||
self.__interface = interface
|
||||
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
|
@ -157,11 +254,12 @@ class TinySocketServerThread(threading.Thread,netsvc.Server):
|
|||
|
||||
netrpcd = None
|
||||
|
||||
def init_servers():
|
||||
def start_service():
|
||||
global netrpcd
|
||||
if tools.config.get('netrpc', False):
|
||||
netrpcd = TinySocketServerThread(
|
||||
tools.config.get('netrpc_interface', ''),
|
||||
int(tools.config.get('netrpc_port', 8070)))
|
||||
netrpcd = TinySocketServerThread(tools.config.get('netrpc_interface', ''), int(tools.config.get('netrpc_port', 8070)))
|
||||
|
||||
def stop_service():
|
||||
Server.quitAll()
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||
|
|
|
@ -165,6 +165,7 @@ class db(netsvc.ExportService):
|
|||
|
||||
def exp_duplicate_database(self, db_original_name, db_name):
|
||||
_logger.info('Duplicate database `%s` to `%s`.', db_original_name, db_name)
|
||||
sql_db.close_db(db_original_name)
|
||||
db = sql_db.db_connect('postgres')
|
||||
cr = db.cursor()
|
||||
try:
|
||||
|
@ -603,68 +604,13 @@ class objects_proxy(netsvc.ExportService):
|
|||
raise NameError("Method not available %s" % method)
|
||||
security.check(db,uid,passwd)
|
||||
assert openerp.osv.osv.service, "The object_proxy class must be started with start_object_proxy."
|
||||
openerp.modules.registry.RegistryManager.check_registry_signaling(db)
|
||||
fn = getattr(openerp.osv.osv.service, method)
|
||||
res = fn(db, uid, *params)
|
||||
openerp.modules.registry.RegistryManager.signal_caches_change(db)
|
||||
return res
|
||||
|
||||
|
||||
#
|
||||
# Wizard ID: 1
|
||||
# - None = end of wizard
|
||||
#
|
||||
# Wizard Type: 'form'
|
||||
# - form
|
||||
# - print
|
||||
#
|
||||
# Wizard datas: {}
|
||||
# TODO: change local request to OSE request/reply pattern
|
||||
#
|
||||
class wizard(netsvc.ExportService):
|
||||
def __init__(self, name='wizard'):
|
||||
netsvc.ExportService.__init__(self,name)
|
||||
self.id = 0
|
||||
self.wiz_datas = {}
|
||||
self.wiz_name = {}
|
||||
self.wiz_uid = {}
|
||||
|
||||
def dispatch(self, method, params):
|
||||
(db, uid, passwd ) = params[0:3]
|
||||
threading.current_thread().uid = uid
|
||||
params = params[3:]
|
||||
if method not in ['execute','create']:
|
||||
raise KeyError("Method not supported %s" % method)
|
||||
security.check(db,uid,passwd)
|
||||
fn = getattr(self, 'exp_'+method)
|
||||
res = fn(db, uid, *params)
|
||||
return res
|
||||
|
||||
def _execute(self, db, uid, wiz_id, datas, action, context):
|
||||
self.wiz_datas[wiz_id].update(datas)
|
||||
wiz = netsvc.LocalService('wizard.'+self.wiz_name[wiz_id])
|
||||
return wiz.execute(db, uid, self.wiz_datas[wiz_id], action, context)
|
||||
|
||||
def exp_create(self, db, uid, wiz_name, datas=None):
|
||||
if not datas:
|
||||
datas={}
|
||||
#FIXME: this is not thread-safe
|
||||
self.id += 1
|
||||
self.wiz_datas[self.id] = {}
|
||||
self.wiz_name[self.id] = wiz_name
|
||||
self.wiz_uid[self.id] = uid
|
||||
return self.id
|
||||
|
||||
def exp_execute(self, db, uid, wiz_id, datas, action='init', context=None):
|
||||
if not context:
|
||||
context={}
|
||||
|
||||
if wiz_id in self.wiz_uid:
|
||||
if self.wiz_uid[wiz_id] == uid:
|
||||
return self._execute(db, uid, wiz_id, datas, action, context)
|
||||
else:
|
||||
raise openerp.exceptions.AccessDenied()
|
||||
else:
|
||||
raise openerp.exceptions.Warning('Wizard not found.')
|
||||
|
||||
#
|
||||
# TODO: set a maximum report number per user to avoid DOS attacks
|
||||
#
|
||||
|
@ -686,8 +632,10 @@ class report_spool(netsvc.ExportService):
|
|||
if method not in ['report', 'report_get', 'render_report']:
|
||||
raise KeyError("Method not supported %s" % method)
|
||||
security.check(db,uid,passwd)
|
||||
openerp.modules.registry.RegistryManager.check_registry_signaling(db)
|
||||
fn = getattr(self, 'exp_' + method)
|
||||
res = fn(db, uid, *params)
|
||||
openerp.modules.registry.RegistryManager.signal_caches_change(db)
|
||||
return res
|
||||
|
||||
def exp_render_report(self, db, uid, object, ids, datas=None, context=None):
|
||||
|
@ -799,13 +747,11 @@ class report_spool(netsvc.ExportService):
|
|||
raise Exception, 'ReportNotFound'
|
||||
|
||||
|
||||
def start_web_services():
|
||||
def start_service():
|
||||
db()
|
||||
common()
|
||||
objects_proxy()
|
||||
wizard()
|
||||
report_spool()
|
||||
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||
|
||||
|
|
|
@ -16,6 +16,10 @@ import sys
|
|||
import time
|
||||
|
||||
import werkzeug.serving
|
||||
try:
|
||||
from setproctitle import setproctitle
|
||||
except ImportError:
|
||||
setproctitle = lambda x: None
|
||||
|
||||
import openerp
|
||||
import openerp.tools.config as config
|
||||
|
@ -134,7 +138,7 @@ class Multicorn(object):
|
|||
def process_spawn(self):
|
||||
while len(self.workers_http) < self.population:
|
||||
self.worker_spawn(WorkerHTTP, self.workers_http)
|
||||
while len(self.workers_cron) < 1: # config option ?
|
||||
while len(self.workers_cron) < config['max_cron_threads']:
|
||||
self.worker_spawn(WorkerCron, self.workers_cron)
|
||||
|
||||
def sleep(self):
|
||||
|
@ -189,8 +193,7 @@ class Multicorn(object):
|
|||
for pid in self.workers.keys():
|
||||
self.worker_kill(pid, signal.SIGTERM)
|
||||
self.socket.close()
|
||||
import __main__
|
||||
__main__.quit_signals_received = 1
|
||||
openerp.cli.server.quit_signals_received = 1
|
||||
|
||||
def run(self):
|
||||
self.start()
|
||||
|
@ -252,7 +255,7 @@ class Worker(object):
|
|||
# Reset the worker if it consumes too much memory (e.g. caused by a memory leak).
|
||||
rss, vms = psutil.Process(os.getpid()).get_memory_info()
|
||||
if vms > config['limit_memory_soft']:
|
||||
_logger.info('Virtual memory consumption too high, rebooting the worker.')
|
||||
_logger.info('Worker (%d) virtual memory limit (%s) reached.', self.pid, vms)
|
||||
self.alive = False # Commit suicide after the request.
|
||||
|
||||
# VMS and RLIMIT_AS are the same thing: virtual memory, a.k.a. address space
|
||||
|
@ -263,7 +266,8 @@ class Worker(object):
|
|||
r = resource.getrusage(resource.RUSAGE_SELF)
|
||||
cpu_time = r.ru_utime + r.ru_stime
|
||||
def time_expired(n, stack):
|
||||
_logger.info('CPU time limit exceeded.')
|
||||
_logger.info('Worker (%d) CPU time limit (%s) reached.', config['limit_time_cpu'])
|
||||
# We dont suicide in such case
|
||||
raise Exception('CPU time limit exceeded.')
|
||||
signal.signal(signal.SIGXCPU, time_expired)
|
||||
soft, hard = resource.getrlimit(resource.RLIMIT_CPU)
|
||||
|
@ -274,6 +278,7 @@ class Worker(object):
|
|||
|
||||
def start(self):
|
||||
self.pid = os.getpid()
|
||||
setproctitle('openerp: %s %s' % (self.__class__.__name__, self.pid))
|
||||
_logger.info("Worker %s (%s) alive", self.__class__.__name__, self.pid)
|
||||
# Reseed the random number generator
|
||||
random.seed()
|
||||
|
@ -346,19 +351,27 @@ class WorkerBaseWSGIServer(werkzeug.serving.BaseWSGIServer):
|
|||
class WorkerCron(Worker):
|
||||
""" Cron workers """
|
||||
def sleep(self):
|
||||
time.sleep(60)
|
||||
interval = 60 + self.pid % 10 # chorus effect
|
||||
time.sleep(interval)
|
||||
|
||||
def process_work(self):
|
||||
_logger.debug("WorkerCron (%s) polling for jobs", self.pid)
|
||||
if config['db_name']:
|
||||
db_names = config['db_name'].split(',')
|
||||
else:
|
||||
db_names = openerp.netsvc.ExportService._services['db'].exp_list(True)
|
||||
for db_name in db_names:
|
||||
while True:
|
||||
# TODO Each job should be considered as one request in multiprocessing
|
||||
acquired = openerp.addons.base.ir.ir_cron.ir_cron._acquire_job(db_name)
|
||||
# acquired = openerp.addons.base.ir.ir_cron.ir_cron._acquire_job(db_name)
|
||||
# TODO why isnt openerp.addons.base defined ?
|
||||
import base
|
||||
acquired = base.ir.ir_cron.ir_cron._acquire_job(db_name)
|
||||
if not acquired:
|
||||
break
|
||||
# dont keep cursors in multi database mode
|
||||
if len(db_names) > 1:
|
||||
openerp.sql_db.close_db(db_name)
|
||||
# TODO Each job should be considered as one request instead of each db
|
||||
self.request_count += 1
|
||||
|
||||
def start(self):
|
||||
|
|
|
@ -428,14 +428,14 @@ def serve():
|
|||
_logger.info('HTTP service (werkzeug) running on %s:%s', interface, port)
|
||||
httpd.serve_forever()
|
||||
|
||||
def start_server():
|
||||
def start_service():
|
||||
""" Call serve() in its own thread.
|
||||
|
||||
The WSGI server can be shutdown with stop_server() below.
|
||||
"""
|
||||
threading.Thread(target=serve).start()
|
||||
|
||||
def stop_server():
|
||||
def stop_service():
|
||||
""" Initiate the shutdown of the WSGI server.
|
||||
|
||||
The server is supposed to have been started by start_server() above.
|
||||
|
|
|
@ -57,10 +57,12 @@ class ormcache(object):
|
|||
try:
|
||||
key = args[self.skiparg-2:]
|
||||
del d[key]
|
||||
self2.pool._any_cache_cleared = True
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
d.clear()
|
||||
self2.pool._any_cache_cleared = True
|
||||
|
||||
class ormcache_multi(ormcache):
|
||||
def __init__(self, skiparg=2, size=8192, multi=3):
|
||||
|
|
|
@ -146,8 +146,9 @@ class configmanager(object):
|
|||
help="specify the TCP IP address for the NETRPC protocol")
|
||||
group.add_option("--netrpc-port", dest="netrpc_port", my_default=8070,
|
||||
help="specify the TCP port for the NETRPC protocol", type="int")
|
||||
group.add_option("--no-netrpc", dest="netrpc", action="store_false", my_default=True,
|
||||
help="disable the NETRPC protocol")
|
||||
# Needed a few day for runbot and saas
|
||||
group.add_option("--no-netrpc", dest="netrpc", action="store_false", my_default=False, help="disable the NETRPC protocol")
|
||||
group.add_option("--netrpc", dest="netrpc", action="store_true", my_default=False, help="enable the NETRPC protocol")
|
||||
parser.add_option_group(group)
|
||||
|
||||
# WEB
|
||||
|
@ -269,8 +270,8 @@ class configmanager(object):
|
|||
"osv_memory tables. This is a decimal value expressed in hours, "
|
||||
"and the default is 1 hour.",
|
||||
type="float")
|
||||
group.add_option("--max-cron-threads", dest="max_cron_threads", my_default=4,
|
||||
help="Maximum number of threads processing concurrently cron jobs.",
|
||||
group.add_option("--max-cron-threads", dest="max_cron_threads", my_default=2,
|
||||
help="Maximum number of threads processing concurrently cron jobs (default 2).",
|
||||
type="int")
|
||||
group.add_option("--unaccent", dest="unaccent", my_default=False, action="store_true",
|
||||
help="Use the unaccent function provided by the database when available.")
|
||||
|
@ -282,19 +283,19 @@ class configmanager(object):
|
|||
help="Specify the number of workers, 0 disable prefork mode.",
|
||||
type="int")
|
||||
group.add_option("--limit-memory-soft", dest="limit_memory_soft", my_default=640 * 1024 * 1024,
|
||||
help="Maximum allowed virtual memory per worker, when reached the worker be reset after the current request.",
|
||||
help="Maximum allowed virtual memory per worker, when reached the worker be reset after the current request (default 640M).",
|
||||
type="int")
|
||||
group.add_option("--limit-memory-hard", dest="limit_memory_hard", my_default=768 * 1024 * 1024,
|
||||
help="Maximum allowed virtual memory per worker, when reached, any memory allocation will fail.",
|
||||
help="Maximum allowed virtual memory per worker, when reached, any memory allocation will fail (default 768M).",
|
||||
type="int")
|
||||
group.add_option("--limit-time-cpu", dest="limit_time_cpu", my_default=60,
|
||||
help="Maximum allowed CPU time per request.",
|
||||
help="Maximum allowed CPU time per request (default 60).",
|
||||
type="int")
|
||||
group.add_option("--limit-time-real", dest="limit_time_real", my_default=60,
|
||||
help="Maximum allowed Real time per request. ",
|
||||
group.add_option("--limit-time-real", dest="limit_time_real", my_default=120,
|
||||
help="Maximum allowed Real time per request (default 120).",
|
||||
type="int")
|
||||
group.add_option("--limit-request", dest="limit_request", my_default=8192,
|
||||
help="Maximum number of request to be processed per worker.",
|
||||
help="Maximum number of request to be processed per worker (default 8192).",
|
||||
type="int")
|
||||
parser.add_option_group(group)
|
||||
|
||||
|
@ -479,8 +480,6 @@ class configmanager(object):
|
|||
if opt.save:
|
||||
self.save()
|
||||
|
||||
openerp.conf.max_cron_threads = self.options['max_cron_threads']
|
||||
|
||||
openerp.conf.addons_paths = self.options['addons_path'].split(',')
|
||||
if opt.server_wide_modules:
|
||||
openerp.conf.server_wide_modules = map(lambda m: m.strip(), opt.server_wide_modules.split(','))
|
||||
|
|
|
@ -68,7 +68,7 @@ def image_resize_image(base64_source, size=(1024, 1024), encoding='base64', file
|
|||
return base64_source
|
||||
# create a thumbnail: will resize and keep ratios, then sharpen for better looking result
|
||||
image.thumbnail(size, Image.ANTIALIAS)
|
||||
sharpener = ImageEnhance.Sharpness(image.convert('RGB'))
|
||||
sharpener = ImageEnhance.Sharpness(image.convert('RGBA'))
|
||||
image = sharpener.enhance(2.0)
|
||||
# create a transparent image for background
|
||||
background = Image.new('RGBA', size, (255, 255, 255, 0))
|
||||
|
|
|
@ -27,6 +27,7 @@ Miscellaneous tools used by OpenERP.
|
|||
"""
|
||||
|
||||
from functools import wraps
|
||||
import cProfile
|
||||
import subprocess
|
||||
import logging
|
||||
import os
|
||||
|
@ -592,16 +593,10 @@ class profile(object):
|
|||
def __call__(self, f):
|
||||
@wraps(f)
|
||||
def wrapper(*args, **kwargs):
|
||||
class profile_wrapper(object):
|
||||
def __init__(self):
|
||||
self.result = None
|
||||
def __call__(self):
|
||||
self.result = f(*args, **kwargs)
|
||||
pw = profile_wrapper()
|
||||
import cProfile
|
||||
fname = self.fname or ("%s.cprof" % (f.func_name,))
|
||||
cProfile.runctx('pw()', globals(), locals(), filename=fname)
|
||||
return pw.result
|
||||
profile = cProfile.Profile()
|
||||
result = profile.runcall(f, *args, **kwargs)
|
||||
profile.dump_stats(self.fname or ("%s.cprof" % (f.func_name,)))
|
||||
return result
|
||||
|
||||
return wrapper
|
||||
|
||||
|
|
|
@ -1,269 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#Copyright (c) 2004-2005, CherryPy Team (team@cherrypy.org)
|
||||
#All rights reserved.
|
||||
#
|
||||
#Redistribution and use in source and binary forms, with or without
|
||||
#modification, are permitted provided that the following conditions are met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the above copyright notice,
|
||||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
# * Neither the name of the CherryPy Team nor the names of its contributors
|
||||
# may be used to endorse or promote products derived from this software
|
||||
# without specific prior written permission.
|
||||
#
|
||||
#THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
#ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
#WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
#DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
#FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
#DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
#SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
#CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
#OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
#OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
# This is a backport of Python-2.4's threading.local() implementation
|
||||
|
||||
"""Thread-local objects
|
||||
|
||||
(Note that this module provides a Python version of thread
|
||||
threading.local class. Depending on the version of Python you're
|
||||
using, there may be a faster one available. You should always import
|
||||
the local class from threading.)
|
||||
|
||||
Thread-local objects support the management of thread-local data.
|
||||
If you have data that you want to be local to a thread, simply create
|
||||
a thread-local object and use its attributes:
|
||||
|
||||
>>> mydata = local()
|
||||
>>> mydata.number = 42
|
||||
>>> mydata.number
|
||||
42
|
||||
|
||||
You can also access the local-object's dictionary:
|
||||
|
||||
>>> mydata.__dict__
|
||||
{'number': 42}
|
||||
>>> mydata.__dict__.setdefault('widgets', [])
|
||||
[]
|
||||
>>> mydata.widgets
|
||||
[]
|
||||
|
||||
What's important about thread-local objects is that their data are
|
||||
local to a thread. If we access the data in a different thread:
|
||||
|
||||
>>> log = []
|
||||
>>> def f():
|
||||
... items = mydata.__dict__.items()
|
||||
... items.sort()
|
||||
... log.append(items)
|
||||
... mydata.number = 11
|
||||
... log.append(mydata.number)
|
||||
|
||||
>>> import threading
|
||||
>>> thread = threading.Thread(target=f)
|
||||
>>> thread.start()
|
||||
>>> thread.join()
|
||||
>>> log
|
||||
[[], 11]
|
||||
|
||||
we get different data. Furthermore, changes made in the other thread
|
||||
don't affect data seen in this thread:
|
||||
|
||||
>>> mydata.number
|
||||
42
|
||||
|
||||
Of course, values you get from a local object, including a __dict__
|
||||
attribute, are for whatever thread was current at the time the
|
||||
attribute was read. For that reason, you generally don't want to save
|
||||
these values across threads, as they apply only to the thread they
|
||||
came from.
|
||||
|
||||
You can create custom local objects by subclassing the local class:
|
||||
|
||||
>>> class MyLocal(local):
|
||||
... number = 2
|
||||
... initialized = False
|
||||
... def __init__(self, **kw):
|
||||
... if self.initialized:
|
||||
... raise SystemError('__init__ called too many times')
|
||||
... self.initialized = True
|
||||
... self.__dict__.update(kw)
|
||||
... def squared(self):
|
||||
... return self.number ** 2
|
||||
|
||||
This can be useful to support default values, methods and
|
||||
initialization. Note that if you define an __init__ method, it will be
|
||||
called each time the local object is used in a separate thread. This
|
||||
is necessary to initialize each thread's dictionary.
|
||||
|
||||
Now if we create a local object:
|
||||
|
||||
>>> mydata = MyLocal(color='red')
|
||||
|
||||
Now we have a default number:
|
||||
|
||||
>>> mydata.number
|
||||
2
|
||||
|
||||
an initial color:
|
||||
|
||||
>>> mydata.color
|
||||
'red'
|
||||
>>> del mydata.color
|
||||
|
||||
And a method that operates on the data:
|
||||
|
||||
>>> mydata.squared()
|
||||
4
|
||||
|
||||
As before, we can access the data in a separate thread:
|
||||
|
||||
>>> log = []
|
||||
>>> thread = threading.Thread(target=f)
|
||||
>>> thread.start()
|
||||
>>> thread.join()
|
||||
>>> log
|
||||
[[('color', 'red'), ('initialized', True)], 11]
|
||||
|
||||
without affecting this thread's data:
|
||||
|
||||
>>> mydata.number
|
||||
2
|
||||
>>> mydata.color
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
AttributeError: 'MyLocal' object has no attribute 'color'
|
||||
|
||||
Note that subclasses can define slots, but they are not thread
|
||||
local. They are shared across threads:
|
||||
|
||||
>>> class MyLocal(local):
|
||||
... __slots__ = 'number'
|
||||
|
||||
>>> mydata = MyLocal()
|
||||
>>> mydata.number = 42
|
||||
>>> mydata.color = 'red'
|
||||
|
||||
So, the separate thread:
|
||||
|
||||
>>> thread = threading.Thread(target=f)
|
||||
>>> thread.start()
|
||||
>>> thread.join()
|
||||
|
||||
affects what we see:
|
||||
|
||||
>>> mydata.number
|
||||
11
|
||||
|
||||
>>> del mydata
|
||||
"""
|
||||
|
||||
# Threading import is at end
|
||||
|
||||
class _localbase(object):
|
||||
__slots__ = '_local__key', '_local__args', '_local__lock'
|
||||
|
||||
def __new__(cls, *args, **kw):
|
||||
self = object.__new__(cls)
|
||||
key = '_local__key', 'thread.local.' + str(id(self))
|
||||
object.__setattr__(self, '_local__key', key)
|
||||
object.__setattr__(self, '_local__args', (args, kw))
|
||||
object.__setattr__(self, '_local__lock', RLock())
|
||||
|
||||
if args or kw and (cls.__init__ is object.__init__):
|
||||
raise TypeError("Initialization arguments are not supported")
|
||||
|
||||
# We need to create the thread dict in anticipation of
|
||||
# __init__ being called, to make sure we don't call it
|
||||
# again ourselves.
|
||||
dict = object.__getattribute__(self, '__dict__')
|
||||
currentThread().__dict__[key] = dict
|
||||
|
||||
return self
|
||||
|
||||
def _patch(self):
|
||||
key = object.__getattribute__(self, '_local__key')
|
||||
d = currentThread().__dict__.get(key)
|
||||
if d is None:
|
||||
d = {}
|
||||
currentThread().__dict__[key] = d
|
||||
object.__setattr__(self, '__dict__', d)
|
||||
|
||||
# we have a new instance dict, so call out __init__ if we have
|
||||
# one
|
||||
cls = type(self)
|
||||
if cls.__init__ is not object.__init__:
|
||||
args, kw = object.__getattribute__(self, '_local__args')
|
||||
cls.__init__(self, *args, **kw)
|
||||
else:
|
||||
object.__setattr__(self, '__dict__', d)
|
||||
|
||||
class local(_localbase):
|
||||
|
||||
def __getattribute__(self, name):
|
||||
lock = object.__getattribute__(self, '_local__lock')
|
||||
lock.acquire()
|
||||
try:
|
||||
_patch(self)
|
||||
return object.__getattribute__(self, name)
|
||||
finally:
|
||||
lock.release()
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
lock = object.__getattribute__(self, '_local__lock')
|
||||
lock.acquire()
|
||||
try:
|
||||
_patch(self)
|
||||
return object.__setattr__(self, name, value)
|
||||
finally:
|
||||
lock.release()
|
||||
|
||||
def __delattr__(self, name):
|
||||
lock = object.__getattribute__(self, '_local__lock')
|
||||
lock.acquire()
|
||||
try:
|
||||
_patch(self)
|
||||
return object.__delattr__(self, name)
|
||||
finally:
|
||||
lock.release()
|
||||
|
||||
|
||||
def __del__():
|
||||
threading_enumerate = enumerate
|
||||
__getattribute__ = object.__getattribute__
|
||||
|
||||
def __del__(self):
|
||||
key = __getattribute__(self, '_local__key')
|
||||
|
||||
try:
|
||||
threads = list(threading_enumerate())
|
||||
except:
|
||||
# if enumerate fails, as it seems to do during
|
||||
# shutdown, we'll skip cleanup under the assumption
|
||||
# that there is nothing to clean up
|
||||
return
|
||||
|
||||
for thread in threads:
|
||||
try:
|
||||
__dict__ = thread.__dict__
|
||||
except AttributeError:
|
||||
# Thread is dying, rest in peace
|
||||
continue
|
||||
|
||||
if key in __dict__:
|
||||
try:
|
||||
del __dict__[key]
|
||||
except KeyError:
|
||||
pass # didn't have anything in this thread
|
||||
|
||||
return __del__
|
||||
__del__ = __del__()
|
||||
|
||||
from threading import currentThread, enumerate, RLock
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||
|
|
@ -793,26 +793,33 @@ def trans_generate(lang, modules, cr):
|
|||
cr.execute(query_models, query_param)
|
||||
|
||||
def push_constraint_msg(module, term_type, model, msg):
|
||||
# Check presence of __call__ directly instead of using
|
||||
# callable() because it will be deprecated as of Python 3.0
|
||||
if not hasattr(msg, '__call__'):
|
||||
push_translation(module, term_type, model, 0, encode(msg))
|
||||
push_translation(encode(module), term_type, encode(model), 0, encode(msg))
|
||||
|
||||
def push_local_constraints(module, model, cons_type='sql_constraints'):
|
||||
"""Climb up the class hierarchy and ignore inherited constraints
|
||||
from other modules"""
|
||||
term_type = 'sql_constraint' if cons_type == 'sql_constraints' else 'constraint'
|
||||
msg_pos = 2 if cons_type == 'sql_constraints' else 1
|
||||
for cls in model.__class__.__mro__:
|
||||
if getattr(cls, '_module', None) != module:
|
||||
continue
|
||||
constraints = getattr(cls, '_local_' + cons_type, [])
|
||||
for constraint in constraints:
|
||||
push_constraint_msg(module, term_type, model._name, constraint[msg_pos])
|
||||
|
||||
for (_, model, module) in cr.fetchall():
|
||||
module = encode(module)
|
||||
model = encode(model)
|
||||
|
||||
model_obj = pool.get(model)
|
||||
|
||||
if not model_obj:
|
||||
_logger.error("Unable to find object %r", model)
|
||||
continue
|
||||
|
||||
for constraint in getattr(model_obj, '_constraints', []):
|
||||
push_constraint_msg(module, 'constraint', model, constraint[1])
|
||||
if model_obj._constraints:
|
||||
push_local_constraints(module, model_obj, 'constraints')
|
||||
|
||||
for constraint in getattr(model_obj, '_sql_constraints', []):
|
||||
push_constraint_msg(module, 'sql_constraint', model, constraint[2])
|
||||
if model_obj._sql_constraints:
|
||||
push_local_constraints(module, model_obj, 'sql_constraints')
|
||||
|
||||
def get_module_from_path(path, mod_paths=None):
|
||||
if not mod_paths:
|
||||
|
|
|
@ -1,191 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Management Solution
|
||||
# Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
import copy
|
||||
import logging
|
||||
|
||||
import openerp.netsvc as netsvc
|
||||
from openerp.tools.misc import UpdateableStr, UpdateableDict
|
||||
from openerp.tools.translate import translate
|
||||
from lxml import etree
|
||||
|
||||
import openerp.pooler as pooler
|
||||
|
||||
from openerp.osv.osv import except_osv
|
||||
from openerp.osv.orm import except_orm
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
class except_wizard(Exception):
|
||||
def __init__(self, name, value):
|
||||
self.name = name
|
||||
self.value = value
|
||||
self.args = (name, value)
|
||||
|
||||
class interface(netsvc.Service):
|
||||
"""
|
||||
This is the base class used to implement Wizards. This class is deprecated
|
||||
and `openerp.osv.TransientModel` must be used instead.
|
||||
"""
|
||||
states = {}
|
||||
|
||||
def __init__(self, name):
|
||||
assert not self.exists('wizard.'+name), 'The wizard "%s" already exists!' % (name,)
|
||||
_logger.warning(
|
||||
"The wizard %s uses the deprecated openerp.wizard.interface class.\n"
|
||||
"It must use the openerp.osv.TransientModel class instead." % \
|
||||
name)
|
||||
super(interface, self).__init__('wizard.'+name)
|
||||
self.wiz_name = name
|
||||
|
||||
def translate_view(self, cr, node, state, lang):
|
||||
if node.get('string'):
|
||||
trans = translate(cr, self.wiz_name+','+state, 'wizard_view', lang, node.get('string').encode('utf8'))
|
||||
if trans:
|
||||
node.set('string', trans)
|
||||
for n in node:
|
||||
self.translate_view(cr, n, state, lang)
|
||||
|
||||
def execute_cr(self, cr, uid, data, state='init', context=None):
|
||||
if not context:
|
||||
context={}
|
||||
res = {}
|
||||
try:
|
||||
state_def = self.states[state]
|
||||
|
||||
result_def = state_def.get('result', {})
|
||||
|
||||
actions_res = {}
|
||||
# iterate through the list of actions defined for this state
|
||||
for action in state_def.get('actions', []):
|
||||
# execute them
|
||||
action_res = action(self, cr, uid, data, context)
|
||||
assert isinstance(action_res, dict), 'The return value of wizard actions should be a dictionary'
|
||||
actions_res.update(action_res)
|
||||
|
||||
res = copy.copy(result_def)
|
||||
res['datas'] = actions_res
|
||||
|
||||
lang = context.get('lang', False)
|
||||
if result_def['type'] == 'action':
|
||||
res['action'] = result_def['action'](self, cr, uid, data, context)
|
||||
elif result_def['type'] == 'form':
|
||||
fields = copy.deepcopy(result_def['fields'])
|
||||
arch = copy.copy(result_def['arch'])
|
||||
button_list = copy.copy(result_def['state'])
|
||||
|
||||
if isinstance(fields, UpdateableDict):
|
||||
fields = fields.dict
|
||||
if isinstance(arch, UpdateableStr):
|
||||
arch = arch.string
|
||||
|
||||
# fetch user-set defaut values for the field... shouldn't we pass it the uid?
|
||||
ir_values_obj = pooler.get_pool(cr.dbname).get('ir.values')
|
||||
defaults = ir_values_obj.get(cr, uid, 'default', False, [('wizard.'+self.wiz_name, False)])
|
||||
default_values = dict([(x[1], x[2]) for x in defaults])
|
||||
for val in fields.keys():
|
||||
if 'default' in fields[val]:
|
||||
# execute default method for this field
|
||||
if callable(fields[val]['default']):
|
||||
fields[val]['value'] = fields[val]['default'](uid, data, state)
|
||||
else:
|
||||
fields[val]['value'] = fields[val]['default']
|
||||
del fields[val]['default']
|
||||
else:
|
||||
# if user has set a default value for the field, use it
|
||||
if val in default_values:
|
||||
fields[val]['value'] = default_values[val]
|
||||
if 'selection' in fields[val]:
|
||||
if not isinstance(fields[val]['selection'], (tuple, list)):
|
||||
fields[val] = copy.copy(fields[val])
|
||||
fields[val]['selection'] = fields[val]['selection'](self, cr, uid, context)
|
||||
elif lang:
|
||||
res_name = "%s,%s,%s" % (self.wiz_name, state, val)
|
||||
trans = lambda x: translate(cr, res_name, 'selection', lang, x) or x
|
||||
for idx, (key, val2) in enumerate(fields[val]['selection']):
|
||||
fields[val]['selection'][idx] = (key, trans(val2))
|
||||
|
||||
if lang:
|
||||
# translate fields
|
||||
for field in fields:
|
||||
res_name = "%s,%s,%s" % (self.wiz_name, state, field)
|
||||
|
||||
trans = translate(cr, res_name, 'wizard_field', lang)
|
||||
if trans:
|
||||
fields[field]['string'] = trans
|
||||
|
||||
if 'help' in fields[field]:
|
||||
t = translate(cr, res_name, 'help', lang, fields[field]['help'])
|
||||
if t:
|
||||
fields[field]['help'] = t
|
||||
|
||||
# translate arch
|
||||
if not isinstance(arch, UpdateableStr):
|
||||
doc = etree.XML(arch)
|
||||
self.translate_view(cr, doc, state, lang)
|
||||
arch = etree.tostring(doc)
|
||||
|
||||
# translate buttons
|
||||
button_list = list(button_list)
|
||||
for i, aa in enumerate(button_list):
|
||||
button_name = aa[0]
|
||||
trans = translate(cr, self.wiz_name+','+state+','+button_name, 'wizard_button', lang)
|
||||
if trans:
|
||||
aa = list(aa)
|
||||
aa[1] = trans
|
||||
button_list[i] = aa
|
||||
|
||||
res['fields'] = fields
|
||||
res['arch'] = arch
|
||||
res['state'] = button_list
|
||||
|
||||
elif result_def['type'] == 'choice':
|
||||
next_state = result_def['next_state'](self, cr, uid, data, context)
|
||||
return self.execute_cr(cr, uid, data, next_state, context)
|
||||
|
||||
except Exception, e:
|
||||
if isinstance(e, except_wizard) \
|
||||
or isinstance(e, except_osv) \
|
||||
or isinstance(e, except_orm):
|
||||
netsvc.abort_response(2, e.name, 'warning', e.value)
|
||||
else:
|
||||
_logger.exception('Exception in call:')
|
||||
raise
|
||||
|
||||
return res
|
||||
|
||||
def execute(self, db, uid, data, state='init', context=None):
|
||||
if not context:
|
||||
context={}
|
||||
cr = pooler.get_db(db).cursor()
|
||||
try:
|
||||
try:
|
||||
res = self.execute_cr(cr, uid, data, state, context)
|
||||
cr.commit()
|
||||
except Exception:
|
||||
cr.rollback()
|
||||
raise
|
||||
finally:
|
||||
cr.close()
|
||||
return res
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||
|
36
setup.py
36
setup.py
|
@ -21,31 +21,29 @@
|
|||
##############################################################################
|
||||
|
||||
import glob, os, re, setuptools, sys
|
||||
from os.path import join, isfile
|
||||
from os.path import join
|
||||
|
||||
# List all data files
|
||||
def data():
|
||||
files = []
|
||||
r = {}
|
||||
for root, dirnames, filenames in os.walk('openerp'):
|
||||
for filename in filenames:
|
||||
if not re.match(r'.*(\.pyc|\.pyo|\~)$',filename):
|
||||
files.append(os.path.join(root, filename))
|
||||
d = {}
|
||||
for v in files:
|
||||
k=os.path.dirname(v)
|
||||
if k in d:
|
||||
d[k].append(v)
|
||||
else:
|
||||
d[k]=[v]
|
||||
r = d.items()
|
||||
if not re.match(r'.*(\.pyc|\.pyo|\~)$', filename):
|
||||
r.setdefault(root, []).append(os.path.join(root, filename))
|
||||
|
||||
if os.name == 'nt':
|
||||
r.append(("Microsoft.VC90.CRT", glob.glob('C:\Microsoft.VC90.CRT\*.*')))
|
||||
r["Microsoft.VC90.CRT"] = glob.glob('C:\Microsoft.VC90.CRT\*.*')
|
||||
|
||||
import babel
|
||||
r.append(("localedata",
|
||||
glob.glob(os.path.join(os.path.dirname(babel.__file__), "localedata" , '*'))))
|
||||
import babel
|
||||
r["localedata"] = glob.glob(os.path.join(os.path.dirname(babel.__file__), "localedata", '*'))
|
||||
|
||||
return r
|
||||
import pytz
|
||||
tzdir = os.path.dirname(pytz.__file__)
|
||||
for root, _, filenames in os.walk(os.path.join(tzdir, "zoneinfo")):
|
||||
base = os.path.join('pytz', root[len(tzdir) + 1:])
|
||||
r[base] = [os.path.join(root, f) for f in filenames]
|
||||
|
||||
return r.items()
|
||||
|
||||
def gen_manifest():
|
||||
file_list="\n".join(data())
|
||||
|
@ -64,7 +62,7 @@ def py2exe_options():
|
|||
"skip_archive": 1,
|
||||
"optimize": 2,
|
||||
"dist_dir": 'dist',
|
||||
"packages": [ "DAV", "HTMLParser", "PIL", "asynchat", "asyncore", "commands", "dateutil", "decimal", "docutils", "email", "encodings", "imaplib", "lxml", "lxml._elementpath", "lxml.builder", "lxml.etree", "lxml.objectify", "mako", "openerp", "poplib", "pychart", "pydot", "pyparsing", "reportlab", "select", "simplejson", "smtplib", "uuid", "vatnumber", "vobject", "xml", "xml.dom", "yaml", ],
|
||||
"packages": [ "DAV", "HTMLParser", "PIL", "asynchat", "asyncore", "commands", "dateutil", "decimal", "docutils", "email", "encodings", "imaplib", "lxml", "lxml._elementpath", "lxml.builder", "lxml.etree", "lxml.objectify", "mako", "openerp", "poplib", "pychart", "pydot", "pyparsing", "pytz", "reportlab", "select", "simplejson", "smtplib", "uuid", "vatnumber", "vobject", "xml", "xml.dom", "yaml", ],
|
||||
"excludes" : ["Tkconstants","Tkinter","tcl"],
|
||||
}
|
||||
}
|
||||
|
@ -110,6 +108,7 @@ setuptools.setup(
|
|||
'gdata',
|
||||
'lxml < 3', # windows binary http://www.lfd.uci.edu/~gohlke/pythonlibs/
|
||||
'mako',
|
||||
'mock',
|
||||
'PIL', # windows binary http://www.lfd.uci.edu/~gohlke/pythonlibs/
|
||||
'psutil', # windows binary code.google.com/p/psutil/downloads/list
|
||||
'psycopg2',
|
||||
|
@ -122,6 +121,7 @@ setuptools.setup(
|
|||
'pyyaml',
|
||||
'reportlab', # windows binary pypi.python.org/pypi/reportlab
|
||||
'simplejson',
|
||||
'unittest2',
|
||||
'vatnumber',
|
||||
'vobject',
|
||||
'werkzeug',
|
||||
|
|
Loading…
Reference in New Issue