From f9c2437959fba731b808208e184d86f51f2db600 Mon Sep 17 00:00:00 2001 From: "Amit (OpenERP)" Date: Fri, 18 May 2012 16:05:41 +0530 Subject: [PATCH 01/29] [FIX] account : Add access rules for entry analysis report lp bug: https://launchpad.net/bugs/999506 fixed bzr revid: amp@tinyerp.com-20120518103541-vfp9bh4p9dqwbr18 --- addons/account/security/account_security.xml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/addons/account/security/account_security.xml b/addons/account/security/account_security.xml index e1a29a6edfb..997918650a5 100644 --- a/addons/account/security/account_security.xml +++ b/addons/account/security/account_security.xml @@ -107,6 +107,13 @@ ['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])] + + Entries Analysis multi-company + + + ['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])] + + Account fiscal Mapping company rule From 0195eb407d8f71b7adc878f9bc39d14de8bde38b Mon Sep 17 00:00:00 2001 From: "Amit (OpenERP)" Date: Wed, 22 Aug 2012 11:55:55 +0530 Subject: [PATCH 02/29] [FIX] mail_template : Fixes the duplication problem on email template lp bug: https://launchpad.net/bugs/1039645 fixed bzr revid: amp@tinyerp.com-20120822062555-d7thh474wn5lisnv --- addons/email_template/email_template.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/addons/email_template/email_template.py b/addons/email_template/email_template.py index b43b7d19b85..6f48d37c714 100644 --- a/addons/email_template/email_template.py +++ b/addons/email_template/email_template.py @@ -236,6 +236,10 @@ class email_template(osv.osv): default = {} default = default.copy() default['name'] = template.name + _('(copy)') + default.update({ + 'ref_ir_act_window': False, + 'ref_ir_value': False, + }) return super(email_template, self).copy(cr, uid, id, default, context) def build_expression(self, field_name, sub_field_name, null_value): From 4679680dba42a4071c8fbbc43a33e2e61bdb812e Mon Sep 17 00:00:00 2001 From: Antony Lesuisse Date: Sat, 22 Sep 2012 12:51:07 +0200 Subject: [PATCH 03/29] manual merge of multicorn bzr revid: al@openerp.com-20120922105107-q02ogtsfudphzkl6 --- gunicorn.conf.py | 63 --- openerp-server | 26 +- openerp-wsgi.py | 53 +++ openerp/__init__.py | 12 +- openerp/addons/base/ir/ir_cron.py | 99 +++++ openerp/db/__init__.py | 30 -- openerp/service/__init__.py | 45 ++- openerp/service/netrpc_server.py | 4 +- .../netrpc_socket.py} | 5 +- openerp/service/workers.py | 368 ++++++++++++++++++ .../{wsgi/core.py => service/wsgi_server.py} | 131 +------ openerp/tools/config.py | 47 +-- openerp/wsgi/__init__.py | 33 -- openerp/wsgi/proxied.py | 34 -- 14 files changed, 618 insertions(+), 332 deletions(-) delete mode 100644 gunicorn.conf.py create mode 100644 openerp-wsgi.py delete mode 100644 openerp/db/__init__.py rename openerp/{tiny_socket.py => service/netrpc_socket.py} (98%) create mode 100644 openerp/service/workers.py rename openerp/{wsgi/core.py => service/wsgi_server.py} (79%) delete mode 100644 openerp/wsgi/__init__.py delete mode 100644 openerp/wsgi/proxied.py diff --git a/gunicorn.conf.py b/gunicorn.conf.py deleted file mode 100644 index 0f1d93a8638..00000000000 --- a/gunicorn.conf.py +++ /dev/null @@ -1,63 +0,0 @@ -# Gunicorn sample configuration file. -# See http://gunicorn.org/configure.html for more details. -# -# To run the OpenERP server via Gunicorn, change the appropriate -# settings below, in order to provide the parameters that -# would normally be passed in the command-line, -# (at least `bind` and `conf['addons_path']`), then execute: -# $ gunicorn openerp:wsgi.core.application -c gunicorn.conf.py -# or if you want to run it behind a reverse proxy, add the line -# import openerp.wsgi.proxied -# in this file and execute: -# $ gunicorn openerp:wsgi.proxied.application -c gunicorn.conf.py - -import openerp - -# Standard OpenERP XML-RPC port is 8069 -bind = '127.0.0.1:8069' - -pidfile = '.gunicorn.pid' - -# Gunicorn recommends 2-4 x number_of_cpu_cores, but -# you'll want to vary this a bit to find the best for your -# particular work load. -workers = 4 - -# Some application-wide initialization is needed. -on_starting = openerp.wsgi.core.on_starting -when_ready = openerp.wsgi.core.when_ready -pre_request = openerp.wsgi.core.pre_request -post_request = openerp.wsgi.core.post_request - -# openerp request-response cycle can be quite long for -# big reports for example -timeout = 240 - -max_requests = 2000 - -# Equivalent of --load command-line option -openerp.conf.server_wide_modules = ['web'] - -# internal TODO: use openerp.conf.xxx when available -conf = openerp.tools.config - -# Path to the OpenERP Addons repository (comma-separated for -# multiple locations) -conf['addons_path'] = '/home/openerp/addons/trunk,/home/openerp/web/trunk/addons' - -# Optional database config if not using local socket -#conf['db_name'] = 'mycompany' -#conf['db_host'] = 'localhost' -#conf['db_user'] = 'foo' -#conf['db_port'] = 5432 -#conf['db_password'] = 'secret' - -# OpenERP Log Level -# DEBUG=10, DEBUG_RPC=8, DEBUG_RPC_ANSWER=6, DEBUG_SQL=5, INFO=20, -# WARNING=30, ERROR=40, CRITICAL=50 -# conf['log_level'] = 20 - -# If --static-http-enable is used, path for the static web directory -#conf['static_http_document_root'] = '/var/www' - -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp-server b/openerp-server index 5d717c834de..5222de88eb4 100755 --- a/openerp-server +++ b/openerp-server @@ -110,7 +110,6 @@ def run_test_file(dbname, test_file): except Exception: _logger.exception('Failed to initialize database `%s` and run test file `%s`.', dbname, test_file) - def export_translation(): config = openerp.tools.config dbname = config['db_name'] @@ -205,6 +204,7 @@ def quit_on_signals(): except KeyboardInterrupt: pass + config = openerp.tools.config if config['pidfile']: os.unlink(config['pidfile']) @@ -217,8 +217,7 @@ def configure_babel_localedata_path(): import babel babel.localedata._dirname = os.path.join(os.path.dirname(sys.executable), 'localedata') -if __name__ == "__main__": - +def main(): os.environ["TZ"] = "UTC" check_root_user() @@ -247,20 +246,13 @@ if __name__ == "__main__": sys.exit(0) if not config["stop_after_init"]: + setup_pid_file() # Some module register themselves when they are loaded so we need the # services to be running before loading any registry. - openerp.service.start_services() - - for m in openerp.conf.server_wide_modules: - try: - openerp.modules.module.load_openerp_module(m) - except Exception: - msg = '' - if m == 'web': - msg = """ -The `web` module is provided by the addons found in the `openerp-web` project. -Maybe you forgot to add those addons in your addons_path configuration.""" - _logger.exception('Failed to load server-wide module `%s`.%s', m, msg) + if config['workers']: + openerp.service.start_services_workers() + else: + openerp.service.start_services() if config['db_name']: for dbname in config['db_name'].split(','): @@ -269,8 +261,10 @@ Maybe you forgot to add those addons in your addons_path configuration.""" if config["stop_after_init"]: sys.exit(0) - setup_pid_file() _logger.info('OpenERP server is running, waiting for connections...') quit_on_signals() +if __name__ == "__main__": + main() + # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp-wsgi.py b/openerp-wsgi.py new file mode 100644 index 00000000000..7382b90d3b7 --- /dev/null +++ b/openerp-wsgi.py @@ -0,0 +1,53 @@ +#!/usr/bin/python +# WSGI Handler sample configuration file. +# +# Change the appropriate settings below, in order to provide the parameters +# that would normally be passed in the command-line. +# (at least conf['addons_path']) +# +# For generic wsgi handlers a global application is defined. +# For uwsgi this should work: +# $ uwsgi --http :9090 --pythonpath . --wsgi-file openerp-wsgi.py +# +# For gunicorn additional globals need to be defined in the Gunicorn section. +# Then the following command should run: +# $ gunicorn openerp:wsgi.core.application -c gunicorn.conf.py + +import openerp + +#---------------------------------------------------------- +# Common +#---------------------------------------------------------- +# Equivalent of --load command-line option +openerp.conf.server_wide_modules = ['web'] +conf = openerp.tools.config + +# Path to the OpenERP Addons repository (comma-separated for +# multiple locations) +conf['addons_path'] = '/home/openerp/addons/trunk,/home/openerp/web/trunk/addons' +conf['addons_path'] = '/home/wis/stuff/version/openerp/source/addons/6.1,/home/wis/stuff/version/openerp/source/web/6.1/addons' + + +# Optional database config if not using local socket +#conf['db_name'] = 'mycompany' +#conf['db_host'] = 'localhost' +#conf['db_user'] = 'foo' +#conf['db_port'] = 5432 +#conf['db_password'] = 'secret' + +#---------------------------------------------------------- +# Generic WSGI handlers application +#---------------------------------------------------------- +application = openerp.service.wsgi_server.application + +#---------------------------------------------------------- +# Gunicorn +#---------------------------------------------------------- +# Standard OpenERP XML-RPC port is 8069 +bind = '127.0.0.1:8069' +pidfile = '.gunicorn.pid' +workers = 4 +timeout = 240 +max_requests = 2000 + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/__init__.py b/openerp/__init__.py index 5827b744cf8..2fd2a9812f9 100644 --- a/openerp/__init__.py +++ b/openerp/__init__.py @@ -38,11 +38,19 @@ import run_tests import service import sql_db import test -import tiny_socket import tools import wizard import workflow -import wsgi +# backward compatilbility +# TODO: This is for the web addons, can be removed later. +wsgi = service +wsgi.register_wsgi_handler = wsgi.wsgi_server.register_wsgi_handler +# Is the server running in multi-process mode (e.g. behind Gunicorn). +# If this is True, the processes have to communicate some events, +# e.g. database update or cache invalidation. Each process has also +# its own copy of the data structure and we don't need to care about +# locks between threads. +multi_process = True # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/addons/base/ir/ir_cron.py b/openerp/addons/base/ir/ir_cron.py index 3c437c93282..dbc89278ece 100644 --- a/openerp/addons/base/ir/ir_cron.py +++ b/openerp/addons/base/ir/ir_cron.py @@ -266,6 +266,105 @@ class ir_cron(osv.osv): cr.commit() cr.close() + def _process_job(self, cr, job): + """ Run a given job taking care of the repetition. + + The cursor has a lock on the job (aquired by _acquire_job()). + + :param job: job to be run (as a dictionary). + """ + try: + now = datetime.now() + 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'])) + + finally: + cr.commit() + cr.close() + + @classmethod + def _acquire_job(cls, db_name): + # TODO remove 'check' argument from addons/base_action_rule/base_action_rule.py + """ Try to process one cron job. + + This selects in database all the jobs that should be processed. It then + tries to lock each of them and, if it succeeds, run the cron job (if it + doesn't succeed, it means the job was already locked to be taken care + of by another thread) and return. + + If a job was processed, returns True, otherwise returns False. + """ + db = openerp.sql_db.db_connect(db_name) + cr = db.cursor() + try: + # 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(): + 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, run its code + _logger.debug('Starting job `%s`.', job['name']) + openerp.modules.registry.RegistryManager.check_registry_signaling(db_name) + registry = openerp.pooler.get_pool(db_name) + registry[cls._name]._process_job(task_cr, job) + openerp.modules.registry.RegistryManager.signal_caches_change(db_name) + return True + + except psycopg2.ProgrammingError, e: + if e.pgcode == '42P01': + # Class 42 — Syntax Error or Access Rule Violation; 42P01: undefined_table + # The table ir_cron does not exist; this is probably not an OpenERP database. + _logger.warning('Tried to poll an undefined table on database %s.', db_name) + else: + raise + except Exception, ex: + _logger.warning('Exception in cron:', exc_info=True) + + finally: + cr.commit() + cr.close() + + 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 diff --git a/openerp/db/__init__.py b/openerp/db/__init__.py deleted file mode 100644 index 5eb06af1ad1..00000000000 --- a/openerp/db/__init__.py +++ /dev/null @@ -1,30 +0,0 @@ -# -*- coding: utf-8 -*- -############################################################################## -# -# OpenERP, Open Source Management Solution -# Copyright (C) 2011 OpenERP s.a. (). -# -# 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 . -# -############################################################################## - -""" Lower-level database access. - -This module provides access to the underlying database without going -through the ORM. The goal is to gather sql_db.py and other various db -code. - -""" - -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/service/__init__.py b/openerp/service/__init__.py index 511d62c5862..30a765b1f60 100644 --- a/openerp/service/__init__.py +++ b/openerp/service/__init__.py @@ -33,7 +33,8 @@ import openerp.modules import openerp.netsvc import openerp.osv import openerp.tools -import openerp.wsgi +import openerp.service.wsgi_server +import openerp.service.workers #.apidoc title: RPC Services @@ -48,22 +49,40 @@ import openerp.wsgi _logger = logging.getLogger(__name__) -# TODO block until the server is really up, accepting connections -# TODO be idemptotent (as long as stop_service was not called). -def start_services(): - """ Start all services. +def load_server_wide_modules(): + for m in openerp.conf.server_wide_modules: + try: + openerp.modules.module.load_openerp_module(m) + except Exception: + msg = '' + if m == 'web': + msg = """ +The `web` module is provided by the addons found in the `openerp-web` project. +Maybe you forgot to add those addons in your addons_path configuration.""" + _logger.exception('Failed to load server-wide module `%s`.%s', m, msg) - Services include the different servers and cron threads. +start_internal_done = False - """ +def start_internal(): + global start_internal_done + if start_internal_done: + 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() + load_server_wide_modules() + start_internal_done = True + +def start_services(): + """ Start all services including http, netrpc and cron """ + openerp.multi_process = False # Nah! + + start_internal() # Initialize the HTTP stack. - #http_server.init_servers() - #http_server.init_static_http() netrpc_server.init_servers() # Start the main cron thread. @@ -73,8 +92,7 @@ def start_services(): openerp.netsvc.Server.startAll() # Start the WSGI server. - openerp.wsgi.core.start_server() - + openerp.service.wsgi_server.start_server() def stop_services(): """ Stop all services. """ @@ -82,7 +100,7 @@ def stop_services(): openerp.cron.cancel_all() openerp.netsvc.Server.quitAll() - openerp.wsgi.core.stop_server() + 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.") @@ -101,6 +119,9 @@ def stop_services(): openerp.modules.registry.RegistryManager.delete_all() +def start_services_workers(): + openerp.service.workers.Multicorn(openerp.service.wsgi_server.application).run() + # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/service/netrpc_server.py b/openerp/service/netrpc_server.py index fcbfaf0ba82..cc78d0620cf 100644 --- a/openerp/service/netrpc_server.py +++ b/openerp/service/netrpc_server.py @@ -32,8 +32,8 @@ import sys import threading import traceback import openerp +import openerp.service.netrpc_socket import openerp.netsvc as netsvc -import openerp.tiny_socket as tiny_socket import openerp.tools as tools _logger = logging.getLogger(__name__) @@ -52,7 +52,7 @@ class TinySocketClientThread(threading.Thread): def run(self): self.running = True try: - ts = tiny_socket.mysocket(self.sock) + ts = openerp.server.netrpc_socket.mysocket(self.sock) except Exception: self.threads.remove(self) self.running = False diff --git a/openerp/tiny_socket.py b/openerp/service/netrpc_socket.py similarity index 98% rename from openerp/tiny_socket.py rename to openerp/service/netrpc_socket.py index b5f5bbccee3..c49c8f7c2a2 100644 --- a/openerp/tiny_socket.py +++ b/openerp/service/netrpc_socket.py @@ -23,14 +23,11 @@ import socket import cPickle import cStringIO -import netsvc - -#.apidoc title: Net-RPC classes +import openerp.netsvc as netsvc # Pickle protocol version 2 is optimized compared to default (version 0) PICKLE_PROTOCOL = 2 - class Myexception(Exception): """ custom exception object store diff --git a/openerp/service/workers.py b/openerp/service/workers.py new file mode 100644 index 00000000000..824bc07bb53 --- /dev/null +++ b/openerp/service/workers.py @@ -0,0 +1,368 @@ +#----------------------------------------------------------- +# Multicorn, multiprocessing inspired by gunicorn +# TODO rename class: Multicorn -> Arbiter ? +#----------------------------------------------------------- +import errno +import fcntl +import psutil +import random +import resource +import select +import socket +import time +import logging +import os +import signal +import sys + +import werkzeug.serving + +import openerp +import openerp.tools.config as config + +_logger = logging.getLogger(__name__) + +class Multicorn(object): + """ Multiprocessing inspired by (g)unicorn. + Multicorn currently uses accept(2) as dispatching method between workers + but we plan to replace it by a more intelligent dispatcher to will parse + the first HTTP request line. + """ + def __init__(self, app): + # config + self.address = (config['xmlrpc_interface'] or '0.0.0.0', config['xmlrpc_port']) + self.population = config['workers'] + self.timeout = config['limit_time_real'] + self.limit_request = config['limit_request'] + # working vars + self.beat = 4 + self.app = app + self.pid = os.getpid() + self.socket = None + self.workers_http = {} + self.workers_cron = {} + self.workers = {} + self.generation = 0 + self.queue = [] + + def pipe_new(self): + pipe = os.pipe() + for fd in pipe: + # non_blocking + flags = fcntl.fcntl(fd, fcntl.F_GETFL) | os.O_NONBLOCK + fcntl.fcntl(fd, fcntl.F_SETFL, flags) + # close_on_exec + flags = fcntl.fcntl(fd, fcntl.F_GETFD) | fcntl.FD_CLOEXEC + fcntl.fcntl(fd, fcntl.F_SETFD, flags) + return pipe + + def pipe_ping(self, pipe): + try: + os.write(pipe[1], '.') + except IOError, e: + if e.errno not in [errno.EAGAIN, errno.EINTR]: + raise + + def signal_handler(self, sig, frame): + if len(self.queue) < 5 or sig == signal.SIGCHLD: + self.queue.append(sig) + self.pipe_ping(self.pipe) + else: + _logger.warn("Dropping signal: %s", sig) + + def worker_spawn(self, klass, workers_registry): + self.generation += 1 + worker = klass(self) + pid = os.fork() + if pid != 0: + worker.pid = pid + self.workers[pid] = worker + workers_registry[pid] = worker + return worker + else: + worker.run() + sys.exit(0) + + def worker_pop(self, pid): + if pid in self.workers: + _logger.debug("Worker (%s) unregistered",pid) + try: + self.workers_http.pop(pid,None) + self.workers_cron.pop(pid,None) + u = self.workers.pop(pid) + u.close() + except OSError: + return + + def worker_kill(self, pid, sig): + try: + os.kill(pid, sig) + except OSError, e: + if e.errno == errno.ESRCH: + self.worker_pop(pid) + + def process_signals(self): + while len(self.queue): + sig = self.queue.pop(0) + if sig in [signal.SIGINT,signal.SIGTERM]: + raise KeyboardInterrupt + + def process_zombie(self): + # reap dead workers + while 1: + try: + wpid, status = os.waitpid(-1, os.WNOHANG) + if not wpid: + break + if (status >> 8) == 3: + msg = "Critial worker error (%s)" + _logger.critical(msg, wpid) + raise Exception(msg % wpid) + self.worker_pop(wpid) + except OSError, e: + if e.errno == errno.ECHILD: + break + raise + + def process_timeout(self): + now = time.time() + for (pid, worker) in self.workers.items(): + if now - worker.watchdog_time >= worker.watchdog_timeout: + _logger.error("Worker (%s) timeout", pid) + self.worker_kill(pid, signal.SIGKILL) + + 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 ? + self.worker_spawn(WorkerCron, self.workers_cron) + + def sleep(self): + try: + # map of fd -> worker + fds = dict([(w.watchdog_pipe[0],w) for k,w in self.workers.items()]) + fd_in = fds.keys() + [self.pipe[0]] + # check for ping or internal wakeups + ready = select.select(fd_in, [], [], self.beat) + # update worker watchdogs + for fd in ready[0]: + if fd in fds: + fds[fd].watchdog_time = time.time() + try: + # empty pipe + while os.read(fd, 1): + pass + except OSError, e: + if e.errno not in [errno.EAGAIN]: + raise + except select.error, e: + if e[0] not in [errno.EINTR]: + raise + + def start(self): + # wakeup pipe, python doesnt throw EINTR when a syscall is interrupted + # by a signal simulating a pseudo SA_RESTART. We write to a pipe in the + # signal handler to overcome this behaviour + self.pipe = self.pipe_new() + # set signal + signal.signal(signal.SIGINT, self.signal_handler) + signal.signal(signal.SIGTERM, self.signal_handler) + signal.signal(signal.SIGCHLD, self.signal_handler) + # listen to socket + self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self.socket.setblocking(0) + self.socket.bind(self.address) + self.socket.listen(8) + + def stop(self, graceful=True): + if graceful: + _logger.info("Stopping gracefully") + limit = time.time() + self.timeout + for pid in self.workers.keys(): + self.worker_kill(pid, signal.SIGTERM) + while self.workers and time.time() < limit: + self.process_zombie() + time.sleep(0.1) + else: + _logger.info("Stopping forcefully") + for pid in self.workers.keys(): + self.worker_kill(pid, signal.SIGTERM) + self.socket.close() + import __main__ + __main__.quit_signals_received = 1 + + def run(self): + self.start() + _logger.debug("Multiprocess starting") + while 1: + try: + #_logger.debug("Multiprocess beat (%s)",time.time()) + self.process_signals() + self.process_zombie() + self.process_timeout() + self.process_spawn() + self.sleep() + except KeyboardInterrupt: + _logger.debug("Multiprocess clean stop") + self.stop() + break + except Exception,e: + _logger.exception(e) + self.stop(False) + sys.exit(-1) + +class Worker(object): + """ Workers """ + def __init__(self, multi): + self.multi = multi + self.watchdog_time = time.time() + self.watchdog_pipe = multi.pipe_new() + self.watchdog_timeout = multi.timeout + self.ppid = os.getpid() + self.pid = None + self.alive = True + # should we rename into lifetime ? + self.request_max = multi.limit_request + self.request_count = 0 + + def close(self): + os.close(self.watchdog_pipe[0]) + os.close(self.watchdog_pipe[1]) + + def signal_handler(self, sig, frame): + self.alive = False + + def sleep(self): + try: + ret = select.select([self.multi.socket], [], [], self.multi.beat) + except select.error, e: + if e[0] not in [errno.EINTR]: + raise + + def process_limit(self): + # If our parent changed sucide + if self.ppid != os.getppid(): + _logger.info("Worker (%s) Parent changed", self.pid) + self.alive = False + # check for lifetime + if self.request_count >= self.request_max: + _logger.info("Worker (%d) max request (%s) reached.", self.pid, self.request_count) + self.alive = False + # 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.') + self.alive = False # Commit suicide after the request. + + # VMS and RLIMIT_AS are the same thing: virtual memory, a.k.a. address space + soft, hard = resource.getrlimit(resource.RLIMIT_AS) + resource.setrlimit(resource.RLIMIT_AS, (config['limit_memory_hard'], hard)) + + # SIGXCPU (exceeded CPU time) signal handler will raise an exception. + 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.') + raise Exception('CPU time limit exceeded.') + signal.signal(signal.SIGXCPU, time_expired) + soft, hard = resource.getrlimit(resource.RLIMIT_CPU) + resource.setrlimit(resource.RLIMIT_CPU, (cpu_time + config['limit_time_cpu'], hard)) + + def process_work(self): + pass + + def start(self): + self.pid = os.getpid() + _logger.info("Worker %s (%s) alive", self.__class__.__name__, self.pid) + # Reseed the random number generator + random.seed() + # Prevent fd inherientence close_on_exec + flags = fcntl.fcntl(self.multi.socket, fcntl.F_GETFD) | fcntl.FD_CLOEXEC + fcntl.fcntl(self.multi.socket, fcntl.F_SETFD, flags) + # reset blocking status + self.multi.socket.setblocking(0) + signal.signal(signal.SIGINT, self.signal_handler) + signal.signal(signal.SIGTERM, signal.SIG_DFL) + signal.signal(signal.SIGCHLD, signal.SIG_DFL) + + def stop(self): + pass + + def run(self): + try: + self.start() + while self.alive: + self.process_limit() + self.multi.pipe_ping(self.watchdog_pipe) + self.sleep() + self.process_work() + _logger.info("Worker (%s) exiting...",self.pid) + self.stop() + except Exception,e: + _logger.exception("Worker (%s) Exception occured, exiting..."%self.pid) + # should we use 3 to abort everything ? + sys.exit(1) + +class WorkerHTTP(Worker): + """ HTTP Request workers """ + def process_request(self, client, addr): + client.setblocking(1) + client.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + # Prevent fd inherientence close_on_exec + flags = fcntl.fcntl(client, fcntl.F_GETFD) | fcntl.FD_CLOEXEC + fcntl.fcntl(client, fcntl.F_SETFD, flags) + # do request using WorkerBaseWSGIServer monkey patched with socket + self.server.socket = client + self.server.process_request(client,addr) + self.request_count += 1 + + def process_work(self): + try: + client, addr = self.multi.socket.accept() + self.process_request(client, addr) + except socket.error, e: + if e[0] not in (errno.EAGAIN, errno.ECONNABORTED): + raise + + def start(self): + Worker.start(self) + self.server = WorkerBaseWSGIServer(self.multi.app) + +class WorkerBaseWSGIServer(werkzeug.serving.BaseWSGIServer): + """ werkzeug WSGI Server patched to allow using an external listen socket + """ + def __init__(self, app): + werkzeug.serving.BaseWSGIServer.__init__(self, "1", "1", app) + def server_bind(self): + # we dont bind beause we use the listen socket of Multicorn#socket + # instead we close the socket + if self.socket: + self.socket.close() + def server_activate(self): + # dont listen as we use Multicorn#socket + pass + +class WorkerCron(Worker): + """ Cron workers """ + def sleep(self): + time.sleep(60) + + def process_work(self): + 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) + if not acquired: + break + self.request_count += 1 + + def start(self): + Worker.start(self) + openerp.service.start_internal() + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/wsgi/core.py b/openerp/service/wsgi_server.py similarity index 79% rename from openerp/wsgi/core.py rename to openerp/service/wsgi_server.py index 0aff7172480..61556974a00 100644 --- a/openerp/wsgi/core.py +++ b/openerp/service/wsgi_server.py @@ -38,10 +38,13 @@ import sys import threading import traceback +import werkzeug.serving +import werkzeug.contrib.fixers + import openerp import openerp.modules import openerp.tools.config as config -from ..service import websrv_lib +import websrv_lib _logger = logging.getLogger(__name__) @@ -221,9 +224,6 @@ def wsgi_xmlrpc_legacy(environ, start_response): params, method = xmlrpclib.loads(data) return xmlrpc_return(start_response, path, method, params, True) -def wsgi_jsonrpc(environ, start_response): - pass - def wsgi_webdav(environ, start_response): pi = environ['PATH_INFO'] if environ['REQUEST_METHOD'] == 'OPTIONS' and pi in ['*','/']: @@ -382,28 +382,31 @@ def register_wsgi_handler(handler): """ module_handlers.append(handler) -def application(environ, start_response): +def application_unproxied(environ, start_response): """ WSGI entry point.""" + openerp.service.start_internal() # Try all handlers until one returns some result (i.e. not None). - wsgi_handlers = [ - wsgi_xmlrpc_1, - wsgi_xmlrpc, - wsgi_jsonrpc, - wsgi_xmlrpc_legacy, - wsgi_webdav - ] + module_handlers + wsgi_handlers = [wsgi_xmlrpc_1, wsgi_xmlrpc, wsgi_xmlrpc_legacy, wsgi_webdav] + wsgi_handlers += module_handlers for handler in wsgi_handlers: result = handler(environ, start_response) if result is None: continue return result + # We never returned from the loop. response = 'No handler found.\n' start_response('404 Not Found', [('Content-Type', 'text/plain'), ('Content-Length', str(len(response)))]) return [response] +def application(environ, start_response): + if 'HTTP_X_FORWARDED_HOST' in environ: + return werkzeug.contrib.fixers.ProxyFix(application_unproxied)(environ, start_response) + else: + return application_unproxied(environ, start_response) + # The WSGI server, started by start_server(), stopped by stop_server(). httpd = None @@ -421,25 +424,8 @@ def serve(): # TODO Change the xmlrpc_* options to http_* interface = config['xmlrpc_interface'] or '0.0.0.0' port = config['xmlrpc_port'] - try: - import werkzeug.serving - if config['proxy_mode']: - from werkzeug.contrib.fixers import ProxyFix - app = ProxyFix(application) - suffix = ' (in proxy mode)' - else: - app = application - suffix = '' - httpd = werkzeug.serving.make_server(interface, port, app, threaded=True) - _logger.info('HTTP service (werkzeug) running on %s:%s%s', interface, port, suffix) - except ImportError: - import wsgiref.simple_server - _logger.warning('Werkzeug module unavailable, falling back to wsgiref.') - if config['proxy_mode']: - _logger.warning('Werkzeug module unavailable, not using proxy mode.') - httpd = wsgiref.simple_server.make_server(interface, port, application) - _logger.info('HTTP service (wsgiref) running on %s:%s', interface, port) - + httpd = werkzeug.serving.make_server(interface, port, application, threaded=True) + _logger.info('HTTP service (werkzeug) running on %s:%s', interface, port) httpd.serve_forever() def start_server(): @@ -457,87 +443,4 @@ def stop_server(): if httpd: httpd.shutdown() -# Master process id, can be used for signaling. -arbiter_pid = None - -# Application setup before we can spawn any worker process. -# This is suitable for e.g. gunicorn's on_starting hook. -def on_starting(server): - global arbiter_pid - arbiter_pid = os.getpid() # TODO check if this is true even after replacing the executable - #openerp.tools.cache = kill_workers_cache - openerp.netsvc.init_logger() - openerp.osv.osv.start_object_proxy() - openerp.service.web_services.start_web_services() - openerp.modules.module.initialize_sys_path() - openerp.modules.loading.open_openerp_namespace() - for m in openerp.conf.server_wide_modules: - try: - openerp.modules.module.load_openerp_module(m) - except Exception: - msg = '' - if m == 'web': - msg = """ -The `web` module is provided by the addons found in the `openerp-web` project. -Maybe you forgot to add those addons in your addons_path configuration.""" - _logger.exception('Failed to load server-wide module `%s`.%s', m, msg) - -# Install our own signal handler on the master process. -def when_ready(server): - # Hijack gunicorn's SIGWINCH handling; we can choose another one. - signal.signal(signal.SIGWINCH, make_winch_handler(server)) - -# Install limits on virtual memory and CPU time consumption. -def pre_request(worker, req): - import os - import psutil - import resource - import signal - # VMS and RLIMIT_AS are the same thing: virtual memory, a.k.a. address space - rss, vms = psutil.Process(os.getpid()).get_memory_info() - soft, hard = resource.getrlimit(resource.RLIMIT_AS) - resource.setrlimit(resource.RLIMIT_AS, (config['virtual_memory_limit'], hard)) - - r = resource.getrusage(resource.RUSAGE_SELF) - cpu_time = r.ru_utime + r.ru_stime - signal.signal(signal.SIGXCPU, time_expired) - soft, hard = resource.getrlimit(resource.RLIMIT_CPU) - resource.setrlimit(resource.RLIMIT_CPU, (cpu_time + config['cpu_time_limit'], hard)) - -# Reset the worker if it consumes too much memory (e.g. caused by a memory leak). -def post_request(worker, req, environ): - import os - import psutil - rss, vms = psutil.Process(os.getpid()).get_memory_info() - if vms > config['virtual_memory_reset']: - _logger.info('Virtual memory consumption ' - 'too high, rebooting the worker.') - worker.alive = False # Commit suicide after the request. - -# Our signal handler will signal a SGIQUIT to all workers. -def make_winch_handler(server): - def handle_winch(sig, fram): - server.kill_workers(signal.SIGQUIT) # This is gunicorn specific. - return handle_winch - -# SIGXCPU (exceeded CPU time) signal handler will raise an exception. -def time_expired(n, stack): - _logger.info('CPU time limit exceeded.') - raise Exception('CPU time limit exceeded.') # TODO one of openerp.exception - -# Kill gracefuly the workers (e.g. because we want to clear their cache). -# This is done by signaling a SIGWINCH to the master process, so it can be -# called by the workers themselves. -def kill_workers(): - try: - os.kill(arbiter_pid, signal.SIGWINCH) - except OSError, e: - if e.errno == errno.ESRCH: # no such pid - return - raise - -class kill_workers_cache(openerp.tools.ormcache): - def clear(self, dbname, *args, **kwargs): - kill_workers() - # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/tools/config.py b/openerp/tools/config.py index ce157c5e697..58d1d3c9c82 100644 --- a/openerp/tools/config.py +++ b/openerp/tools/config.py @@ -116,8 +116,6 @@ class configmanager(object): help="specify the TCP port for the XML-RPC protocol", type="int") group.add_option("--no-xmlrpc", dest="xmlrpc", action="store_false", my_default=True, help="disable the XML-RPC protocol") - group.add_option("--proxy-mode", dest="proxy_mode", action="store_true", my_default=False, - help="Enable correct behavior when behind a reverse proxy") parser.add_option_group(group) # XML-RPC / HTTPS @@ -253,8 +251,6 @@ class configmanager(object): # Advanced options group = optparse.OptionGroup(parser, "Advanced options") - group.add_option("--cache-timeout", dest="cache_timeout", my_default=100000, - help="set the timeout for the cache system", type="int") group.add_option('--debug', dest='debug_mode', action='store_true', my_default=False, help='enable debug mode') group.add_option("--stop-after-init", action="store_true", dest="stop_after_init", my_default=False, help="stop the server after its initialization") @@ -272,23 +268,30 @@ class configmanager(object): group.add_option("--max-cron-threads", dest="max_cron_threads", my_default=4, help="Maximum number of threads processing concurrently cron jobs.", type="int") - # TODO sensible default for the three following limits. - group.add_option("--virtual-memory-limit", dest="virtual_memory_limit", my_default=768 * 1024 * 1024, - help="Maximum allowed virtual memory per Gunicorn process. " - "When the limit is reached, any memory allocation will fail.", - type="int") - group.add_option("--virtual-memory-reset", dest="virtual_memory_reset", my_default=640 * 1024 * 1024, - help="Maximum allowed virtual memory per Gunicorn process. " - "When the limit is reached, the worker will be reset after " - "the current request.", - type="int") - group.add_option("--cpu-time-limit", dest="cpu_time_limit", my_default=60, - help="Maximum allowed CPU time per Gunicorn process. " - "When the limit is reached, an exception is raised.", - 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.") + parser.add_option_group(group) + group = optparse.OptionGroup(parser, "Multiprocessing options") + # TODO sensible default for the three following limits. + group.add_option("--workers", dest="workers", my_default=0, + 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.", + 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.", + type="int") + group.add_option("--limit-time-cpu", dest="limit_time_cpu", my_default=60, + help="Maximum allowed CPU time per request.", + type="int") + group.add_option("--limit-time-real", dest="limit_time_real", my_default=60, + help="Maximum allowed Real time per request. ", + type="int") + group.add_option("--limit-request", dest="limit_request", my_default=8192, + help="Maximum number of request to be processed per worker.", + type="int") parser.add_option_group(group) # Copy all optparse options (i.e. MyOption) into self.options. @@ -369,7 +372,7 @@ class configmanager(object): # if defined dont take the configfile value even if the defined value is None keys = ['xmlrpc_interface', 'xmlrpc_port', 'db_name', 'db_user', 'db_password', 'db_host', - 'db_port', 'db_template', 'logfile', 'pidfile', 'smtp_port', 'cache_timeout', + 'db_port', 'db_template', 'logfile', 'pidfile', 'smtp_port', 'email_from', 'smtp_server', 'smtp_user', 'smtp_password', 'netrpc_interface', 'netrpc_port', 'db_maxconn', 'import_partial', 'addons_path', 'netrpc', 'xmlrpc', 'syslog', 'without_demo', 'timezone', @@ -391,10 +394,10 @@ class configmanager(object): 'language', 'translate_out', 'translate_in', 'overwrite_existing_translations', 'debug_mode', 'smtp_ssl', 'load_language', 'stop_after_init', 'logrotate', 'without_demo', 'netrpc', 'xmlrpc', 'syslog', - 'list_db', 'xmlrpcs', 'proxy_mode', + 'list_db', 'xmlrpcs', 'test_file', 'test_enable', 'test_commit', 'test_report_directory', - 'osv_memory_count_limit', 'osv_memory_age_limit', 'max_cron_threads', - 'virtual_memory_limit', 'virtual_memory_reset', 'cpu_time_limit', 'unaccent', + 'osv_memory_count_limit', 'osv_memory_age_limit', 'max_cron_threads', 'unaccent', + 'workers', 'limit_memory_hard', 'limit_memory_soft', 'limit_time_cpu', 'limit_time_real', 'limit_request' ] for arg in keys: diff --git a/openerp/wsgi/__init__.py b/openerp/wsgi/__init__.py deleted file mode 100644 index a536ee57327..00000000000 --- a/openerp/wsgi/__init__.py +++ /dev/null @@ -1,33 +0,0 @@ -# -*- coding: utf-8 -*- -############################################################################## -# -# OpenERP, Open Source Management Solution -# Copyright (C) 2012-2012 OpenERP s.a. (). -# -# 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 . -# -############################################################################## - -""" WSGI stack - -This module offers a WSGI interface to/from OpenERP. - -""" - -from . import core - -# TODO: This is for the web addons, can be removed later. -register_wsgi_handler = core.register_wsgi_handler - -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/wsgi/proxied.py b/openerp/wsgi/proxied.py deleted file mode 100644 index 658f6f29c55..00000000000 --- a/openerp/wsgi/proxied.py +++ /dev/null @@ -1,34 +0,0 @@ -# -*- coding: utf-8 -*- -############################################################################## -# -# OpenERP, Open Source Management Solution -# Copyright (C) 2012 OpenERP s.a. (). -# -# 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 . -# -############################################################################## - -""" - -WSGI entry point with Proxy mode (from Werkzeug). - -""" - -from werkzeug.contrib.fixers import ProxyFix - -from . import core - -application = ProxyFix(core.application) - -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: From 7833ed2137053faf8c3d5946fb77c2eed447c64b Mon Sep 17 00:00:00 2001 From: Antony Lesuisse Date: Sat, 22 Sep 2012 12:55:15 +0200 Subject: [PATCH 04/29] typo bzr revid: al@openerp.com-20120922105515-hk2fcc4ux5rb1xz1 --- openerp-wsgi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openerp-wsgi.py b/openerp-wsgi.py index 7382b90d3b7..5ab7afc406e 100644 --- a/openerp-wsgi.py +++ b/openerp-wsgi.py @@ -11,7 +11,7 @@ # # For gunicorn additional globals need to be defined in the Gunicorn section. # Then the following command should run: -# $ gunicorn openerp:wsgi.core.application -c gunicorn.conf.py +# $ gunicorn openerp:application -c openerp-wsgi.py import openerp From ac2ad9aa8444125b938f48d99b4e6cb3083eb381 Mon Sep 17 00:00:00 2001 From: Antony Lesuisse Date: Sat, 22 Sep 2012 13:10:33 +0200 Subject: [PATCH 05/29] fix deps bzr revid: al@openerp.com-20120922111033-jzxjr8fpm15a3510 --- debian/control | 1 + setup.py | 1 + 2 files changed, 2 insertions(+) diff --git a/debian/control b/debian/control index f5d425c5586..7a1bfb62bb8 100644 --- a/debian/control +++ b/debian/control @@ -24,6 +24,7 @@ Depends: python-lxml, python-mako, python-openid, + python-psutil, python-psycopg2, python-pybabel, python-pychart, diff --git a/setup.py b/setup.py index a26b74e255d..18d341039ea 100755 --- a/setup.py +++ b/setup.py @@ -97,6 +97,7 @@ setuptools.setup( 'gdata', 'lxml < 3', 'mako', + 'psutil', 'psycopg2', 'pydot', 'python-dateutil < 2', From 2fdccd40c3976cded1b2d89130d3e830b76ebdab Mon Sep 17 00:00:00 2001 From: Antony Lesuisse Date: Sat, 22 Sep 2012 13:17:14 +0200 Subject: [PATCH 06/29] fix command line installation bzr revid: al@openerp.com-20120922111714-moxkguy729vout9x --- openerp-server | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openerp-server b/openerp-server index 5222de88eb4..9025e2d50c2 100755 --- a/openerp-server +++ b/openerp-server @@ -91,7 +91,7 @@ def setup_pid_file(): def preload_registry(dbname): """ Preload a registry, and start the cron.""" try: - db, registry = openerp.pooler.get_db_and_pool(dbname, update_module=config['init'] or config['update'], pooljobs=False) + db, registry = openerp.pooler.get_db_and_pool(dbname, update_module=openerp.tools.config['init'] or openerp.tools.config['update'], pooljobs=False) # jobs will start to be processed later, when openerp.cron.start_master_thread() is called by openerp.service.start_services() registry.schedule_cron_jobs() From 70f6e847f3001edb3aee08aff078930fcc093ac4 Mon Sep 17 00:00:00 2001 From: Rifakat Date: Mon, 24 Sep 2012 13:10:36 +0530 Subject: [PATCH 07/29] [FIX] stock: wrong stock accounting entries while doing return of any purchase/sales Accounting entries should be reverse while doing any return bzr revid: rha@tinyerp.com-20120924074036-0h7sybkwaegvg4tz --- addons/stock/stock.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/addons/stock/stock.py b/addons/stock/stock.py index 329159494d3..332a21895f2 100644 --- a/addons/stock/stock.py +++ b/addons/stock/stock.py @@ -2300,7 +2300,11 @@ class stock_move(osv.osv): or move.location_id.company_id != move.location_dest_id.company_id): journal_id, acc_src, acc_dest, acc_valuation = self._get_accounting_data_for_valuation(cr, uid, move, src_company_ctx) reference_amount, reference_currency_id = self._get_reference_accounting_values_for_valuation(cr, uid, move, src_company_ctx) - account_moves += [(journal_id, self._create_account_move_line(cr, uid, move, acc_valuation, acc_dest, reference_amount, reference_currency_id, context))] + #returning goods to supplier + if move.location_dest_id.usage == 'supplier': + account_moves += [(journal_id, self._create_account_move_line(cr, uid, move, acc_valuation, acc_src, reference_amount, reference_currency_id, context))] + else: + account_moves += [(journal_id, self._create_account_move_line(cr, uid, move, acc_valuation, acc_dest, reference_amount, reference_currency_id, context))] # Incoming moves (or cross-company input part) if move.location_dest_id.company_id \ @@ -2308,7 +2312,11 @@ class stock_move(osv.osv): or move.location_id.company_id != move.location_dest_id.company_id): journal_id, acc_src, acc_dest, acc_valuation = self._get_accounting_data_for_valuation(cr, uid, move, dest_company_ctx) reference_amount, reference_currency_id = self._get_reference_accounting_values_for_valuation(cr, uid, move, src_company_ctx) - account_moves += [(journal_id, self._create_account_move_line(cr, uid, move, acc_src, acc_valuation, reference_amount, reference_currency_id, context))] + #goods return from customer + if move.location_id.usage == 'customer': + account_moves += [(journal_id, self._create_account_move_line(cr, uid, move, acc_dest, acc_valuation, reference_amount, reference_currency_id, context))] + else: + account_moves += [(journal_id, self._create_account_move_line(cr, uid, move, acc_src, acc_valuation, reference_amount, reference_currency_id, context))] move_obj = self.pool.get('account.move') for j_id, move_lines in account_moves: From 7b4e8cbbf8c26cbf98db92a779672c4b9361870f Mon Sep 17 00:00:00 2001 From: "Quentin (OpenERP)" Date: Mon, 24 Sep 2012 12:33:07 +0200 Subject: [PATCH 08/29] [IMP] product: added constraints to make sure that the decimal precision of 'Account' is greater than the rounding factor of the company's main currency bzr revid: qdp-launchpad@openerp.com-20120924103307-4sdx61qxfy6lak2e --- addons/product/product.py | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/addons/product/product.py b/addons/product/product.py index ab282e722c0..144aa28c458 100644 --- a/addons/product/product.py +++ b/addons/product/product.py @@ -897,4 +897,43 @@ class pricelist_partnerinfo(osv.osv): } _order = 'min_quantity asc' pricelist_partnerinfo() + +class res_currency(osv.osv): + _inherit = 'res.currency' + + def _check_main_currency_rounding(self, cr, uid, ids, context=None): + cr.execute('SELECT digits FROM decimal_precision WHERE name like %s',('Account',)) + digits = cr.fetchone() + if len(digits): + digits = digits[0] + main_currency = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.currency_id + for currency_id in ids: + if currency_id == main_currency.id: + if main_currency.rounding < 10 ** -digits: + return False + return True + + _constraints = [ + (_check_main_currency_rounding, 'Error! You cannot define a rounding factor for the company\'s main currency that is smaller than the decimal precision of \'Account\'.', ['rounding']), + ] + +class decimal_precision(osv.osv): + _inherit = 'decimal.precision' + + def _check_main_currency_rounding(self, cr, uid, ids, context=None): + cr.execute('SELECT id, digits FROM decimal_precision WHERE name like %s',('Account',)) + res = cr.fetchone() + if len(res): + account_precision_id, digits = res + main_currency = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.currency_id + for decimal_precision in ids: + if decimal_precision == account_precision_id: + if main_currency.rounding < 10 ** -digits: + return False + return True + + _constraints = [ + (_check_main_currency_rounding, 'Error! You cannot define the decimal precision of \'Account\' as greater than the rounding factor of the company\'s main currency', ['digits']), + ] + # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: From 6c60619a2bfeec6f3241e47ff4dbe1dd4844132c Mon Sep 17 00:00:00 2001 From: Antony Lesuisse Date: Mon, 24 Sep 2012 12:36:48 +0200 Subject: [PATCH 09/29] [FIX] wsgi wrapper bzr revid: al@openerp.com-20120924103648-uhxteeu6nd8batvd --- openerp-wsgi.py | 7 ++++--- openerp/__init__.py | 2 +- openerp/service/__init__.py | 4 ++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/openerp-wsgi.py b/openerp-wsgi.py index 5ab7afc406e..48ffeaf3373 100644 --- a/openerp-wsgi.py +++ b/openerp-wsgi.py @@ -7,7 +7,7 @@ # # For generic wsgi handlers a global application is defined. # For uwsgi this should work: -# $ uwsgi --http :9090 --pythonpath . --wsgi-file openerp-wsgi.py +# $ uwsgi_python --http :9090 --pythonpath . --wsgi-file openerp-wsgi.py # # For gunicorn additional globals need to be defined in the Gunicorn section. # Then the following command should run: @@ -18,15 +18,16 @@ import openerp #---------------------------------------------------------- # Common #---------------------------------------------------------- +openerp.multi_process = True # Nah! + # Equivalent of --load command-line option openerp.conf.server_wide_modules = ['web'] conf = openerp.tools.config # Path to the OpenERP Addons repository (comma-separated for # multiple locations) -conf['addons_path'] = '/home/openerp/addons/trunk,/home/openerp/web/trunk/addons' -conf['addons_path'] = '/home/wis/stuff/version/openerp/source/addons/6.1,/home/wis/stuff/version/openerp/source/web/6.1/addons' +conf['addons_path'] = '../../addons/trunk,../../web/trunk/addons' # Optional database config if not using local socket #conf['db_name'] = 'mycompany' diff --git a/openerp/__init__.py b/openerp/__init__.py index 2fd2a9812f9..3a8f5f86f26 100644 --- a/openerp/__init__.py +++ b/openerp/__init__.py @@ -50,7 +50,7 @@ wsgi.register_wsgi_handler = wsgi.wsgi_server.register_wsgi_handler # e.g. database update or cache invalidation. Each process has also # its own copy of the data structure and we don't need to care about # locks between threads. -multi_process = True +multi_process = False # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/service/__init__.py b/openerp/service/__init__.py index 30a765b1f60..ef0fd757ddd 100644 --- a/openerp/service/__init__.py +++ b/openerp/service/__init__.py @@ -78,8 +78,6 @@ def start_internal(): def start_services(): """ Start all services including http, netrpc and cron """ - openerp.multi_process = False # Nah! - start_internal() # Initialize the HTTP stack. @@ -120,6 +118,8 @@ def stop_services(): openerp.modules.registry.RegistryManager.delete_all() def start_services_workers(): + openerp.multi_process = True # Nah! + openerp.service.workers.Multicorn(openerp.service.wsgi_server.application).run() From 470415ced97698c66bab4ae912e6d08c4831e54d Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Mon, 24 Sep 2012 12:40:26 +0200 Subject: [PATCH 10/29] [IMP] ir.ui.view: discard custom views before updating view arch - as the custom views are still in place when validating the new arch, they would hide the new arch and prevent proper validation (if custom views are set for uid 1) - conversely, if the RNG validation has changed, the old view customizations may not pass the updated validation rules, and cause spurious validation errors bzr revid: odo@openerp.com-20120924104026-z7bjzzq80ifxy1oc --- openerp/addons/base/ir/ir_ui_view.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py index bad2c3d85af..9208297cf84 100644 --- a/openerp/addons/base/ir/ir_ui_view.py +++ b/openerp/addons/base/ir/ir_ui_view.py @@ -192,7 +192,6 @@ class view(osv.osv): def write(self, cr, uid, ids, vals, context=None): if not isinstance(ids, (list, tuple)): ids = [ids] - result = super(view, self).write(cr, uid, ids, vals, context) # drop the corresponding view customizations (used for dashboards for example), otherwise # not all users would see the updated views @@ -200,7 +199,7 @@ class view(osv.osv): if custom_view_ids: self.pool.get('ir.ui.view.custom').unlink(cr, uid, custom_view_ids) - return result + return super(view, self).write(cr, uid, ids, vals, context) def graph_get(self, cr, uid, id, model, node_obj, conn_obj, src_node, des_node, label, scale, context=None): nodes=[] From feb66a13c39a6605f7e1d46b5ffe5c108a0205c5 Mon Sep 17 00:00:00 2001 From: Antony Lesuisse Date: Mon, 24 Sep 2012 12:50:11 +0200 Subject: [PATCH 11/29] [FIX] gunicorn wsgi wrapper instructions bzr revid: al@openerp.com-20120924105011-2gtopghuooxh37o0 --- openerp-wsgi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openerp-wsgi.py b/openerp-wsgi.py index 48ffeaf3373..9ae18ffc69e 100644 --- a/openerp-wsgi.py +++ b/openerp-wsgi.py @@ -11,7 +11,7 @@ # # For gunicorn additional globals need to be defined in the Gunicorn section. # Then the following command should run: -# $ gunicorn openerp:application -c openerp-wsgi.py +# $ gunicorn openerp:service.wsgi_server.application -c openerp-wsgi.py import openerp From d77fe9c1853b046fb97f77e96416c4bcdf7de4d8 Mon Sep 17 00:00:00 2001 From: Antony Lesuisse Date: Mon, 24 Sep 2012 12:56:22 +0200 Subject: [PATCH 12/29] [FIX] restore poxy-mode command line option to prevent ip address impersonation bzr revid: al@openerp.com-20120924105622-nb3iwe1vnuffqrbm --- openerp/service/wsgi_server.py | 2 +- openerp/tools/config.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/openerp/service/wsgi_server.py b/openerp/service/wsgi_server.py index 61556974a00..a73617d8510 100644 --- a/openerp/service/wsgi_server.py +++ b/openerp/service/wsgi_server.py @@ -402,7 +402,7 @@ def application_unproxied(environ, start_response): return [response] def application(environ, start_response): - if 'HTTP_X_FORWARDED_HOST' in environ: + if config['proxy_mode'] and 'HTTP_X_FORWARDED_HOST' in environ: return werkzeug.contrib.fixers.ProxyFix(application_unproxied)(environ, start_response) else: return application_unproxied(environ, start_response) diff --git a/openerp/tools/config.py b/openerp/tools/config.py index 58d1d3c9c82..18b442221c9 100644 --- a/openerp/tools/config.py +++ b/openerp/tools/config.py @@ -116,6 +116,8 @@ class configmanager(object): help="specify the TCP port for the XML-RPC protocol", type="int") group.add_option("--no-xmlrpc", dest="xmlrpc", action="store_false", my_default=True, help="disable the XML-RPC protocol") + group.add_option("--proxy-mode", dest="proxy_mode", action="store_true", my_default=False, + help="Enable correct behavior when behind a reverse proxy") parser.add_option_group(group) # XML-RPC / HTTPS @@ -394,7 +396,7 @@ class configmanager(object): 'language', 'translate_out', 'translate_in', 'overwrite_existing_translations', 'debug_mode', 'smtp_ssl', 'load_language', 'stop_after_init', 'logrotate', 'without_demo', 'netrpc', 'xmlrpc', 'syslog', - 'list_db', 'xmlrpcs', + 'list_db', 'xmlrpcs', 'proxy_mode', 'test_file', 'test_enable', 'test_commit', 'test_report_directory', 'osv_memory_count_limit', 'osv_memory_age_limit', 'max_cron_threads', 'unaccent', 'workers', 'limit_memory_hard', 'limit_memory_soft', 'limit_time_cpu', 'limit_time_real', 'limit_request' From dfeeca1e8fd702bddd7ace33090d99395c485811 Mon Sep 17 00:00:00 2001 From: "Quentin (OpenERP)" Date: Mon, 24 Sep 2012 13:04:50 +0200 Subject: [PATCH 13/29] [FIX] account: fixed list_partners_to_reconcile() in order to return all partners and not the first one only bzr revid: qdp-launchpad@openerp.com-20120924110450-q1k51qv0p2djumxk --- addons/account/account_move_line.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/account/account_move_line.py b/addons/account/account_move_line.py index 2aed5b02107..5ddbbee74f2 100644 --- a/addons/account/account_move_line.py +++ b/addons/account/account_move_line.py @@ -736,7 +736,7 @@ class account_move_line(osv.osv): WHERE debit > 0 AND credit > 0 ORDER BY last_reconciliation_date""") ids = cr.fetchall() - ids = len(ids) and list(ids[0]) or [] + ids = len(ids) and [x[0] for x in ids] or [] return self.pool.get('res.partner').name_get(cr, uid, ids, context=context) def reconcile_partial(self, cr, uid, ids, type='auto', context=None, writeoff_acc_id=False, writeoff_period_id=False, writeoff_journal_id=False): From 29badc35a54e00f0ca0d516473eb82016d2b9355 Mon Sep 17 00:00:00 2001 From: "Quentin (OpenERP)" Date: Mon, 24 Sep 2012 13:16:41 +0200 Subject: [PATCH 14/29] [FIX] product: 'Nonetype object has no len()' error fixed in newly added constraints for decimal precision of 'Account' <= company's main currency rounding bzr revid: qdp-launchpad@openerp.com-20120924111641-9papztx074vdbrpq --- addons/product/product.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/product/product.py b/addons/product/product.py index 144aa28c458..df0ab9122c2 100644 --- a/addons/product/product.py +++ b/addons/product/product.py @@ -904,7 +904,7 @@ class res_currency(osv.osv): def _check_main_currency_rounding(self, cr, uid, ids, context=None): cr.execute('SELECT digits FROM decimal_precision WHERE name like %s',('Account',)) digits = cr.fetchone() - if len(digits): + if digits and len(digits): digits = digits[0] main_currency = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.currency_id for currency_id in ids: @@ -923,7 +923,7 @@ class decimal_precision(osv.osv): def _check_main_currency_rounding(self, cr, uid, ids, context=None): cr.execute('SELECT id, digits FROM decimal_precision WHERE name like %s',('Account',)) res = cr.fetchone() - if len(res): + if res and len(res): account_precision_id, digits = res main_currency = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.currency_id for decimal_precision in ids: From f7255ac0b3b7e6b3d2dc67b571f7a169b2019598 Mon Sep 17 00:00:00 2001 From: Christophe Simonis Date: Mon, 24 Sep 2012 13:53:44 +0200 Subject: [PATCH 15/29] [FIX] clear breadcrumbs for client actions bzr revid: chs@openerp.com-20120924115344-rbtaus7a48pr56ka --- addons/web/static/src/js/views.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/web/static/src/js/views.js b/addons/web/static/src/js/views.js index 608a856785e..d170aacc89d 100644 --- a/addons/web/static/src/js/views.js +++ b/addons/web/static/src/js/views.js @@ -318,8 +318,8 @@ instance.web.ActionManager = instance.web.Widget.extend({ } return this.ir_actions_common(action, on_close, clear_breadcrumbs); }, - ir_actions_client: function (action, on_close) { - return this.ir_actions_common(action, on_close); + ir_actions_client: function (action, on_close, clear_breadcrumbs) { + return this.ir_actions_common(action, on_close, clear_breadcrumbs); }, ir_actions_act_window_close: function (action, on_closed) { if (!this.dialog && on_closed) { From 4488c42ee532cb6715d986764f5532f26476cd25 Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Mon, 24 Sep 2012 14:53:55 +0200 Subject: [PATCH 16/29] [FIX] problem in o2m, not reloading when we click a button in a o2m form popup bzr revid: nicolas.vanhoren@openerp.com-20120924125355-m1b53sjb6hle789z --- addons/web/static/src/js/view_form.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/addons/web/static/src/js/view_form.js b/addons/web/static/src/js/view_form.js index af1980b7b6e..e3b7bde1bfe 100644 --- a/addons/web/static/src/js/view_form.js +++ b/addons/web/static/src/js/view_form.js @@ -940,6 +940,15 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM return self.dataset.parent_view.recursive_save(); }); }, + recursive_reload: function() { + var self = this; + var pre = $.when(); + if (self.dataset.parent_view) + pre = self.dataset.parent_view.recursive_reload(); + return pre.pipe(function() { + return self.reload(); + }); + }, is_dirty: function() { return _.any(this.fields, function (value_) { return value_._dirty_flag; @@ -1809,7 +1818,7 @@ instance.web.form.WidgetButton = instance.web.form.FormWidget.extend({ return this.view.do_execute_action( _.extend({}, this.node.attrs, {context: context}), this.view.dataset, this.view.datarecord.id, function () { - self.view.reload(); + self.view.recursive_reload(); }); }, check_disable: function() { From b3027a371cef466856307b5ac88c1fcc69a0be81 Mon Sep 17 00:00:00 2001 From: "Quentin (OpenERP)" Date: Mon, 24 Sep 2012 15:45:33 +0200 Subject: [PATCH 17/29] [IMP] account: improved demo data to add a second bank journal, in foreign currency bzr revid: qdp-launchpad@openerp.com-20120924134533-s1j8mmqou9uw2mdt --- addons/account/demo/account_minimal.xml | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/addons/account/demo/account_minimal.xml b/addons/account/demo/account_minimal.xml index 53f2b54e0ef..2647364597b 100644 --- a/addons/account/demo/account_minimal.xml +++ b/addons/account/demo/account_minimal.xml @@ -122,6 +122,14 @@ other + + X11007 + USD Bank Account - (test) + + liquidity + + + Liabilities - (test) @@ -360,8 +368,8 @@ bank - - + + @@ -412,6 +420,17 @@ + + + USD Bank Journal - (test) + TUBK + bank + + + + + + From 247752851272d5bb3e222d31fcde81e1259e9f17 Mon Sep 17 00:00:00 2001 From: "Quentin (OpenERP)" Date: Mon, 24 Sep 2012 15:49:10 +0200 Subject: [PATCH 18/29] [FIX] l10n_ch: linked the demo journal of BANK CHF JOURNAL to the CHF currency bzr revid: qdp-launchpad@openerp.com-20120924134910-5yye9t2m7yrqhhu7 --- addons/l10n_ch/journal_data.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/addons/l10n_ch/journal_data.xml b/addons/l10n_ch/journal_data.xml index 04870ca936b..ede9136c3d2 100644 --- a/addons/l10n_ch/journal_data.xml +++ b/addons/l10n_ch/journal_data.xml @@ -19,6 +19,7 @@ + From edb74f79b1d502921dbf58c179d67f624b5a9879 Mon Sep 17 00:00:00 2001 From: "Quentin (OpenERP)" Date: Mon, 24 Sep 2012 16:06:17 +0200 Subject: [PATCH 19/29] [IMP] base: added a new rate for USD, for demo transactions in multi currency bzr revid: qdp-launchpad@openerp.com-20120924140617-e0mlvtz2pf7icqo2 --- openerp/addons/base/base_demo.xml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/openerp/addons/base/base_demo.xml b/openerp/addons/base/base_demo.xml index 064b1f49e69..ce675ebb99e 100644 --- a/openerp/addons/base/base_demo.xml +++ b/openerp/addons/base/base_demo.xml @@ -14,5 +14,12 @@ + + + + 1.5289 + + + From de9eb7f78cdb3dcaa3c4320498f16dcda0a43a24 Mon Sep 17 00:00:00 2001 From: Antonin Bourguignon Date: Mon, 24 Sep 2012 16:06:32 +0200 Subject: [PATCH 20/29] [IMP] indentation and arguments order for readabily sake bzr revid: abo@openerp.com-20120924140632-mig0szmfvopl1wq0 --- addons/account/security/account_security.xml | 46 ++++++++++--------- .../portal_sale/security/portal_security.xml | 2 +- 2 files changed, 26 insertions(+), 22 deletions(-) diff --git a/addons/account/security/account_security.xml b/addons/account/security/account_security.xml index 26265cb2b2c..afbd19289b6 100644 --- a/addons/account/security/account_security.xml +++ b/addons/account/security/account_security.xml @@ -1,5 +1,6 @@ - + + Invoicing & Payments @@ -7,11 +8,13 @@ + Accountant + Manager @@ -25,107 +28,107 @@ Account Entry - - + + ['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])] - Entry lines + Entry lines - + ['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])] Journal Period - + ['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])] - + Journal multi-company - + ['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])] Analytic journal multi-company - + ['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])] Period multi-company - + ['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])] Fiscal year multi-company - + ['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])] Account multi-company - + ['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])] - Tax multi-company + Tax multi-company - + ['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])] Tax code multi-company - + ['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])] Invoice multi-company - + ['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])] Invoice Analysis multi-company - + ['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])] Entries Analysis multi-company - + ['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])] Account fiscal Mapping company rule - + ['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])] Account model company rule - + ['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])] @@ -150,4 +153,5 @@ ['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])] - + + diff --git a/addons/portal_sale/security/portal_security.xml b/addons/portal_sale/security/portal_security.xml index 8028d5360c4..2f161a73281 100644 --- a/addons/portal_sale/security/portal_security.xml +++ b/addons/portal_sale/security/portal_security.xml @@ -12,7 +12,7 @@ Portal Personal Delivery Orders - + [('partner_id','child_of',[user.partner_id.id])] From aae20beef065bfd394ad5c7d988d06ae276c541b Mon Sep 17 00:00:00 2001 From: Antonin Bourguignon Date: Mon, 24 Sep 2012 16:10:47 +0200 Subject: [PATCH 21/29] [IMP] remove the useless attribute model="ir.model" bzr revid: abo@openerp.com-20120924141047-2mang092z0ak288g --- addons/account/security/account_security.xml | 28 ++++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/addons/account/security/account_security.xml b/addons/account/security/account_security.xml index afbd19289b6..442b51466f8 100644 --- a/addons/account/security/account_security.xml +++ b/addons/account/security/account_security.xml @@ -35,14 +35,14 @@ Entry lines - + ['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])] Journal Period - + ['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])] @@ -50,84 +50,84 @@ Journal multi-company - + ['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])] Analytic journal multi-company - + ['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])] Period multi-company - + ['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])] Fiscal year multi-company - + ['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])] Account multi-company - + ['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])] Tax multi-company - + ['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])] Tax code multi-company - + ['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])] Invoice multi-company - + ['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])] Invoice Analysis multi-company - + ['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])] Entries Analysis multi-company - + ['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])] Account fiscal Mapping company rule - + ['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])] Account model company rule - + ['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])] From 9aa98a03d1ff0ca456e67041a7b73d436a44d358 Mon Sep 17 00:00:00 2001 From: "Quentin (OpenERP)" Date: Mon, 24 Sep 2012 16:40:27 +0200 Subject: [PATCH 22/29] [FIX] account: fixed missing reference to sequence in demo journal bzr revid: qdp-launchpad@openerp.com-20120924144027-jxj61tt3cg0vud5j --- addons/account/demo/account_minimal.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/addons/account/demo/account_minimal.xml b/addons/account/demo/account_minimal.xml index 2647364597b..8f879d6b465 100644 --- a/addons/account/demo/account_minimal.xml +++ b/addons/account/demo/account_minimal.xml @@ -426,7 +426,6 @@ TUBK bank - From 7b6280243572f4e0c60772be23ba8fc154e541b2 Mon Sep 17 00:00:00 2001 From: Antonin Bourguignon Date: Mon, 24 Sep 2012 17:34:30 +0200 Subject: [PATCH 23/29] [IMP] use kwargs instead of a new dict bzr revid: abo@openerp.com-20120924153430-0nb2gxker8v9is5p --- addons/email_template/email_template.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/addons/email_template/email_template.py b/addons/email_template/email_template.py index 6f48d37c714..cc4b52992a4 100644 --- a/addons/email_template/email_template.py +++ b/addons/email_template/email_template.py @@ -236,9 +236,7 @@ class email_template(osv.osv): default = {} default = default.copy() default['name'] = template.name + _('(copy)') - default.update({ - 'ref_ir_act_window': False, - 'ref_ir_value': False, + default.update(ref_ir_act_window=False, ref_ir_value=False) }) return super(email_template, self).copy(cr, uid, id, default, context) @@ -392,7 +390,7 @@ class email_template(osv.osv): :param bool force_send: if True, the generated mail.message is immediately sent after being created, as if the scheduler was executed for this message only. - :returns: id of the mail.message that was created + :returns: id of the mail.message that was created """ if context is None: context = {} mail_message = self.pool.get('mail.message') From f7b90af93ad6d97444d56ec81ee9d3ce959299e4 Mon Sep 17 00:00:00 2001 From: Antonin Bourguignon Date: Mon, 24 Sep 2012 17:56:00 +0200 Subject: [PATCH 24/29] [FIX] forgot to remove }) bzr revid: abo@openerp.com-20120924155600-dnaanyupa5jm0yre --- addons/email_template/email_template.py | 1 - 1 file changed, 1 deletion(-) diff --git a/addons/email_template/email_template.py b/addons/email_template/email_template.py index 53d484bd92d..87bdfccbbcf 100644 --- a/addons/email_template/email_template.py +++ b/addons/email_template/email_template.py @@ -217,7 +217,6 @@ class email_template(osv.osv): default = default.copy() default['name'] = template.name + _('(copy)') default.update(ref_ir_act_window=False, ref_ir_value=False) - }) return super(email_template, self).copy(cr, uid, id, default, context) def build_expression(self, field_name, sub_field_name, null_value): From 70a1aac877933c591b3d807dbed541c92fc7a6fe Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Mon, 24 Sep 2012 18:07:21 +0200 Subject: [PATCH 25/29] [IMP] minor change in form view fields bzr revid: nicolas.vanhoren@openerp.com-20120924160721-hay0ar7ap22gjyk7 --- addons/web/static/src/js/view_form.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/addons/web/static/src/js/view_form.js b/addons/web/static/src/js/view_form.js index e3b7bde1bfe..dff9df11140 100644 --- a/addons/web/static/src/js/view_form.js +++ b/addons/web/static/src/js/view_form.js @@ -331,7 +331,9 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM _(this.fields).each(function (field, f) { field._dirty_flag = false; + field._inhibit_on_change_flag = true; var result = field.set_value(self.datarecord[f] || false); + field._inhibit_on_change_flag = false; set_values.push(result); }); return $.when.apply(null, set_values).pipe(function() { @@ -580,7 +582,9 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM if (field) { var value_ = result.value[f]; if (field.get_value() != value_) { + field._inhibit_on_change_flag = true; field.set_value(value_); + field._inhibit_on_change_flag = false; field._dirty_flag = true; if (!_.contains(processed, field.name)) { this.do_onchange(field, processed); @@ -1052,6 +1056,9 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM this.translatable_fields.push(field); } field.on('changed_value', this, function() { + if (field._inhibit_on_change_flag) { + return; + } field._dirty_flag = true; if (field.is_syntax_valid()) { this.do_onchange(field); @@ -1948,8 +1955,7 @@ instance.web.form.AbstractField = instance.web.form.FormWidget.extend(instance.w this.on("change:force_readonly", this, test_effective_readonly); test_effective_readonly.call(this); this.on("change:value", this, function() { - if (! this._inhibit_on_change) - this.trigger('changed_value'); + this.trigger('changed_value'); this._check_css_flags(); }); }, @@ -1984,9 +1990,7 @@ instance.web.form.AbstractField = instance.web.form.FormWidget.extend(instance.w this.$el.toggleClass('oe_form_required', this.get("required")); }, set_value: function(value_) { - this._inhibit_on_change = true; this.set({'value': value_}); - this._inhibit_on_change = false; }, get_value: function() { return this.get('value'); @@ -3321,6 +3325,7 @@ instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({ this.dataset.index = 0; } self.is_setted.resolve(); + this.trigger_on_change(); return self.reload_current_view(); }, get_value: function() { From 1fbb4a5c16232c6130b31d870be4e44ecc743a72 Mon Sep 17 00:00:00 2001 From: Antonin Bourguignon Date: Mon, 24 Sep 2012 18:26:45 +0200 Subject: [PATCH 26/29] [IMP] handling of '(copy)' l10n bzr revid: abo@openerp.com-20120924162645-z1h4l2mj6c8i6wr3 --- addons/account/account.py | 9 ++-- addons/account_coda/account_coda.py | 11 +++-- addons/analytic/analytic.py | 7 +-- addons/document/document.py | 2 +- addons/document/document_directory.py | 2 +- addons/email_template/email_template.py | 4 +- addons/hr_payroll/hr_payroll.py | 11 ++--- addons/mrp/mrp.py | 2 +- addons/product/product.py | 2 +- addons/project/project.py | 6 +-- addons/project_issue/project_issue.py | 6 +-- addons/project_long_term/project_long_term.py | 6 +-- addons/resource/resource.py | 48 +++++++++---------- addons/survey/survey.py | 6 +-- 14 files changed, 62 insertions(+), 60 deletions(-) diff --git a/addons/account/account.py b/addons/account/account.py index d145f0308b0..c2e6f3d1221 100644 --- a/addons/account/account.py +++ b/addons/account/account.py @@ -601,7 +601,7 @@ class account_account(osv.osv): if not default: default = {} default = default.copy() - default['code'] = (account['code'] or '') + '(copy)' + default.update(code=_("%s (copy)") % (account['code'] or '')) if not local: done_list = [] if account.id in done_list: @@ -782,9 +782,10 @@ class account_journal(osv.osv): if not default: default = {} default = default.copy() - default['code'] = (journal['code'] or '') + '(copy)' - default['name'] = (journal['name'] or '') + '(copy)' - default['sequence_id'] = False + default.update( + code=_("%s (copy)") % (journal['code'] or ''), + name=_("%s (copy)") % (journal['name'] or ''), + sequence_id=False) return super(account_journal, self).copy(cr, uid, id, default, context=context) def write(self, cr, uid, ids, vals, context=None): diff --git a/addons/account_coda/account_coda.py b/addons/account_coda/account_coda.py index cd117b5be88..1aa6e84402a 100644 --- a/addons/account_coda/account_coda.py +++ b/addons/account_coda/account_coda.py @@ -107,11 +107,12 @@ class coda_bank_account(osv.osv): if not default: default = {} default = default.copy() - default.update({'journal_id': None}) - default['description1'] = cba['description1'] or '' - default['description2'] = cba['description2'] or '' - default['name'] = (cba['name'] or '') + ' (copy)' - default['state'] = cba['state'] + default.update( + journal_id=None, + description1=cba['description1'] or '', + description2=cba['description2'] or '', + name=_("%s (copy)") % (cba['name'] or ''), + state=cba['state']) return super(coda_bank_account, self).copy(cr, uid, id, default, context) def onchange_state(self, cr, uid, ids, state): diff --git a/addons/analytic/analytic.py b/addons/analytic/analytic.py index d3a22782a68..269bf24ed61 100644 --- a/addons/analytic/analytic.py +++ b/addons/analytic/analytic.py @@ -223,9 +223,10 @@ class account_analytic_account(osv.osv): if not default: default = {} analytic = self.browse(cr, uid, id, context=context) - default['code'] = False - default['line_ids'] = [] - default['name'] = analytic['name'] + ' (' + _('copy') + ')' + default.update( + code=False, + line_ids=[], + name=_("%s (copy)") % (analytic['name'])) return super(account_analytic_account, self).copy(cr, uid, id, default, context=context) def on_change_company(self, cr, uid, id, company_id): diff --git a/addons/document/document.py b/addons/document/document.py index 3ee52f8e4fb..cb30e5f93c9 100644 --- a/addons/document/document.py +++ b/addons/document/document.py @@ -209,7 +209,7 @@ class document_file(osv.osv): default = {} if 'name' not in default: name = self.read(cr, uid, [id], ['name'])[0]['name'] - default.update({'name': name + " " + _("(copy)")}) + default.update(name=_("%s (copy)") % (name)) return super(document_file, self).copy(cr, uid, id, default, context=context) def write(self, cr, uid, ids, vals, context=None): diff --git a/addons/document/document_directory.py b/addons/document/document_directory.py index 1ead23ab402..ac0671d36d3 100644 --- a/addons/document/document_directory.py +++ b/addons/document/document_directory.py @@ -231,7 +231,7 @@ class document_directory(osv.osv): if not default: default ={} name = self.read(cr, uid, [id])[0]['name'] - default.update({'name': name+ " (copy)"}) + default.update(name=_("%s (copy)") % (name)) return super(document_directory,self).copy(cr, uid, id, default, context=context) def _check_duplication(self, cr, uid, vals, ids=[], op='create'): diff --git a/addons/email_template/email_template.py b/addons/email_template/email_template.py index 4080e62f002..dd05d1850fd 100644 --- a/addons/email_template/email_template.py +++ b/addons/email_template/email_template.py @@ -215,7 +215,7 @@ class email_template(osv.osv): if default is None: default = {} default = default.copy() - default['name'] = template.name + _('(copy)') + default.update(name=_("%s (copy)") % (template.name)) return super(email_template, self).copy(cr, uid, id, default, context) def build_expression(self, field_name, sub_field_name, null_value): @@ -335,7 +335,7 @@ class email_template(osv.osv): :param bool force_send: if True, the generated mail.message is immediately sent after being created, as if the scheduler was executed for this message only. - :returns: id of the mail.message that was created + :returns: id of the mail.message that was created """ if context is None: context = {} mail_mail = self.pool.get('mail.mail') diff --git a/addons/hr_payroll/hr_payroll.py b/addons/hr_payroll/hr_payroll.py index 6a2e40b9571..08dacc9a951 100644 --- a/addons/hr_payroll/hr_payroll.py +++ b/addons/hr_payroll/hr_payroll.py @@ -81,10 +81,9 @@ class hr_payroll_structure(osv.osv): """ if not default: default = {} - default.update({ - 'code': self.browse(cr, uid, id, context=context).code + "(copy)", - 'company_id': self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.id - }) + default.update( + code=_("%s (copy)") % (self.browse(cr, uid, id, context=context).code), + company_id=self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.id) return super(hr_payroll_structure, self).copy(cr, uid, id, default, context=context) def get_all_rules(self, cr, uid, structure_ids, context=None): @@ -305,7 +304,7 @@ class hr_payslip(osv.osv): return False return True - _constraints = [(_check_dates, "Payslip 'Date From' must be before 'Date To'.", ['date_from', 'date_to'])] + _constraints = [(_check_dates, "Payslip 'Date From' must be before 'Date To'.", ['date_from', 'date_to'])] def copy(self, cr, uid, id, default=None, context=None): if not default: @@ -851,7 +850,7 @@ result = rules.NET > categories.NET * 0.10''', """ :param rule_id: id of rule to compute :param localdict: dictionary containing the environement in which to compute the rule - :return: returns a tuple build as the base/amount computed, the quantity and the rate + :return: returns a tuple build as the base/amount computed, the quantity and the rate :rtype: (float, float, float) """ rule = self.browse(cr, uid, rule_id, context=context) diff --git a/addons/mrp/mrp.py b/addons/mrp/mrp.py index ae29ad324db..94ee4bdb3b2 100644 --- a/addons/mrp/mrp.py +++ b/addons/mrp/mrp.py @@ -356,7 +356,7 @@ class mrp_bom(osv.osv): if default is None: default = {} bom_data = self.read(cr, uid, id, [], context=context) - default.update({'name': bom_data['name'] + ' ' + _('Copy'), 'bom_id':False}) + default.update(name=_("%s (copy)") % (bom_data['name']), bom_id=False) return super(mrp_bom, self).copy_data(cr, uid, id, default, context=context) def create(self, cr, uid, vals, context=None): diff --git a/addons/product/product.py b/addons/product/product.py index df0ab9122c2..8bc0789d44b 100644 --- a/addons/product/product.py +++ b/addons/product/product.py @@ -727,7 +727,7 @@ class product_product(osv.osv): context_wo_lang.pop('lang', None) product = self.read(cr, uid, id, ['name'], context=context_wo_lang) default = default.copy() - default['name'] = product['name'] + ' (' + _('copy') + ')' + default.update(name=_("%s (copy)") % (product['name'])) if context.get('variant',False): fields = ['product_tmpl_id', 'active', 'variants', 'default_code', diff --git a/addons/project/project.py b/addons/project/project.py index f15656905ca..3572fd094b3 100644 --- a/addons/project/project.py +++ b/addons/project/project.py @@ -323,7 +323,7 @@ class project(osv.osv): default.pop('alias_id', None) proj = self.browse(cr, uid, id, context=context) if not default.get('name', False): - default['name'] = proj.name + _(' (copy)') + default.update(name=_("%s (copy)") % (proj.name)) res = super(project, self).copy(cr, uid, id, default, context) self.map_tasks(cr,uid,id,res,context) return res @@ -344,7 +344,7 @@ class project(osv.osv): new_date_end = (datetime(*time.strptime(new_date_start,'%Y-%m-%d')[:3])+(end_date-start_date)).strftime('%Y-%m-%d') context.update({'copy':True}) new_id = self.copy(cr, uid, proj.id, default = { - 'name': proj.name +_(' (copy)'), + 'name':_("%s (copy)") % (proj.name), 'state':'open', 'date_start':new_date_start, 'date':new_date_end, @@ -694,7 +694,7 @@ class task(base_stage, osv.osv): if not default.get('name', False): default['name'] = self.browse(cr, uid, id, context=context).name or '' if not context.get('copy',False): - new_name = _("%s (copy)")%default.get('name','') + new_name = _("%s (copy)") % (default.get('name', '')) default.update({'name':new_name}) return super(task, self).copy_data(cr, uid, id, default, context) diff --git a/addons/project_issue/project_issue.py b/addons/project_issue/project_issue.py index 6121acbd607..f39be0c09d5 100644 --- a/addons/project_issue/project_issue.py +++ b/addons/project_issue/project_issue.py @@ -355,7 +355,7 @@ class project_issue(base_stage, osv.osv): if not default: default = {} default = default.copy() - default['name'] = issue['name'] + _(' (copy)') + default.update(name=_('%s (copy)') % (issue['name'])) return super(project_issue, self).copy(cr, uid, id, default=default, context=context) @@ -534,7 +534,7 @@ class project(osv.osv): def _get_alias_models(self, cr, uid, context=None): return [('project.task', "Tasks"), ("project.issue", "Issues")] - + def _issue_count(self, cr, uid, ids, field_name, arg, context=None): res = dict.fromkeys(ids, 0) issue_ids = self.pool.get('project.issue').search(cr, uid, [('project_id', 'in', ids)]) @@ -557,7 +557,7 @@ class project(osv.osv): _constraints = [ (_check_escalation, 'Error! You cannot assign escalation to the same project!', ['project_escalation_id']) ] - + project() class account_analytic_account(osv.osv): diff --git a/addons/project_long_term/project_long_term.py b/addons/project_long_term/project_long_term.py index 2eab5fa6ac6..5b88d5209c8 100644 --- a/addons/project_long_term/project_long_term.py +++ b/addons/project_long_term/project_long_term.py @@ -138,7 +138,7 @@ class project_phase(osv.osv): if default is None: default = {} if not default.get('name', False): - default['name'] = self.browse(cr, uid, id, context=context).name + _(' (copy)') + default.update(name=_('%s (copy)') % (self.browse(cr, uid, id, context=context).name)) return super(project_phase, self).copy(cr, uid, id, default, context) def set_draft(self, cr, uid, ids, *args): @@ -227,7 +227,7 @@ class project(osv.osv): 'phase_ids': fields.one2many('project.phase', 'project_id', "Project Phases"), 'phase_count': fields.function(_phase_count, type='integer', string="Open Phases"), } - + def schedule_phases(self, cr, uid, ids, context=None): context = context or {} if type(ids) in (long, int,): @@ -250,7 +250,7 @@ class project(osv.osv): # Maybe it's better to update than unlink/create if it already exists ? p = getattr(project_gantt, 'Phase_%d' % (phase.id,)) - self.pool.get('project.user.allocation').unlink(cr, uid, + self.pool.get('project.user.allocation').unlink(cr, uid, [x.id for x in phase.user_ids], context=context ) diff --git a/addons/resource/resource.py b/addons/resource/resource.py index 354e9c5b27c..ed21eea85cc 100644 --- a/addons/resource/resource.py +++ b/addons/resource/resource.py @@ -45,24 +45,24 @@ class resource_calendar(osv.osv): def working_hours_on_day(self, cr, uid, resource_calendar_id, day, context=None): """Calculates the Working Total Hours based on Resource Calendar and given working day (datetime object). - + @param resource_calendar_id: resource.calendar browse record @param day: datetime object - + @return: returns the working hours (as float) men should work on the given day if is in the attendance_ids of the resource_calendar_id (i.e if that day is a working day), returns 0.0 otherwise """ res = 0.0 for working_day in resource_calendar_id.attendance_ids: if (int(working_day.dayofweek) + 1) == day.isoweekday(): res += working_day.hour_to - working_day.hour_from - return res + return res def _get_leaves(self, cr, uid, id, resource): - """Private Method to Calculate resource Leaves days - + """Private Method to Calculate resource Leaves days + @param id: resource calendar id - @param resource: resource id for which leaves will ew calculated - + @param resource: resource id for which leaves will ew calculated + @return : returns the list of dates, where resource on leave in resource.calendar.leaves object (e.g.['%Y-%m-%d', '%Y-%m-%d']) """ @@ -85,9 +85,9 @@ class resource_calendar(osv.osv): """ Calculates the working Schedule from supplied from date to till hours will be satisfied based or resource calendar id. If resource is also - given then it will consider the resource leave also and than will + given then it will consider the resource leave also and than will calculates resource working schedule - + @param dt_from: datetime object, start of working scheduled @param hours: float, total number working hours needed scheduled from start date @@ -187,29 +187,29 @@ class resource_calendar(osv.osv): def interval_get(self, cr, uid, id, dt_from, hours, resource=False, byday=True): """Calculates Resource Working Internal Timing Based on Resource Calendar. - + @param dt_from: start resource schedule calculation. @param hours : total number of working hours to be scheduled. - @param resource: optional resource id, If supplied it will take care of + @param resource: optional resource id, If supplied it will take care of resource leave while scheduling. @param byday: boolean flag bit enforce day wise scheduling - + @return : list of scheduled working timing based on resource calendar. """ res = self.interval_get_multi(cr, uid, [(dt_from.strftime('%Y-%m-%d %H:%M:%S'), hours, id)], resource, byday)[(dt_from.strftime('%Y-%m-%d %H:%M:%S'), hours, id)] return res def interval_hours_get(self, cr, uid, id, dt_from, dt_to, resource=False): - """ Calculates the Total Working hours based on given start_date to - end_date, If resource id is supplied that it will consider the source + """ Calculates the Total Working hours based on given start_date to + end_date, If resource id is supplied that it will consider the source leaves also in calculating the hours. - + @param dt_from : date start to calculate hours @param dt_end : date end to calculate hours @param resource: optional resource id, If given resource leave will be - considered. - - @return : Total number of working hours based dt_from and dt_end and + considered. + + @return : Total number of working hours based dt_from and dt_end and resource if supplied. """ if not id: @@ -257,7 +257,7 @@ resource_calendar() class resource_calendar_attendance(osv.osv): _name = "resource.calendar.attendance" _description = "Work Detail" - + _columns = { 'name' : fields.char("Name", size=64, required=True), 'dayofweek': fields.selection([('0','Monday'),('1','Tuesday'),('2','Wednesday'),('3','Thursday'),('4','Friday'),('5','Saturday'),('6','Sunday')], 'Day of Week', required=True, select=True), @@ -266,9 +266,9 @@ class resource_calendar_attendance(osv.osv): 'hour_to' : fields.float("Work to", required=True), 'calendar_id' : fields.many2one("resource.calendar", "Resource's Calendar", required=True), } - + _order = 'dayofweek, hour_from' - + _defaults = { 'dayofweek' : '0' } @@ -302,12 +302,12 @@ class resource_resource(osv.osv): 'company_id': lambda self, cr, uid, context: self.pool.get('res.company')._company_default_get(cr, uid, 'resource.resource', context=context) } - + def copy(self, cr, uid, id, default=None, context=None): if default is None: default = {} if not default.get('name', False): - default['name'] = self.browse(cr, uid, id, context=context).name + _(' (copy)') + default.update(name=_('%s (copy)') % (self.browse(cr, uid, id, context=context).name)) return super(resource_resource, self).copy(cr, uid, id, default, context) def generate_resources(self, cr, uid, user_ids, calendar_id, context=None): @@ -368,7 +368,7 @@ class resource_resource(osv.osv): """ if not calendar_id: # Calendar is not specified: working days: 24/7 - return [('fri', '8:0-12:0','13:0-17:0'), ('thu', '8:0-12:0','13:0-17:0'), ('wed', '8:0-12:0','13:0-17:0'), + return [('fri', '8:0-12:0','13:0-17:0'), ('thu', '8:0-12:0','13:0-17:0'), ('wed', '8:0-12:0','13:0-17:0'), ('mon', '8:0-12:0','13:0-17:0'), ('tue', '8:0-12:0','13:0-17:0')] resource_attendance_pool = self.pool.get('resource.calendar.attendance') time_range = "8:00-8:00" diff --git a/addons/survey/survey.py b/addons/survey/survey.py index 0239f8bf32f..2426ae5eeed 100644 --- a/addons/survey/survey.py +++ b/addons/survey/survey.py @@ -94,7 +94,7 @@ class survey(osv.osv): def copy(self, cr, uid, ids, default=None, context=None): vals = {} current_rec = self.read(cr, uid, ids, context=context) - title = current_rec.get('title') + ' (Copy)' + title = _("%s (copy)") % (current_rec.get('title')) vals.update({'title':title}) vals.update({'history':[],'tot_start_survey':0,'tot_comp_survey':0}) return super(survey, self).copy(cr, uid, ids, vals, context=context) @@ -143,7 +143,7 @@ class survey(osv.osv): 'nodestroy':True, } return report - + def fill_survey(self, cr, uid, ids, context=None): sur_obj = self.read(cr, uid, ids,['title'], context=context) for sur in sur_obj: @@ -232,7 +232,7 @@ class survey_page(osv.osv): def copy(self, cr, uid, ids, default=None, context=None): vals = {} current_rec = self.read(cr, uid, ids, context=context) - title = current_rec.get('title') + ' (Copy)' + title = _("%s (copy)") % (current_rec.get('title')) vals.update({'title':title}) return super(survey_page, self).copy(cr, uid, ids, vals, context=context) From 26780da2ef5b9b6f5a636ded6f8b21517f6b2198 Mon Sep 17 00:00:00 2001 From: Antony Lesuisse Date: Tue, 25 Sep 2012 02:09:46 +0200 Subject: [PATCH 27/29] [FIX] fcntl not available on windows bzr revid: al@openerp.com-20120925000946-7qikupf5rt2hiwx4 --- openerp/service/workers.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openerp/service/workers.py b/openerp/service/workers.py index 824bc07bb53..eae1b2839a8 100644 --- a/openerp/service/workers.py +++ b/openerp/service/workers.py @@ -3,7 +3,10 @@ # TODO rename class: Multicorn -> Arbiter ? #----------------------------------------------------------- import errno -import fcntl +try: + import fcntl +except ImportError: + fcntl = None import psutil import random import resource From 3cc8c24ef41027d52f2293f98877c2e1834d3468 Mon Sep 17 00:00:00 2001 From: Launchpad Translations on behalf of openerp <> Date: Tue, 25 Sep 2012 04:44:36 +0000 Subject: [PATCH 28/29] Launchpad automatic translations update. bzr revid: launchpad_translations_on_behalf_of_openerp-20120925044436-la8k19ynfgu07eeu --- openerp/addons/base/i18n/ru.po | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openerp/addons/base/i18n/ru.po b/openerp/addons/base/i18n/ru.po index fd9f91aa09d..82e7869a7ce 100644 --- a/openerp/addons/base/i18n/ru.po +++ b/openerp/addons/base/i18n/ru.po @@ -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-02-08 00:44+0000\n" -"PO-Revision-Date: 2012-09-10 14:37+0000\n" +"PO-Revision-Date: 2012-09-24 14:20+0000\n" "Last-Translator: Chertykov Denis \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-09-20 04:40+0000\n" -"X-Generator: Launchpad (build 15985)\n" +"X-Launchpad-Export-Date: 2012-09-25 04:44+0000\n" +"X-Generator: Launchpad (build 16019)\n" #. module: base #: model:res.country,name:base.sh @@ -18206,7 +18206,7 @@ msgstr "Корпорация" #. module: base #: model:ir.module.module,shortdesc:base.module_purchase_requisition msgid "Purchase Requisitions" -msgstr "Требования закупки" +msgstr "Запросы на закупку" #. module: base #: selection:ir.cron,interval_type:0 From 773ae0715bc0bd00ed9006abe234784c8b07265f Mon Sep 17 00:00:00 2001 From: Launchpad Translations on behalf of openerp <> Date: Tue, 25 Sep 2012 04:44:51 +0000 Subject: [PATCH 29/29] Launchpad automatic translations update. bzr revid: launchpad_translations_on_behalf_of_openerp-20120925044451-qfp4bi4a18pm8iy1 --- addons/stock/i18n/nl.po | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/addons/stock/i18n/nl.po b/addons/stock/i18n/nl.po index 04c3875df25..e74b8c680eb 100644 --- a/addons/stock/i18n/nl.po +++ b/addons/stock/i18n/nl.po @@ -7,14 +7,14 @@ msgstr "" "Project-Id-Version: OpenERP Server 6.0dev\n" "Report-Msgid-Bugs-To: support@openerp.com\n" "POT-Creation-Date: 2012-02-08 01:37+0100\n" -"PO-Revision-Date: 2012-05-10 17:48+0000\n" -"Last-Translator: Raphael Collet (OpenERP) \n" +"PO-Revision-Date: 2012-09-24 13:55+0000\n" +"Last-Translator: Erwin \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-08-28 05:56+0000\n" -"X-Generator: Launchpad (build 15864)\n" +"X-Launchpad-Export-Date: 2012-09-25 04:44+0000\n" +"X-Generator: Launchpad (build 16019)\n" #. module: stock #: field:product.product,track_outgoing:0 @@ -1179,7 +1179,7 @@ msgstr "Auteur" #: code:addons/stock/stock.py:1332 #, python-format msgid "Delivery Order" -msgstr "Leveringsbon" +msgstr "Uitgaande levering" #. module: stock #: selection:stock.location,chained_auto_packing:0 @@ -1791,7 +1791,7 @@ msgstr "Voorraadwaarde rekening (inkomend)" #. module: stock #: model:stock.location,name:stock.stock_location_14 msgid "Shelf 2" -msgstr "Schap 2" +msgstr "Plank 2" #. module: stock #: field:stock.report.tracklots,tracking_id:0