Merge with neox
|
@ -26,3 +26,4 @@ package-lock*
|
|||
|
||||
/__pycache__
|
||||
/app/__pycache__
|
||||
/app/commons/__pycache__
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
# this repository contains the full copyright notices and license terms.
|
||||
__version__ = "5.0.0"
|
||||
__version__ = "5.0.40"
|
||||
|
|
|
@ -3,7 +3,7 @@ from PyQt5.QtCore import Qt
|
|||
from PyQt5.QtWidgets import QWidget, QGridLayout, QHBoxLayout, QStackedWidget
|
||||
|
||||
from neox.commons.custom_button import CustomButton
|
||||
from .common import get_icon
|
||||
from .tools import get_icon
|
||||
|
||||
DIR_SHARE = os.path.abspath(os.path.normpath(os.path.join(__file__,
|
||||
'..', '..', 'share')))
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
|
|
@ -0,0 +1,45 @@
|
|||
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
from .common import slugify, file_open
|
||||
from .rpc import RPCProgress
|
||||
|
||||
|
||||
class Action(object):
|
||||
|
||||
@staticmethod
|
||||
def exec_report(conn, name, data, direct_print=False, context=None):
|
||||
if context is None:
|
||||
context = {}
|
||||
|
||||
data = data.copy()
|
||||
ctx = {}
|
||||
ctx.update(context)
|
||||
ctx['direct_print'] = direct_print
|
||||
args = ('report', name, 'execute', data.get('ids', []), data, ctx)
|
||||
try:
|
||||
rpc_progress = RPCProgress(conn, 'execute', args)
|
||||
res = rpc_progress.run()
|
||||
except:
|
||||
return False
|
||||
if not res:
|
||||
return False
|
||||
|
||||
(type, content, print_p, name) = res
|
||||
|
||||
dtemp = tempfile.mkdtemp(prefix='tryton_')
|
||||
|
||||
fp_name = os.path.join(dtemp,
|
||||
slugify(name) + os.extsep + slugify(type))
|
||||
|
||||
print(dtemp)
|
||||
if os.name == 'nt':
|
||||
operation = 'open'
|
||||
os.startfile(fp_name, operation)
|
||||
else:
|
||||
with open(fp_name, 'wb') as file_d:
|
||||
file_d.write(content.data)
|
||||
|
||||
file_open(fp_name, type, direct_print=direct_print)
|
||||
return True
|
|
@ -0,0 +1,22 @@
|
|||
|
||||
from PyQt5.QtWidgets import QPushButton
|
||||
|
||||
__all__ = ['ActionButton']
|
||||
|
||||
|
||||
class ActionButton(QPushButton):
|
||||
|
||||
def __init__(self, action, method):
|
||||
|
||||
super(ActionButton, self).__init__('')
|
||||
if action == 'ok':
|
||||
name = self.tr("&ACCEPT")
|
||||
else:
|
||||
name = self.tr("&CANCEL")
|
||||
|
||||
self.setText(name)
|
||||
self.clicked.connect(method)
|
||||
if action == 'ok':
|
||||
self.setAutoDefault(True)
|
||||
self.setDefault(True)
|
||||
self.setObjectName('button_' + action)
|
|
@ -0,0 +1,20 @@
|
|||
from PyQt5 import QtWidgets, QtCore
|
||||
|
||||
__all__ = ['DigitalClock']
|
||||
|
||||
|
||||
class DigitalClock(QtWidgets.QLCDNumber):
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(DigitalClock, self).__init__(parent)
|
||||
self.setSegmentStyle(QtWidgets.QLCDNumber.Filled)
|
||||
timer = QtCore.QTimer(self)
|
||||
timer.timeout.connect(self.showTime)
|
||||
timer.start(1000)
|
||||
|
||||
def showTime(self):
|
||||
time = QtCore.QTime.currentTime()
|
||||
text = time.toString('hh:mm')
|
||||
if (time.second() % 2) == 0:
|
||||
text = text[:2] + ' ' + text[3:]
|
||||
self.display(text)
|
|
@ -0,0 +1,58 @@
|
|||
|
||||
from PyQt5.QtGui import QColor
|
||||
|
||||
color_white = QColor(255, 255, 255)
|
||||
color_white_hide = QColor(255, 255, 255, 0)
|
||||
|
||||
color_gray_soft = QColor(242, 242, 242, 255)
|
||||
color_gray_light = QColor(102, 102, 102, 255)
|
||||
color_gray_dark = QColor(170, 170, 170, 255)
|
||||
color_gray_hover = QColor(225, 225, 225, 250)
|
||||
|
||||
color_blue_soft = QColor(15, 160, 210, 255)
|
||||
color_blue_light = QColor(220, 240, 250, 255)
|
||||
color_blue_press = QColor(190, 230, 245, 255)
|
||||
color_blue_hover = QColor(80, 170, 210, 120)
|
||||
|
||||
color_green_soft = QColor(140, 225, 210, 235)
|
||||
color_green_light = QColor(170, 215, 110, 250)
|
||||
color_green_dark = QColor(105, 180, 55)
|
||||
color_green_hover = QColor(180, 215, 100)
|
||||
|
||||
color_yellow_font = QColor(185, 110, 5, 255)
|
||||
color_yellow_bg = QColor(251, 215, 110, 230)
|
||||
color_yellow_hover = QColor(251, 215, 50, 255)
|
||||
color_yellow_press = QColor(251, 215, 70, 110)
|
||||
|
||||
color_red = QColor(20, 150, 230)
|
||||
|
||||
themes = {
|
||||
'default': {
|
||||
'font_color': color_gray_light,
|
||||
'bg_color': color_blue_light,
|
||||
'hover_color': color_blue_hover,
|
||||
'press_color': color_gray_hover,
|
||||
'pen': color_white_hide,
|
||||
},
|
||||
'blue': {
|
||||
'font_color': color_gray_dark,
|
||||
'bg_color': color_white,
|
||||
'hover_color': color_blue_light,
|
||||
'press_color': color_blue_press,
|
||||
'pen': color_white_hide,
|
||||
},
|
||||
'green': {
|
||||
'font_color': color_white,
|
||||
'bg_color': color_green_soft,
|
||||
'hover_color': color_green_light,
|
||||
'press_color': color_green_dark,
|
||||
'pen': color_white_hide,
|
||||
},
|
||||
'yellow': {
|
||||
'font_color': color_yellow_font,
|
||||
'bg_color': color_yellow_bg,
|
||||
'hover_color': color_yellow_hover,
|
||||
'press_color': color_yellow_press,
|
||||
'pen': color_white_hide,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
# -*- coding: UTF-8 -*-
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
from re import compile
|
||||
import unicodedata
|
||||
from .rpc import server_version
|
||||
from app import __version__
|
||||
|
||||
_slugify_strip_re = compile(r'[^\w\s-]')
|
||||
_slugify_hyphenate_re = compile(r'[-\s]+')
|
||||
|
||||
|
||||
def test_server_version(host, port):
|
||||
version = server_version(host, port)
|
||||
if not version:
|
||||
return False
|
||||
return version.split('.')[:2] == __version__.split('.')[:2]
|
||||
|
||||
|
||||
def slugify(value):
|
||||
if not isinstance(value, str):
|
||||
value = value.decode('utf-8')
|
||||
value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore')
|
||||
value = value.decode('utf-8')
|
||||
value = _slugify_strip_re.sub('', value).strip().lower()
|
||||
return _slugify_hyphenate_re.sub('-', value)
|
||||
|
||||
|
||||
def file_open(filename, type, direct_print=False):
|
||||
def save():
|
||||
pass
|
||||
|
||||
name = filename.split('.')
|
||||
|
||||
if 'odt' in name:
|
||||
direct_print = False
|
||||
|
||||
if os.name == 'nt':
|
||||
operation = 'open'
|
||||
if direct_print:
|
||||
operation = 'print'
|
||||
try:
|
||||
os.startfile(os.path.normpath(filename), operation)
|
||||
except WindowsError:
|
||||
save()
|
||||
elif sys.platform == 'darwin':
|
||||
try:
|
||||
subprocess.Popen(['/usr/bin/open', filename])
|
||||
except OSError:
|
||||
save()
|
||||
else:
|
||||
if direct_print:
|
||||
try:
|
||||
subprocess.Popen(['lp', filename])
|
||||
except:
|
||||
direct_print = False
|
||||
|
||||
if not direct_print:
|
||||
try:
|
||||
subprocess.Popen(['xdg-open', filename])
|
||||
except OSError:
|
||||
pass
|
|
@ -0,0 +1,40 @@
|
|||
import os
|
||||
from PyQt5.QtCore import QSettings
|
||||
|
||||
|
||||
class Params(object):
|
||||
"""
|
||||
Params Configuration
|
||||
This class load all settings from .ini file
|
||||
"""
|
||||
|
||||
def __init__(self, file_):
|
||||
self.file = file_
|
||||
|
||||
dirx = os.path.abspath(os.path.join(__file__, '..', '..'))
|
||||
|
||||
if os.name == 'posix':
|
||||
homex = 'HOME'
|
||||
dirconfig = '.tryton'
|
||||
elif os.name == 'nt':
|
||||
homex = 'USERPROFILE'
|
||||
dirconfig = 'AppData/Local/tryton'
|
||||
|
||||
HOME_DIR = os.getenv(homex)
|
||||
default_dir = os.path.join(HOME_DIR, dirconfig)
|
||||
|
||||
if os.path.exists(default_dir):
|
||||
config_file = os.path.join(default_dir, self.file)
|
||||
else:
|
||||
config_file = os.path.join(dirx, self.file)
|
||||
|
||||
if not os.path.exists(config_file):
|
||||
config_file = self.file
|
||||
|
||||
settings = QSettings(config_file, QSettings.IniFormat)
|
||||
|
||||
self.params = {}
|
||||
for key in settings.allKeys():
|
||||
if key[0] == '#':
|
||||
continue
|
||||
self.params[key] = settings.value(key, None)
|
|
@ -0,0 +1,461 @@
|
|||
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
# this repository contains the full copyright notices and license terms.
|
||||
"""
|
||||
Configuration functions for the interface to Tryton.
|
||||
"""
|
||||
from __future__ import with_statement
|
||||
__all__ = ['set_trytond', 'set_xmlrpc', 'get_config', 'set_jsonrpc']
|
||||
|
||||
import xmlrpc.client as xmlrpclib
|
||||
import threading
|
||||
from decimal import Decimal
|
||||
import datetime
|
||||
import os
|
||||
import urllib.parse as urlparse
|
||||
|
||||
from functools import partial
|
||||
from .jsonrpc import ServerProxy
|
||||
|
||||
xmlrpclib._stringify = lambda s: s
|
||||
|
||||
|
||||
def dump_decimal(self, value, write):
|
||||
value = {'__class__': 'Decimal',
|
||||
'decimal': str(value),
|
||||
}
|
||||
self.dump_struct(value, write)
|
||||
|
||||
|
||||
def dump_bytes(self, value, write):
|
||||
self.write = write
|
||||
value = xmlrpclib.Binary(value)
|
||||
value.encode(self)
|
||||
del self.write
|
||||
|
||||
|
||||
def dump_date(self, value, write):
|
||||
value = {'__class__': 'date',
|
||||
'year': value.year,
|
||||
'month': value.month,
|
||||
'day': value.day,
|
||||
}
|
||||
self.dump_struct(value, write)
|
||||
|
||||
|
||||
def dump_time(self, value, write):
|
||||
value = {'__class__': 'time',
|
||||
'hour': value.hour,
|
||||
'minute': value.minute,
|
||||
'second': value.second,
|
||||
'microsecond': value.microsecond,
|
||||
}
|
||||
self.dump_struct(value, write)
|
||||
|
||||
|
||||
def dump_timedelta(self, value, write):
|
||||
value = {'__class__': 'timedelta',
|
||||
'seconds': value.total_seconds(),
|
||||
}
|
||||
self.dump_struct(value, write)
|
||||
|
||||
xmlrpclib.Marshaller.dispatch[Decimal] = dump_decimal
|
||||
xmlrpclib.Marshaller.dispatch[datetime.date] = dump_date
|
||||
xmlrpclib.Marshaller.dispatch[datetime.time] = dump_time
|
||||
xmlrpclib.Marshaller.dispatch[datetime.timedelta] = dump_timedelta
|
||||
if bytes != str:
|
||||
xmlrpclib.Marshaller.dispatch[bytes] = dump_bytes
|
||||
xmlrpclib.Marshaller.dispatch[bytearray] = dump_bytes
|
||||
|
||||
|
||||
class XMLRPCDecoder(object):
|
||||
|
||||
decoders = {}
|
||||
|
||||
@classmethod
|
||||
def register(cls, klass, decoder):
|
||||
assert klass not in cls.decoders
|
||||
cls.decoders[klass] = decoder
|
||||
|
||||
def __call__(self, dct):
|
||||
if dct.get('__class__') in self.decoders:
|
||||
return self.decoders[dct['__class__']](dct)
|
||||
return dct
|
||||
|
||||
XMLRPCDecoder.register('date',
|
||||
lambda dct: datetime.date(dct['year'], dct['month'], dct['day']))
|
||||
XMLRPCDecoder.register('time',
|
||||
lambda dct: datetime.time(dct['hour'], dct['minute'], dct['second'],
|
||||
dct['microsecond']))
|
||||
XMLRPCDecoder.register('timedelta',
|
||||
lambda dct: datetime.timedelta(seconds=dct['seconds']))
|
||||
XMLRPCDecoder.register('Decimal', lambda dct: Decimal(dct['decimal']))
|
||||
|
||||
|
||||
def end_struct(self, data):
|
||||
mark = self._marks.pop()
|
||||
# map structs to Python dictionaries
|
||||
dct = {}
|
||||
items = self._stack[mark:]
|
||||
for i in range(0, len(items), 2):
|
||||
dct[xmlrpclib._stringify(items[i])] = items[i + 1]
|
||||
dct[items[i]] = items[i + 1]
|
||||
dct = XMLRPCDecoder()(dct)
|
||||
self._stack[mark:] = [dct]
|
||||
self._value = 0
|
||||
|
||||
xmlrpclib.Unmarshaller.dispatch['struct'] = end_struct
|
||||
|
||||
_CONFIG = threading.local()
|
||||
_CONFIG.current = None
|
||||
|
||||
|
||||
class ContextManager(object):
|
||||
'Context Manager for the tryton context'
|
||||
|
||||
def __init__(self, config):
|
||||
self.config = config
|
||||
self.context = config.context
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
self.config._context = self.context
|
||||
|
||||
|
||||
class Config(object):
|
||||
'Config interface'
|
||||
|
||||
def __init__(self):
|
||||
super(Config, self).__init__()
|
||||
self._context = {}
|
||||
|
||||
@property
|
||||
def context(self):
|
||||
return self._context.copy()
|
||||
|
||||
def set_context(self, context=None, **kwargs):
|
||||
ctx_manager = ContextManager(self)
|
||||
|
||||
if context is None:
|
||||
context = {}
|
||||
self._context = self.context
|
||||
self._context.update(context)
|
||||
self._context.update(kwargs)
|
||||
return ctx_manager
|
||||
|
||||
def get_proxy(self, name):
|
||||
raise NotImplementedError
|
||||
|
||||
def get_proxy_methods(self, name):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class _TrytondMethod(object):
|
||||
|
||||
def __init__(self, name, model, config):
|
||||
super(_TrytondMethod, self).__init__()
|
||||
self._name = name
|
||||
self._object = model
|
||||
self._config = config
|
||||
|
||||
def __call__(self, *args):
|
||||
from trytond.cache import Cache
|
||||
from trytond.transaction import Transaction
|
||||
from trytond.rpc import RPC
|
||||
|
||||
if self._name in self._object.__rpc__:
|
||||
rpc = self._object.__rpc__[self._name]
|
||||
elif self._name in getattr(self._object, '_buttons', {}):
|
||||
rpc = RPC(readonly=False, instantiate=0)
|
||||
else:
|
||||
raise TypeError('%s is not callable' % self._name)
|
||||
|
||||
with Transaction().start(self._config.database_name,
|
||||
self._config.user, readonly=rpc.readonly) as transaction:
|
||||
Cache.clean(self._config.database_name)
|
||||
args, kwargs, transaction.context, transaction.timestamp = \
|
||||
rpc.convert(self._object, *args)
|
||||
meth = getattr(self._object, self._name)
|
||||
if not hasattr(meth, 'im_self') or meth.im_self:
|
||||
result = rpc.result(meth(*args, **kwargs))
|
||||
else:
|
||||
assert rpc.instantiate == 0
|
||||
inst = args.pop(0)
|
||||
if hasattr(inst, self._name):
|
||||
result = rpc.result(meth(inst, *args, **kwargs))
|
||||
else:
|
||||
result = [rpc.result(meth(i, *args, **kwargs))
|
||||
for i in inst]
|
||||
if not rpc.readonly:
|
||||
transaction.commit()
|
||||
Cache.resets(self._config.database_name)
|
||||
return result
|
||||
|
||||
|
||||
class TrytondProxy(object):
|
||||
'Proxy for function call for trytond'
|
||||
|
||||
def __init__(self, name, config, type='model'):
|
||||
super(TrytondProxy, self).__init__()
|
||||
self._config = config
|
||||
self._object = config.pool.get(name, type=type)
|
||||
__init__.__doc__ = object.__init__.__doc__
|
||||
|
||||
def __getattr__(self, name):
|
||||
'Return attribute value'
|
||||
return _TrytondMethod(name, self._object, self._config)
|
||||
|
||||
|
||||
class TrytondConfig(Config):
|
||||
'Configuration for trytond'
|
||||
|
||||
def __init__(self, database=None, user='admin', config_file=None):
|
||||
super(TrytondConfig, self).__init__()
|
||||
if not database:
|
||||
database = os.environ.get('TRYTOND_DATABASE_URI')
|
||||
else:
|
||||
os.environ['TRYTOND_DATABASE_URI'] = database
|
||||
if not config_file:
|
||||
config_file = os.environ.get('TRYTOND_CONFIG')
|
||||
from trytond.config import config
|
||||
config.update_etc(config_file)
|
||||
from trytond.pool import Pool
|
||||
from trytond.cache import Cache
|
||||
from trytond.transaction import Transaction
|
||||
self.database = database
|
||||
database_name = None
|
||||
if database:
|
||||
uri = urlparse.urlparse(database)
|
||||
database_name = uri.path.strip('/')
|
||||
if not database_name:
|
||||
database_name = os.environ['DB_NAME']
|
||||
self.database_name = database_name
|
||||
self._user = user
|
||||
self.config_file = config_file
|
||||
|
||||
Pool.start()
|
||||
self.pool = Pool(database_name)
|
||||
self.pool.init()
|
||||
|
||||
with Transaction().start(self.database_name, 0) as transaction:
|
||||
Cache.clean(database_name)
|
||||
User = self.pool.get('res.user')
|
||||
transaction.context = self.context
|
||||
self.user = User.search([
|
||||
('login', '=', user),
|
||||
], limit=1)[0].id
|
||||
with transaction.set_user(self.user):
|
||||
self._context = User.get_preferences(context_only=True)
|
||||
Cache.resets(database_name)
|
||||
__init__.__doc__ = object.__init__.__doc__
|
||||
|
||||
def __repr__(self):
|
||||
return ("proteus.config.TrytondConfig"
|
||||
"(%s, %s, config_file=%s)"
|
||||
% (repr(self.database), repr(self._user), repr(self.config_file)))
|
||||
__repr__.__doc__ = object.__repr__.__doc__
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, TrytondConfig):
|
||||
raise NotImplementedError
|
||||
return (self.database_name == other.database_name
|
||||
and self._user == other._user
|
||||
and self.database == other.database
|
||||
and self.config_file == other.config_file)
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.database_name, self._user,
|
||||
self.database, self.config_file))
|
||||
|
||||
def get_proxy(self, name, type='model'):
|
||||
'Return Proxy class'
|
||||
return TrytondProxy(name, self, type=type)
|
||||
|
||||
def get_proxy_methods(self, name, type='model'):
|
||||
'Return list of methods'
|
||||
proxy = self.get_proxy(name, type=type)
|
||||
methods = [x for x in proxy._object.__rpc__]
|
||||
if hasattr(proxy._object, '_buttons'):
|
||||
methods += [x for x in proxy._object._buttons]
|
||||
return methods
|
||||
|
||||
|
||||
def set_trytond(database=None, user='admin',
|
||||
config_file=None):
|
||||
'Set trytond package as backend'
|
||||
_CONFIG.current = TrytondConfig(database, user, config_file=config_file)
|
||||
return _CONFIG.current
|
||||
|
||||
|
||||
class XmlrpcProxy(object):
|
||||
'Proxy for function call for XML-RPC'
|
||||
|
||||
def __init__(self, name, config, type='model'):
|
||||
super(XmlrpcProxy, self).__init__()
|
||||
self._config = config
|
||||
self._object = getattr(config.server, '%s.%s' % (type, name))
|
||||
__init__.__doc__ = object.__init__.__doc__
|
||||
|
||||
def __getattr__(self, name):
|
||||
'Return attribute value'
|
||||
return getattr(self._object, name)
|
||||
|
||||
|
||||
class XmlrpcConfig(Config):
|
||||
'Configuration for XML-RPC'
|
||||
|
||||
def __init__(self, url, **kwargs):
|
||||
super(XmlrpcConfig, self).__init__()
|
||||
self.url = url
|
||||
self.server = xmlrpclib.ServerProxy(
|
||||
url, allow_none=1, use_datetime=1, **kwargs)
|
||||
# TODO add user
|
||||
self.user = None
|
||||
self._context = self.server.model.res.user.get_preferences(True, {})
|
||||
__init__.__doc__ = object.__init__.__doc__
|
||||
|
||||
def __repr__(self):
|
||||
return "proteus.config.XmlrpcConfig(%s)" % repr(self.url)
|
||||
__repr__.__doc__ = object.__repr__.__doc__
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, XmlrpcConfig):
|
||||
raise NotImplementedError
|
||||
return self.url == other.url
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.url)
|
||||
|
||||
def get_proxy(self, name, type='model'):
|
||||
'Return Proxy class'
|
||||
return XmlrpcProxy(name, self, type=type)
|
||||
|
||||
def get_proxy_methods(self, name, type='model'):
|
||||
'Return list of methods'
|
||||
object_ = '%s.%s' % (type, name)
|
||||
return [x[len(object_) + 1:]
|
||||
for x in self.server.system.listMethods()
|
||||
if x.startswith(object_)
|
||||
and '.' not in x[len(object_) + 1:]]
|
||||
|
||||
|
||||
def set_xmlrpc(url, **kwargs):
|
||||
'''
|
||||
Set XML-RPC as backend.
|
||||
It pass the keyword arguments received to xmlrpclib.ServerProxy()
|
||||
'''
|
||||
_CONFIG.current = XmlrpcConfig(url, **kwargs)
|
||||
return _CONFIG.current
|
||||
|
||||
|
||||
class JsonrpcProxy(object):
|
||||
'Proxy for function call for JSON-RPC'
|
||||
|
||||
def __init__(self, name, config, type='model'):
|
||||
super(JsonrpcProxy, self).__init__()
|
||||
self._config = config
|
||||
self._object = getattr(config.server, '%s.%s' % (type, name))
|
||||
__init__.__doc__ = object.__init__.__doc__
|
||||
|
||||
def __getattr__(self, name):
|
||||
'Return attribute value'
|
||||
return partial(
|
||||
getattr(self._object, name),
|
||||
self._config.user_id, self._config.session
|
||||
)
|
||||
|
||||
def ping(self):
|
||||
return True
|
||||
|
||||
|
||||
class JsonrpcConfig(Config):
|
||||
'Configuration for JSON-RPC'
|
||||
|
||||
def __init__(self, url, **kwargs):
|
||||
super(JsonrpcConfig, self).__init__()
|
||||
self.full_url = url
|
||||
self.url = urlparse.urlparse(url)
|
||||
self.server = ServerProxy(
|
||||
host=self.url.hostname,
|
||||
port=self.url.port,
|
||||
database=self.url.path[1:]
|
||||
)
|
||||
self.user = None
|
||||
|
||||
result = self.server.common.db.login(
|
||||
self.url.username, self.url.password
|
||||
)
|
||||
self.user_id = result[0]
|
||||
self.user = None
|
||||
self.session = result[1]
|
||||
# FIXME
|
||||
self._context = self.server.model.res.user.get_preferences(
|
||||
self.user_id, self.session, True, {})
|
||||
|
||||
def __repr__(self):
|
||||
return "proteus.config.JsonrpcConfig('%s')" % self.full_url
|
||||
__repr__.__doc__ = object.__repr__.__doc__
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, JsonrpcConfig):
|
||||
raise NotImplementedError
|
||||
return self.url == other.url
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.url)
|
||||
|
||||
def get_proxy(self, name, type='model'):
|
||||
'Return Proxy class'
|
||||
return JsonrpcProxy(name, self, type=type)
|
||||
|
||||
def get_proxy_methods(self, name, type='model'):
|
||||
'Return list of methods'
|
||||
object_ = '%s.%s' % (type, name)
|
||||
return [x[len(object_) + 1:]
|
||||
for x in self.server.system.listMethods(None, None)
|
||||
if x.startswith(object_)
|
||||
and '.' not in x[len(object_) + 1:]]
|
||||
|
||||
|
||||
def set_jsonrpc(url, **kwargs):
|
||||
'Set JSON-RPC as backend'
|
||||
_CONFIG.current = JsonrpcConfig(url, **kwargs)
|
||||
return _CONFIG.current
|
||||
|
||||
|
||||
def get_config():
|
||||
return _CONFIG.current
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import jsonrpc
|
||||
# For testing purposes
|
||||
res = None
|
||||
TEST = 'xmlrpc'
|
||||
user = 'admin'
|
||||
password = 'aa'
|
||||
host = '127.0.0.1'
|
||||
port = '8000'
|
||||
database = 'DEMO40'
|
||||
url = 'http://%s:%s@%s:%s/%s/' % (
|
||||
user,
|
||||
password,
|
||||
host,
|
||||
port,
|
||||
database,
|
||||
)
|
||||
if TEST == 'xmlrpc':
|
||||
conn = set_xmlrpc(url)
|
||||
else: #TEST = 'jsonrpc'
|
||||
conn = set_jsonrpc(url[:-1])
|
||||
User = conn.get_proxy('res.user')
|
||||
print("Super:", User)
|
||||
user = User.search_read([], 0, None, None, ['name'], {})
|
||||
#Party = conn.get_proxy('party.party')
|
||||
#res = Party.search_read([], 0, None, None, ['name'], conn.context)
|
||||
print('- - - - - - - - - - - - - - - - - - - - - - - - - -')
|
||||
print('Context ->', conn._context)
|
||||
print('- - - - - - - - - - - - - - - - - - - - - - - - - -')
|
||||
#print(res)
|
|
@ -0,0 +1,98 @@
|
|||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from functools import partial
|
||||
|
||||
from PyQt5.QtCore import Qt, QSize
|
||||
from PyQt5.QtWidgets import QLabel, QPushButton, QVBoxLayout, QSizePolicy
|
||||
|
||||
root_dir = Path(__file__).parent.parent
|
||||
root_dir = str(root_dir)
|
||||
|
||||
css_screens = {
|
||||
'small': 'flat_button_small.css',
|
||||
'medium': 'flat_button_medium.css',
|
||||
'large': 'flat_button_large.css'
|
||||
}
|
||||
|
||||
__all__ = ['CustomButton']
|
||||
|
||||
|
||||
class CustomButton(QPushButton):
|
||||
|
||||
def __init__(self, parent, id, icon=None, title=None, desc=None, method=None,
|
||||
target=None, size='small', name_style='category_button'):
|
||||
"""
|
||||
Create custom, responsive and nice button flat style,
|
||||
with two subsections
|
||||
_ _ _ _ _
|
||||
| ICON | -> Title / Icon (Up section)
|
||||
| DESC | -> Descriptor section (Optional - bottom section)
|
||||
|_ _ _ _ _|
|
||||
|
||||
:id :: Id of button,
|
||||
:icon:: A QSvgRenderer object,
|
||||
:title :: Name of button,
|
||||
:descriptor:: Text name or descriptor of button,
|
||||
:method:: Method for connect to clicked signal if it missing '*_pressed'
|
||||
will be used instead.
|
||||
:target:: ?
|
||||
:name_style:: define which type of button style must be rendered.
|
||||
"""
|
||||
super(CustomButton, self).__init__()
|
||||
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||
qsize = QSize(50, 50)
|
||||
if name_style == 'toolbar_button':
|
||||
qsize = QSize(35, 35)
|
||||
|
||||
self.id = id
|
||||
styles = []
|
||||
|
||||
css_file = os.path.join(root_dir, 'css', css_screens[size])
|
||||
with open(css_file, 'r') as infile:
|
||||
styles.append(infile.read())
|
||||
|
||||
self.setStyleSheet(''.join(styles))
|
||||
self.setObjectName(name_style)
|
||||
|
||||
rows = []
|
||||
if icon:
|
||||
if not desc:
|
||||
self.setIcon(icon)
|
||||
self.setIconSize(qsize)
|
||||
else:
|
||||
pixmap = icon.pixmap(qsize)
|
||||
label_icon = QLabel()
|
||||
label_icon.setObjectName('label_icon')
|
||||
label_icon.setPixmap(pixmap)
|
||||
label_icon.setAlignment(Qt.AlignCenter | Qt.AlignCenter)
|
||||
rows.append(label_icon)
|
||||
|
||||
if title:
|
||||
if len(desc) > 29:
|
||||
desc = desc[0:29]
|
||||
label_title = QLabel(title)
|
||||
label_title.setWordWrap(True)
|
||||
label_title.setAlignment(Qt.AlignCenter | Qt.AlignCenter)
|
||||
label_title.setObjectName('label_title')
|
||||
rows.append(label_title)
|
||||
|
||||
if desc:
|
||||
if len(desc) > 29:
|
||||
desc = desc[0:29]
|
||||
|
||||
label_desc = QLabel(desc, self)
|
||||
label_desc.setAlignment(Qt.AlignCenter | Qt.AlignCenter)
|
||||
label_desc.setObjectName('label_desc')
|
||||
rows.append(label_desc)
|
||||
|
||||
if len(rows) > 1:
|
||||
vbox = QVBoxLayout()
|
||||
for w in rows:
|
||||
vbox.addWidget(w)
|
||||
self.setLayout(vbox)
|
||||
|
||||
method = getattr(parent, method)
|
||||
if target:
|
||||
method = partial(method, target)
|
||||
self.clicked.connect(method)
|
|
@ -0,0 +1,217 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
import sys
|
||||
import os
|
||||
import gettext
|
||||
import logging
|
||||
from collections import OrderedDict
|
||||
from pathlib import Path
|
||||
|
||||
from PyQt5.QtWidgets import (QDialogButtonBox, QPushButton,
|
||||
QLineEdit, QHBoxLayout, QDialog, QFrame, QLabel, QVBoxLayout)
|
||||
from PyQt5.QtGui import QPixmap
|
||||
from PyQt5.QtCore import Qt
|
||||
|
||||
from app.commons import connection
|
||||
from app.commons import common
|
||||
from app.commons.config import Params
|
||||
from app.commons.dialogs import QuickDialog
|
||||
from app.commons.forms import GridForm
|
||||
|
||||
_ = gettext.gettext
|
||||
|
||||
__all__ = ['Login', 'xconnection']
|
||||
|
||||
pkg_dir = str(Path(os.path.dirname(__file__)).parents[0])
|
||||
path_logo = os.path.join(pkg_dir, 'share', 'login.png')
|
||||
file_base_css = os.path.join(pkg_dir, 'css', 'base.css')
|
||||
file_tablet_css = os.path.join(pkg_dir, 'css', 'tablet.css')
|
||||
|
||||
|
||||
class Login(QDialog):
|
||||
|
||||
def __init__(self, parent=None, file_config=''):
|
||||
super(Login, self).__init__(parent)
|
||||
logging.info(' Start login Neox system X...')
|
||||
self.connection = None
|
||||
params = Params(file_config)
|
||||
self.params = params.params
|
||||
self.setObjectName('dialog_login')
|
||||
if self.params.get('tablet_mode') == 'True':
|
||||
self.tablet_mode = eval(self.params['tablet_mode'])
|
||||
self.set_style([file_tablet_css])
|
||||
else:
|
||||
self.set_style([file_base_css])
|
||||
self.tablet_mode = None
|
||||
self.init_UI()
|
||||
|
||||
def set_style(self, style_files):
|
||||
styles = []
|
||||
for style in style_files:
|
||||
with open(style, 'r') as infile:
|
||||
styles.append(infile.read())
|
||||
self.setStyleSheet(''.join(styles))
|
||||
|
||||
def init_UI(self):
|
||||
hbox_logo = QHBoxLayout()
|
||||
label_logo = QLabel()
|
||||
label_logo.setObjectName('label_logo')
|
||||
hbox_logo.addWidget(label_logo, 0)
|
||||
pixmap_logo = QPixmap(path_logo)
|
||||
label_logo.setPixmap(pixmap_logo)
|
||||
hbox_logo.setAlignment(label_logo, Qt.AlignHCenter)
|
||||
|
||||
values = OrderedDict([
|
||||
('host', {'name': self.tr('HOST'), 'readonly': True}),
|
||||
('database', {'name': self.tr('DATABASE'), 'readonly': True}),
|
||||
('user', {'name': self.tr('USER')}),
|
||||
('password', {'name': self.tr('PASSWORD')}),
|
||||
])
|
||||
formLayout = GridForm(self, values=values, col=1)
|
||||
self.field_password.setEchoMode(QLineEdit.Password)
|
||||
self.field_password.textChanged.connect(self.clear_message)
|
||||
|
||||
box_buttons = QDialogButtonBox()
|
||||
pushButtonCancel = QPushButton(self.tr("C&ANCEL"))
|
||||
pushButtonCancel.setObjectName('button_cancel')
|
||||
box_buttons.addButton(pushButtonCancel, QDialogButtonBox.RejectRole)
|
||||
pushButtonOk = QPushButton(self.tr("&CONNECT"))
|
||||
pushButtonOk.setAutoDefault(True)
|
||||
pushButtonOk.setDefault(False)
|
||||
pushButtonOk.setObjectName('button_ok')
|
||||
box_buttons.addButton(pushButtonOk, QDialogButtonBox.AcceptRole)
|
||||
|
||||
hbox_buttons = QHBoxLayout()
|
||||
hbox_buttons.addWidget(box_buttons)
|
||||
|
||||
line = QFrame()
|
||||
line.setFrameShape(line.HLine)
|
||||
line.setFrameShadow(line.Sunken)
|
||||
hbox_line = QHBoxLayout()
|
||||
hbox_line.addWidget(line)
|
||||
|
||||
hbox_msg = QHBoxLayout()
|
||||
MSG = self.tr('Error: username or password invalid...!')
|
||||
self.error_msg = QLabel(MSG)
|
||||
self.error_msg.setObjectName('login_msg_error')
|
||||
self.error_msg.setAlignment(Qt.AlignCenter)
|
||||
|
||||
hbox_msg.addWidget(self.error_msg)
|
||||
vbox_layout = QVBoxLayout()
|
||||
vbox_layout.addLayout(hbox_logo)
|
||||
vbox_layout.addLayout(formLayout)
|
||||
vbox_layout.addLayout(hbox_msg)
|
||||
vbox_layout.addLayout(hbox_line)
|
||||
vbox_layout.addLayout(hbox_buttons)
|
||||
|
||||
self.setLayout(vbox_layout)
|
||||
self.setWindowTitle('Login Presik System')
|
||||
self.clear_message()
|
||||
|
||||
self.field_password.setFocus()
|
||||
box_buttons.accepted.connect(self.accept)
|
||||
box_buttons.rejected.connect(self.reject)
|
||||
|
||||
def clear_message(self):
|
||||
self.error_msg.hide()
|
||||
|
||||
def run(self, profile=None):
|
||||
if self.params['database']:
|
||||
self.field_database.setText(self.params['database'])
|
||||
if self.params['user']:
|
||||
self.field_user.setText(self.params['user'])
|
||||
if self.params['server']:
|
||||
self.field_host.setText(self.params['server'])
|
||||
|
||||
def accept(self):
|
||||
self.validate_access()
|
||||
super(Login, self).accept()
|
||||
|
||||
def reject(self):
|
||||
sys.exit()
|
||||
|
||||
def validate_access(self):
|
||||
user = self.field_user.text()
|
||||
password = self.field_password.text()
|
||||
self.connection = xconnection(
|
||||
user, password, self.params['server'], self.params['port'],
|
||||
self.params['database'], self.params['protocol']
|
||||
)
|
||||
print(' >> > ', self.connection)
|
||||
if not self.connection:
|
||||
self.field_password.setText('')
|
||||
self.field_password.setFocus()
|
||||
self.error_message()
|
||||
|
||||
self.params['user'] = user
|
||||
self.params['password'] = password
|
||||
|
||||
def error_message(self):
|
||||
self.error_msg.show()
|
||||
|
||||
|
||||
def xconnection(user, password, host, port, database, protocol):
|
||||
# Get user_id and session
|
||||
try:
|
||||
url = 'http://%s:%s@%s:%s/%s/' % (
|
||||
user, password, host, port, database)
|
||||
try:
|
||||
if not common.test_server_version(host, int(port)):
|
||||
print(u'Incompatible version of the server')
|
||||
return
|
||||
except:
|
||||
pass
|
||||
|
||||
if protocol == 'json':
|
||||
print(':::::::::::: Usando protocolo JSON > ', url)
|
||||
conn = connection.set_jsonrpc(url[:-1])
|
||||
elif protocol == 'local':
|
||||
conn = connection.set_trytond(
|
||||
database=database,
|
||||
user=user,
|
||||
)
|
||||
elif protocol == 'xml':
|
||||
print(':::::::::::: Usando protocolo XML > ', url)
|
||||
conn = connection.set_xmlrpc(url)
|
||||
else:
|
||||
print("Protocol error...!")
|
||||
return None
|
||||
|
||||
return conn
|
||||
except:
|
||||
print('LOG: Data connection invalid!')
|
||||
return None
|
||||
|
||||
|
||||
def safe_reconnect(main):
|
||||
field_password = QLineEdit()
|
||||
field_password.setEchoMode(QLineEdit.Password)
|
||||
field_password.cursorPosition()
|
||||
field_password.cursor()
|
||||
dialog_password = QuickDialog(main, 'question',
|
||||
info=main.tr('Enter your password:'),
|
||||
widgets=[field_password],
|
||||
buttons=['ok'],
|
||||
response=True
|
||||
)
|
||||
field_password.setFocus()
|
||||
|
||||
password = field_password.text()
|
||||
if not password or password == '':
|
||||
safe_reconnect(main)
|
||||
|
||||
main.conn = xconnection(
|
||||
main.user,
|
||||
str(password),
|
||||
main.server,
|
||||
main.port,
|
||||
main.database,
|
||||
main.protocol,
|
||||
)
|
||||
|
||||
if main.conn:
|
||||
field_password.setText('')
|
||||
dialog_password.hide()
|
||||
main.global_timer = 0
|
||||
else:
|
||||
safe_reconnect(main)
|
|
@ -0,0 +1,347 @@
|
|||
# -*- coding: UTF-8 -*-
|
||||
import os
|
||||
from collections import OrderedDict
|
||||
|
||||
from PyQt5.QtWidgets import (QDialog, QAbstractItemView, QVBoxLayout,
|
||||
QHBoxLayout, QLabel, QWidget, QTreeView, QLineEdit, QTableView, QCompleter)
|
||||
from PyQt5.QtGui import QStandardItem, QStandardItemModel, QPixmap
|
||||
from PyQt5.QtCore import Qt, pyqtSlot, QModelIndex
|
||||
|
||||
from .qt_models import get_simple_model
|
||||
from .forms import GridForm
|
||||
from .buttons import ActionButton
|
||||
|
||||
__all__ = ['QuickDialog', 'SearchDialog', 'HelpDialog', 'FactoryIcons']
|
||||
|
||||
current_dir = os.path.dirname(__file__)
|
||||
|
||||
_SIZE = (500, 200)
|
||||
|
||||
|
||||
class QuickDialog(QDialog):
|
||||
|
||||
def __init__(self, parent, kind, string=None, data=None, widgets=None,
|
||||
icon=None, size=None, readonly=False):
|
||||
super(QuickDialog, self).__init__(parent)
|
||||
# Size arg is in deprecation
|
||||
if not size:
|
||||
size = _SIZE
|
||||
self.factory = None
|
||||
self.readonly = readonly
|
||||
self.parent = parent
|
||||
self.parent_model = None
|
||||
titles = {
|
||||
'warning': self.tr('Warning...'),
|
||||
'info': self.tr('Information...'),
|
||||
'action': self.tr('Action...'),
|
||||
'help': self.tr('Help...'),
|
||||
'error': self.tr('Error...'),
|
||||
'question': self.tr('Question...'),
|
||||
'selection': self.tr('Selection...'),
|
||||
None: self.tr('Dialog...')
|
||||
}
|
||||
|
||||
self.setWindowTitle(titles[kind])
|
||||
self.setModal(True)
|
||||
self.setParent(parent)
|
||||
self.factory = FactoryIcons()
|
||||
self.default_widget_focus = None
|
||||
self.kind = kind
|
||||
self.widgets = widgets
|
||||
self.data = data
|
||||
string_widget = None
|
||||
data_widget = None
|
||||
_buttons = None
|
||||
row_stretch = 1
|
||||
main_vbox = QVBoxLayout()
|
||||
|
||||
self.sub_hbox = QHBoxLayout()
|
||||
|
||||
# Add main message
|
||||
if string:
|
||||
# For simple dialog
|
||||
string_widget = QLabel(string)
|
||||
|
||||
if kind == 'help':
|
||||
data_widget = widgets[0]
|
||||
elif kind == 'action':
|
||||
if widgets:
|
||||
data_widget = widgets[0]
|
||||
else:
|
||||
data_widget = GridForm(parent, OrderedDict(data))
|
||||
elif kind == 'selection':
|
||||
self.name = data['name']
|
||||
data_widget = self.set_selection(parent, data)
|
||||
elif widgets:
|
||||
data_widget = GridForm(parent, OrderedDict(widgets))
|
||||
|
||||
if string_widget:
|
||||
main_vbox.addWidget(string_widget, 0)
|
||||
|
||||
if data_widget:
|
||||
if isinstance(data_widget, QWidget):
|
||||
row_stretch += 1
|
||||
size = (size[0], size[1] + 200)
|
||||
self.sub_hbox.addWidget(data_widget, 0)
|
||||
else:
|
||||
self.sub_hbox.addLayout(data_widget, 0)
|
||||
|
||||
self.ok_button = ActionButton('ok', self.dialog_accepted)
|
||||
self.ok_button.setFocus()
|
||||
self.ok_button.setDefault(True)
|
||||
self.cancel_button = ActionButton('cancel', self.dialog_rejected)
|
||||
|
||||
_buttons = []
|
||||
if kind in ('info', 'help', 'warning', 'question', 'error'):
|
||||
if kind in ('warning', 'question'):
|
||||
_buttons.append(self.cancel_button)
|
||||
_buttons.append(self.ok_button)
|
||||
elif kind in ('action', 'selection'):
|
||||
_buttons.extend([self.cancel_button, self.ok_button])
|
||||
|
||||
self.buttonbox = QHBoxLayout()
|
||||
for b in _buttons:
|
||||
self.buttonbox.addWidget(b, 1)
|
||||
|
||||
main_vbox.addLayout(self.sub_hbox, 0)
|
||||
main_vbox.addLayout(self.buttonbox, 1)
|
||||
main_vbox.insertStretch(row_stretch, 0)
|
||||
|
||||
self.setLayout(main_vbox)
|
||||
self.setMinimumSize(*size)
|
||||
|
||||
if kind in ('info', 'error'):
|
||||
self.show()
|
||||
|
||||
def exec_(self, args=None):
|
||||
res = None
|
||||
self.parent.releaseKeyboard()
|
||||
res = super(QuickDialog, self).exec()
|
||||
if self.kind == 'action':
|
||||
pass
|
||||
return res
|
||||
|
||||
def show(self):
|
||||
super(QuickDialog, self).show()
|
||||
self.parent.releaseKeyboard()
|
||||
self.ok_button.setFocus()
|
||||
if self.default_widget_focus:
|
||||
self.default_widget_focus.setFocus()
|
||||
if hasattr(self.default_widget_focus, 'setText'):
|
||||
self.default_widget_focus.setText('')
|
||||
else:
|
||||
self.setFocus()
|
||||
|
||||
def hide(self):
|
||||
super(QuickDialog, self).hide()
|
||||
self.parent.setFocus()
|
||||
|
||||
def set_info(self, info):
|
||||
if hasattr(self, 'label_info'):
|
||||
self.label_info.setText(info)
|
||||
|
||||
def set_widgets(self, widgets):
|
||||
if widgets:
|
||||
# Set default focus to first widget created
|
||||
self.default_widget_focus = widgets[0]
|
||||
|
||||
def closeEvent(self, event):
|
||||
super(QuickDialog, self).closeEvent(event)
|
||||
|
||||
def dialog_rejected(self):
|
||||
self.parent.setFocus()
|
||||
self.setResult(0)
|
||||
self.hide()
|
||||
|
||||
def dialog_accepted(self):
|
||||
if self.kind in ('action', 'selection', 'warning', 'question'):
|
||||
self.setResult(1)
|
||||
self.done(1)
|
||||
self.hide()
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
key = event.key()
|
||||
if key == Qt.Key_Escape:
|
||||
self.dialog_rejected()
|
||||
else:
|
||||
super(QuickDialog, self).keyPressEvent(event)
|
||||
|
||||
def set_selection(self, obj, data):
|
||||
self.set_simple_model()
|
||||
setattr(obj, data['name'] + '_model', self.data_model)
|
||||
self.parent_model = data.get('parent_model')
|
||||
self.treeview = QTreeView()
|
||||
self.treeview.setRootIsDecorated(False)
|
||||
self.treeview.setColumnHidden(0, True)
|
||||
self.treeview.setItemsExpandable(False)
|
||||
self.treeview.setAlternatingRowColors(True)
|
||||
self.treeview.setSelectionBehavior(QAbstractItemView.SelectRows)
|
||||
self.treeview.setModel(self.data_model)
|
||||
self.treeview.clicked.connect(self.field_selection_changed)
|
||||
self.treeview.activated.connect(self.field_selection_changed)
|
||||
|
||||
self.update_values(self.data['values'])
|
||||
|
||||
# By default first row must be selected
|
||||
item = self.data_model.item(0, 0)
|
||||
idx = self.data_model.indexFromItem(item)
|
||||
self.treeview.setCurrentIndex(idx)
|
||||
return self.treeview
|
||||
|
||||
def update_values(self, values):
|
||||
self.data_model.removeRows(0, self.data_model.rowCount())
|
||||
self._insert_items(self.data_model, values)
|
||||
self.treeview.resizeColumnToContents(0)
|
||||
|
||||
def set_simple_model(self):
|
||||
self.data_model = QStandardItemModel(0, len(self.data['heads']), self)
|
||||
_horizontal = Qt.Horizontal
|
||||
for i, h in enumerate(self.data['heads'], 0):
|
||||
self.data_model.setHeaderData(i, _horizontal, h)
|
||||
|
||||
def _insert_items(self, model, values):
|
||||
for value in values:
|
||||
row = []
|
||||
for v in value:
|
||||
itemx = QStandardItem(v)
|
||||
itemx.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable)
|
||||
row.append(itemx)
|
||||
self.data_model.insertRow(0, row)
|
||||
self.data_model.sort(0, Qt.AscendingOrder)
|
||||
|
||||
@pyqtSlot(QModelIndex)
|
||||
def field_selection_changed(self, qm_index):
|
||||
if not self.readonly:
|
||||
item_id = self.data_model.item(qm_index.row(), 0).text()
|
||||
item_name = self.data_model.item(qm_index.row(), 1).text()
|
||||
|
||||
if self.parent_model is not None:
|
||||
self.parent_model[self.name] = item_id
|
||||
if hasattr(self.parent, 'field_' + self.name):
|
||||
field = getattr(self.parent, 'field_' + self.name)
|
||||
if hasattr(field, 'setText'):
|
||||
field.setText(item_name)
|
||||
else:
|
||||
setattr(self.parent, 'field_' + self.name + '_name', item_name)
|
||||
setattr(self.parent, 'field_' + self.name + '_id', int(item_id))
|
||||
action = getattr(self.parent, 'action_' + self.name + '_selection_changed')
|
||||
action()
|
||||
self.dialog_accepted()
|
||||
|
||||
|
||||
class SearchDialog(QDialog):
|
||||
|
||||
def __init__(self, parent, headers, values, on_activated,
|
||||
hide_headers=False, completion_column=None, title=None):
|
||||
super(SearchDialog, self).__init__(parent)
|
||||
self.parent = parent
|
||||
self.headers = headers
|
||||
self.values = values
|
||||
if not title:
|
||||
title = self.tr('Search Products...')
|
||||
self.setWindowTitle(title)
|
||||
|
||||
self._product_line = QLineEdit()
|
||||
self.table_view = QTableView()
|
||||
|
||||
button_cancel = ActionButton('cancel', self.on_reject)
|
||||
vbox = QVBoxLayout()
|
||||
hbox = QHBoxLayout()
|
||||
hbox.addWidget(button_cancel)
|
||||
vbox.addWidget(self._product_line)
|
||||
vbox.addLayout(hbox)
|
||||
self.setLayout(vbox)
|
||||
self.completer = QCompleter()
|
||||
self.treeview_search_product = QTreeView()
|
||||
if hide_headers:
|
||||
col_headers = self.treeview_search_product.header()
|
||||
col_headers.hide()
|
||||
self.completer.setPopup(self.treeview_search_product)
|
||||
self._product_line.setCompleter(self.completer)
|
||||
self.set_model()
|
||||
|
||||
self.completer.activated.connect(self.on_accept)
|
||||
self.completer.setFilterMode(Qt.MatchStartsWith)
|
||||
self.completer.setCaseSensitivity(Qt.CaseInsensitive)
|
||||
self.completer.setCompletionColumn(2)
|
||||
self.completer.activated.connect(on_activated)
|
||||
|
||||
def set_model(self):
|
||||
headers_name = [h[1] for h in self.headers]
|
||||
self.model = get_simple_model(self.parent, self.values, headers_name)
|
||||
self.completer.setModel(self.model)
|
||||
|
||||
def get_selected_index(self):
|
||||
model_index = self._get_model_index()
|
||||
idx = self.model.index(model_index.row(), 0)
|
||||
return idx.data()
|
||||
|
||||
def get_selected_data(self):
|
||||
model_index = self._get_model_index()
|
||||
data = {}
|
||||
i = 0
|
||||
for h, _ in self.headers:
|
||||
data[h] = self.model.index(model_index.row(), i).data()
|
||||
i += 1
|
||||
return data
|
||||
|
||||
def _get_model_index(self):
|
||||
item_view = self.completer.popup()
|
||||
index = item_view.currentIndex()
|
||||
proxy_model = self.completer.completionModel()
|
||||
model_index = proxy_model.mapToSource(index)
|
||||
return model_index
|
||||
|
||||
def on_accept(self):
|
||||
self.accept()
|
||||
|
||||
def on_reject(self):
|
||||
self.reject()
|
||||
|
||||
|
||||
class HelpDialog(QuickDialog):
|
||||
|
||||
def __init__(self, parent):
|
||||
self.treeview = QTreeView()
|
||||
self.treeview.setRootIsDecorated(False)
|
||||
self.treeview.setAlternatingRowColors(True)
|
||||
self.treeview.setSelectionBehavior(QAbstractItemView.SelectRows)
|
||||
self.treeview.setEditTriggers(QAbstractItemView.NoEditTriggers)
|
||||
super(HelpDialog, self).__init__(parent, 'help', widgets=[self.treeview],
|
||||
size=(400, 500))
|
||||
self.set_info(self.tr('Keys Shortcuts...'))
|
||||
self.hide()
|
||||
|
||||
def set_shortcuts(self, shortcuts):
|
||||
model = self._help_model(shortcuts)
|
||||
self.treeview.setModel(model)
|
||||
header = self.treeview.header()
|
||||
header.resizeSection(0, 250)
|
||||
|
||||
def _help_model(self, shortcuts):
|
||||
model = QStandardItemModel(0, 2, self)
|
||||
model.setHeaderData(0, Qt.Horizontal, self.tr('Action'))
|
||||
model.setHeaderData(1, Qt.Horizontal, self.tr('Shortcut'))
|
||||
|
||||
for short in shortcuts:
|
||||
model.insertRow(0)
|
||||
model.setData(model.index(0, 0), short[0])
|
||||
model.setData(model.index(0, 1), short[1])
|
||||
return model
|
||||
|
||||
|
||||
class FactoryIcons(object):
|
||||
|
||||
def __init__(self):
|
||||
name_icons = ['print', 'warning', 'info', 'error', 'question']
|
||||
self.icons = {}
|
||||
for name in name_icons:
|
||||
path_icon = os.path.join(current_dir, '..', 'share', 'icon-' + name + '.png')
|
||||
if not os.path.exists(path_icon):
|
||||
continue
|
||||
_qpixmap_icon = QPixmap()
|
||||
_qpixmap_icon.load(path_icon)
|
||||
_icon_label = QLabel()
|
||||
_icon_label.setAlignment(Qt.AlignCenter | Qt.AlignCenter)
|
||||
_icon_label.setPixmap(_qpixmap_icon.scaledToHeight(48))
|
||||
self.icons[name] = _icon_label
|
|
@ -0,0 +1,293 @@
|
|||
import locale
|
||||
|
||||
from PyQt5.QtWidgets import (QLineEdit, QLabel, QComboBox,
|
||||
QGridLayout, QTextEdit, QTreeView, QCompleter)
|
||||
from PyQt5.QtCore import Qt, QRegExp
|
||||
from PyQt5.QtGui import QRegExpValidator, QDoubleValidator
|
||||
|
||||
from .qt_models import get_simple_model
|
||||
from .model import Modules
|
||||
|
||||
regex_ = QRegExp("^\\d{1,3}(([.]\\d{3})*),(\\d{2})$")
|
||||
validator = QRegExpValidator(regex_)
|
||||
|
||||
try:
|
||||
locale.setlocale(locale.LC_ALL, str('es_CO.UTF-8'))
|
||||
except:
|
||||
print("Warning: Error setting locale")
|
||||
|
||||
__all__ = ['Label', 'Field', 'ComboBox', 'GridForm', 'FieldMoney']
|
||||
|
||||
|
||||
def set_object_name(obj, type_, value):
|
||||
size = 'small'
|
||||
color = 'gray'
|
||||
if value.get('size'):
|
||||
size = value.get('size')
|
||||
if value.get('color'):
|
||||
color = value.get('color')
|
||||
name = type_ + size + '_' + color
|
||||
obj.setObjectName(name)
|
||||
|
||||
|
||||
class Completer(QCompleter):
|
||||
|
||||
def __init__(self, parent, records, fields):
|
||||
super(Completer, self).__init__()
|
||||
|
||||
self.parent = parent
|
||||
self.treeview_search = QTreeView()
|
||||
col_headers = self.treeview_search.header()
|
||||
col_headers.hide()
|
||||
self.setPopup(self.treeview_search)
|
||||
self.fields = fields
|
||||
self._set_model(records, fields)
|
||||
self.activated.connect(self.on_accept)
|
||||
self.setFilterMode(Qt.MatchContains)
|
||||
self.setCaseSensitivity(Qt.CaseInsensitive)
|
||||
self.setWrapAround(True)
|
||||
self.setCompletionColumn(1)
|
||||
|
||||
self.treeview_search.setColumnWidth(1, 300)
|
||||
self.treeview_search.setColumnHidden(0, True)
|
||||
self.id = None
|
||||
|
||||
def get_values(self, records):
|
||||
vkeys = [f[0] for f in self.fields]
|
||||
values = []
|
||||
for r in records:
|
||||
row = []
|
||||
for key in vkeys:
|
||||
row.append(r[key])
|
||||
values.append(row)
|
||||
return values
|
||||
|
||||
def _set_model(self, records, headers):
|
||||
headers = [f[1] for f in self.fields]
|
||||
values = self.get_values(records)
|
||||
self.model = get_simple_model(self.parent, values, headers)
|
||||
self.setModel(self.model)
|
||||
|
||||
def on_accept(self):
|
||||
model_index = self._get_model_index()
|
||||
idx = self.model.index(model_index.row(), 0)
|
||||
self.id = idx.data()
|
||||
|
||||
def _get_model_index(self):
|
||||
item_view = self.popup()
|
||||
index = item_view.currentIndex()
|
||||
proxy_model = self.completionModel()
|
||||
model_index = proxy_model.mapToSource(index)
|
||||
return model_index
|
||||
|
||||
|
||||
class Label(QLabel):
|
||||
|
||||
def __init__(self, obj, key, value, align='right'):
|
||||
super(Label, self).__init__()
|
||||
self.setText(value['name'] + ':')
|
||||
set_object_name(self, 'label_', value)
|
||||
if align == 'left':
|
||||
self.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
|
||||
else:
|
||||
self.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
|
||||
|
||||
|
||||
class Field(QLineEdit):
|
||||
|
||||
def __init__(self, obj, key, value, type=None):
|
||||
super(Field, self).__init__()
|
||||
setattr(obj, 'field_' + key, self)
|
||||
self.parent = obj
|
||||
set_object_name(self, 'field_', value)
|
||||
if value.get('type') == 'numeric':
|
||||
self.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
|
||||
elif value.get('type') == 'relation':
|
||||
self.set_completer(value.get('model'), value.get('fields'),
|
||||
value.get('domain'))
|
||||
|
||||
def set_completer(self, tryton_model, fields, domain=[]):
|
||||
records = tryton_model.find(domain)
|
||||
self.completer = Completer(self.parent, records, fields)
|
||||
self.setCompleter(self.completer)
|
||||
|
||||
def get_id(self):
|
||||
return self.completer.id
|
||||
|
||||
# FIXME
|
||||
def _get_tryton_model(self, model, fields):
|
||||
modules = Modules(self, self.conn)
|
||||
modules.set_models([ {
|
||||
'name': '_Model',
|
||||
'model': model,
|
||||
'fields': fields,
|
||||
}])
|
||||
|
||||
|
||||
class TextField(QTextEdit):
|
||||
|
||||
def __init__(self, obj, key, value):
|
||||
super(Field, self).__init__()
|
||||
setattr(obj, 'field_' + key, self)
|
||||
set_object_name(self, 'field_', value)
|
||||
self.value_changed = False
|
||||
self.setValidator(validator)
|
||||
|
||||
def textChanged(self, text):
|
||||
self.value_changed = True
|
||||
|
||||
|
||||
class FieldMoney(QLineEdit):
|
||||
|
||||
def __init__(self, obj, key, value, amount=None, digits=2, readonly=True):
|
||||
super(FieldMoney, self).__init__()
|
||||
setattr(obj, 'field_' + key, self)
|
||||
set_object_name(self, 'field_', value)
|
||||
self.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
|
||||
self.digits = 2
|
||||
self.value_changed = False
|
||||
self.textEdited.connect(self.value_edited)
|
||||
self._text = '0'
|
||||
self.amount = 0
|
||||
self.setReadOnly(readonly)
|
||||
validator = QDoubleValidator()
|
||||
validator.setDecimals(2)
|
||||
self.setValidator(validator)
|
||||
if not amount:
|
||||
self.zero()
|
||||
|
||||
def __str__(self):
|
||||
return self.format_text()
|
||||
|
||||
def format_text(self, text_):
|
||||
amount = float(text_)
|
||||
return "{:,}".format(round(amount, self.digits))
|
||||
|
||||
def setText(self, amount):
|
||||
if not amount:
|
||||
text = ''
|
||||
else:
|
||||
text = self.format_text(amount)
|
||||
super(FieldMoney, self).setText(str(text))
|
||||
|
||||
def zero(self):
|
||||
self.setText(str(0))
|
||||
|
||||
def value_edited(self, amount):
|
||||
self.value_changed = True
|
||||
|
||||
def show(self):
|
||||
pass
|
||||
|
||||
|
||||
class ComboBox(QComboBox):
|
||||
|
||||
def __init__(self, obj, key, data):
|
||||
super(ComboBox, self).__init__()
|
||||
setattr(obj, 'field_' + key, self)
|
||||
self.parent = obj
|
||||
self.setFrame(True)
|
||||
self.setObjectName('field_' + key)
|
||||
values = []
|
||||
if data.get('values'):
|
||||
values = data.get('values')
|
||||
heads = []
|
||||
if data.get('heads'):
|
||||
heads = data.get('heads')
|
||||
selection_model = get_simple_model(obj, values, heads)
|
||||
self.setModel(selection_model)
|
||||
self.setModelColumn(1)
|
||||
selection_model.findItems(str(3), column=0)
|
||||
if data.get('on_change'):
|
||||
self.method_on_change = getattr(self.parent, data.get('on_change'))
|
||||
self.currentIndexChanged.connect(self.on_change)
|
||||
|
||||
def on_change(self, index):
|
||||
self.method_on_change(index)
|
||||
|
||||
def set_editable(self, value=True):
|
||||
self.setEditable(value)
|
||||
|
||||
def set_enabled(self, value=True):
|
||||
self.setEnabled(value)
|
||||
|
||||
def get_id(self):
|
||||
model = self.model()
|
||||
row = self.currentIndex()
|
||||
column = 0 # id ever is column Zero
|
||||
res = model.item(row, column)
|
||||
return res.text()
|
||||
|
||||
def get_label(self):
|
||||
model = self.model()
|
||||
row = self.currentIndex()
|
||||
column = 1 # id ever is column Zero
|
||||
res = model.item(row, column)
|
||||
return res.text()
|
||||
|
||||
def set_from_id(self, id_):
|
||||
model = self.model()
|
||||
items = model.findItems(str(id_), column=0)
|
||||
idx = model.indexFromItem(items[0])
|
||||
self.setCurrentIndex(idx.row())
|
||||
|
||||
|
||||
class GridForm(QGridLayout):
|
||||
"""
|
||||
Add a simple form Grid Style to screen,
|
||||
from a data dict with set of {values, attributes}
|
||||
example:
|
||||
(field_name, {
|
||||
'name': string descriptor,
|
||||
'readonly': Bool,
|
||||
'type': type_widget,
|
||||
'placeholder': True or False,
|
||||
}),
|
||||
col:: is number of columns
|
||||
type_widget :: field or selection
|
||||
"""
|
||||
|
||||
def __init__(self, obj, values, col=1):
|
||||
super(GridForm, self).__init__()
|
||||
row = 1
|
||||
cols = 0
|
||||
align = 'right'
|
||||
if col == 0:
|
||||
align = 'left'
|
||||
|
||||
for key, value in values.items():
|
||||
if not value.get('placeholder'):
|
||||
_label = Label(obj, key, value, align)
|
||||
if value.get('type') == 'selection':
|
||||
_field = ComboBox(obj, key, value)
|
||||
elif value.get('type') == 'money':
|
||||
_field = FieldMoney(obj, key, value)
|
||||
else:
|
||||
_field = Field(obj, key, value)
|
||||
if value.get('password') is True:
|
||||
_field.setEchoMode(QLineEdit.Password)
|
||||
if value.get('placeholder'):
|
||||
_field.setPlaceholderText(value['name'])
|
||||
|
||||
self.setRowStretch(row, 0)
|
||||
column1 = cols * col + 1
|
||||
column2 = column1 + 1
|
||||
if value.get('invisible') is True:
|
||||
continue
|
||||
if not value.get('placeholder'):
|
||||
self.addWidget(_label, row, column1)
|
||||
if col == 0:
|
||||
row = row + 1
|
||||
self.addWidget(_field, row, column1)
|
||||
else:
|
||||
self.addWidget(_field, row, column2)
|
||||
|
||||
if value.get('readonly') is True:
|
||||
_field.setReadOnly(True)
|
||||
_field.setFocusPolicy(Qt.NoFocus)
|
||||
|
||||
if cols < (col - 1):
|
||||
cols += 1
|
||||
else:
|
||||
row += 1
|
||||
cols = 0
|
|
@ -0,0 +1,152 @@
|
|||
# -*- coding: UTF-8 -*-
|
||||
import os
|
||||
import time
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from PyQt5.QtWidgets import QMainWindow, QDesktopWidget, QLabel
|
||||
from PyQt5.QtCore import QTimer, QThread, pyqtSignal, Qt
|
||||
|
||||
from .dialogs import QuickDialog
|
||||
from .dblogin import safe_reconnect
|
||||
|
||||
|
||||
__all__ = ['FrontWindow', 'ClearUi']
|
||||
|
||||
parent = Path(__file__).parent.parent
|
||||
|
||||
file_base_css = os.path.join(str(parent), 'css', 'base.css')
|
||||
_DEFAULT_TIMEOUT = 60000 # on ms (100 minutes)
|
||||
path_trans = os.path.join(os.path.abspath(
|
||||
os.path.dirname(__file__)), 'locale', 'i18n_es.qm')
|
||||
|
||||
|
||||
class FrontWindow(QMainWindow):
|
||||
|
||||
def __init__(self, connection, params, title=None, show_mode=None):
|
||||
super(FrontWindow, self).__init__()
|
||||
if not title:
|
||||
title = self.tr('APPLICATION')
|
||||
|
||||
self._state = None
|
||||
self._keyStates = {}
|
||||
self.window().setWindowTitle(title)
|
||||
self.setObjectName('WinMain')
|
||||
self.conn = connection
|
||||
self._context = connection.context
|
||||
self.set_params(params)
|
||||
self.logger = logging.getLogger('neox_logger')
|
||||
|
||||
"""
|
||||
We need get the size of screen (display)
|
||||
--------------- -------------------
|
||||
name width (px)
|
||||
--------------- -------------------
|
||||
small screen =< 1024
|
||||
medium screen > 1024 and =< 1366
|
||||
large screen > 1366
|
||||
"""
|
||||
|
||||
screen = QDesktopWidget().screenGeometry()
|
||||
self.setGeometry(0, 0, screen.width(), screen.height())
|
||||
screen_width = screen.width()
|
||||
print('Screen width : ', screen_width)
|
||||
|
||||
self.screen_size = 'large'
|
||||
if screen_width <= 1024:
|
||||
self.screen_size = 'small'
|
||||
elif screen_width <= 1366:
|
||||
self.screen_size = 'medium'
|
||||
|
||||
self.timeout = _DEFAULT_TIMEOUT
|
||||
self.set_stack_messages()
|
||||
|
||||
if show_mode == 'fullscreen':
|
||||
self.window().showFullScreen()
|
||||
else:
|
||||
self.window().show()
|
||||
self.setFocus()
|
||||
self.global_timer = 0
|
||||
|
||||
def set_stack_messages(self):
|
||||
self.stack_msg = {}
|
||||
|
||||
def get_geometry(self):
|
||||
screen = QDesktopWidget().screenGeometry()
|
||||
return screen.width(), screen.height()
|
||||
|
||||
def set_statusbar(self, values):
|
||||
status_bar = self.statusBar()
|
||||
status_bar.setSizeGripEnabled(False)
|
||||
|
||||
for k, v in values.items():
|
||||
_label = QLabel(v['name'] + ':')
|
||||
_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
|
||||
status_bar.addWidget(_label, 1)
|
||||
setattr(self, k, QLabel(str(v['value'])))
|
||||
_label_info = getattr(self, k)
|
||||
_label_info.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
|
||||
status_bar.addWidget(_label_info)
|
||||
|
||||
def set_style(self, file_css):
|
||||
styles = []
|
||||
for style in [file_base_css, file_css]:
|
||||
with open(style, 'r') as infile:
|
||||
styles.append(infile.read())
|
||||
self.setStyleSheet(''.join(styles))
|
||||
|
||||
def set_timeout(self):
|
||||
if self.active_timeout != 'True':
|
||||
return
|
||||
|
||||
self.timeout = eval(self.timeout)
|
||||
if not self.timeout:
|
||||
self.timeout = _DEFAULT_TIMEOUT
|
||||
timer = QTimer(self)
|
||||
timer.timeout.connect(self.count_time)
|
||||
timer.start(1000)
|
||||
|
||||
def count_time(self):
|
||||
self.global_timer += 1
|
||||
if self.global_timer > self.timeout:
|
||||
self.global_timer = 0
|
||||
safe_reconnect()
|
||||
|
||||
def dialog(self, name, response=False):
|
||||
res = QuickDialog(
|
||||
parent=self,
|
||||
kind=self.stack_msg[name][0],
|
||||
string=self.stack_msg[name][1],
|
||||
)
|
||||
return res
|
||||
|
||||
def set_params(self, values):
|
||||
for k, v in values.items():
|
||||
if v in ('False', 'True'):
|
||||
v = eval(v)
|
||||
setattr(self, k, v)
|
||||
|
||||
def action_block(self):
|
||||
safe_reconnect(self)
|
||||
|
||||
def dialog_password_accept(self):
|
||||
self.connection()
|
||||
|
||||
def dialog_password_rejected(self):
|
||||
self.connection()
|
||||
|
||||
def keyReleaseEvent(self, event):
|
||||
self._keyStates[event.key()] = False
|
||||
|
||||
|
||||
class ClearUi(QThread):
|
||||
sigActionClear = pyqtSignal()
|
||||
state = None
|
||||
|
||||
def __init__(self, wait_time):
|
||||
QThread.__init__(self)
|
||||
self.wait_time = wait_time
|
||||
|
||||
def run(self):
|
||||
time.sleep(self.wait_time)
|
||||
self.sigActionClear.emit()
|
|
@ -0,0 +1,27 @@
|
|||
# -*- coding: UTF-8 -*-
|
||||
import time
|
||||
import locale
|
||||
from datetime import datetime
|
||||
|
||||
try:
|
||||
locale.setlocale(locale.LC_ALL, str('es_CO.UTF-8'))
|
||||
except:
|
||||
print("Warning: Error setting locale")
|
||||
|
||||
starttime = datetime.now()
|
||||
|
||||
|
||||
def time_record(x):
|
||||
now = datetime.now()
|
||||
print(x, (now - starttime).total_seconds())
|
||||
|
||||
|
||||
def time_dec(func):
|
||||
def time_mes(self, *arg):
|
||||
t1 = time.clock()
|
||||
res = func(self, *arg)
|
||||
t2 = time.clock()
|
||||
delta = (t2 - t1) * 1000.0
|
||||
print('%s take %0.5f ms' % (func.__name__, delta))
|
||||
return res
|
||||
return time_mes
|
|
@ -0,0 +1,68 @@
|
|||
|
||||
from PyQt5.QtWidgets import QLabel, QWidget, QDesktopWidget
|
||||
from PyQt5.QtCore import Qt, QByteArray
|
||||
from PyQt5.QtGui import QPixmap
|
||||
|
||||
__all__ = ['Image']
|
||||
|
||||
|
||||
class Image(QLabel):
|
||||
|
||||
def __init__(self, obj=None, name='', default_img=None, scaled_rate=None):
|
||||
if not obj:
|
||||
obj = QWidget()
|
||||
super(Image, self).__init__(obj)
|
||||
|
||||
screen = QDesktopWidget().screenGeometry()
|
||||
screen_width = screen.width()
|
||||
|
||||
self.parent = obj
|
||||
self.setObjectName('img_' + name)
|
||||
|
||||
if default_img:
|
||||
self.pixmap = QPixmap()
|
||||
self.pixmap.load(default_img)
|
||||
img_width, img_height = self.pixmap.width(), self.pixmap.height()
|
||||
scaled_rate = False
|
||||
if screen_width <= 1024:
|
||||
scaled_rate = 0.5
|
||||
elif screen_width <= 1366:
|
||||
scaled_rate = 0.75
|
||||
if scaled_rate:
|
||||
new_width = img_width * scaled_rate
|
||||
new_height = img_height * scaled_rate
|
||||
self.pixmap = self.pixmap.scaled(new_width, new_height,
|
||||
Qt.KeepAspectRatio, Qt.SmoothTransformation)
|
||||
self.setPixmap(self.pixmap)
|
||||
|
||||
def set_image(self, img, kind=None):
|
||||
self.pixmap = QPixmap()
|
||||
if img:
|
||||
if kind == 'bytes':
|
||||
ba = QByteArray.fromBase64(img)
|
||||
self.pixmap.loadFromData(ba)
|
||||
else:
|
||||
self.pixmap.loadFromData(img.data)
|
||||
self.setPixmap(self.pixmap)
|
||||
|
||||
def load_image(self, pathfile):
|
||||
self.pixmap = QPixmap()
|
||||
self.pixmap.load(pathfile)
|
||||
self.setPixmap(self.pixmap)
|
||||
|
||||
def activate(self):
|
||||
self.free_center()
|
||||
self.parent.show()
|
||||
|
||||
def free_center(self):
|
||||
screen = QDesktopWidget().screenGeometry()
|
||||
screen_width = screen.width()
|
||||
screen_height = screen.height()
|
||||
size = self.pixmap.size()
|
||||
print("image size", size.width(), size.height())
|
||||
self.parent.setGeometry(
|
||||
(screen_width / 2) - (size.width() / 2),
|
||||
(screen_height / 2) - (size.height() / 2),
|
||||
size.width(),
|
||||
size.height()
|
||||
)
|
|
@ -0,0 +1,369 @@
|
|||
|
||||
import sys
|
||||
import ssl
|
||||
from decimal import Decimal
|
||||
import datetime
|
||||
import socket
|
||||
import gzip
|
||||
import hashlib
|
||||
import base64
|
||||
import threading
|
||||
import errno
|
||||
from functools import partial
|
||||
from contextlib import contextmanager
|
||||
import string
|
||||
|
||||
from xmlrpc import client
|
||||
|
||||
try:
|
||||
import simplejson as json
|
||||
except ImportError:
|
||||
import json
|
||||
|
||||
try:
|
||||
import http.client as httplib
|
||||
except:
|
||||
import httplib
|
||||
|
||||
__all__ = ["ResponseError", "Fault", "ProtocolError", "Transport",
|
||||
"ServerProxy", "ServerPool"]
|
||||
|
||||
PYTHON_VERSION = str(sys.version_info[0])
|
||||
CONNECT_TIMEOUT = 5
|
||||
DEFAULT_TIMEOUT = None
|
||||
|
||||
|
||||
class ResponseError(client.ResponseError):
|
||||
pass
|
||||
|
||||
|
||||
class Fault(client.Fault):
|
||||
|
||||
def __init__(self, faultCode, faultString='', **extra):
|
||||
super(Fault, self).__init__(faultCode, faultString, **extra)
|
||||
self.args = faultString
|
||||
|
||||
def __repr__(self):
|
||||
return (
|
||||
"<Fault %s: %s>" %
|
||||
(repr(self.faultCode), repr(self.faultString))
|
||||
)
|
||||
|
||||
|
||||
class ProtocolError(client.ProtocolError):
|
||||
pass
|
||||
|
||||
|
||||
def object_hook(dct):
|
||||
if '__class__' in dct:
|
||||
if dct['__class__'] == 'datetime':
|
||||
return datetime.datetime(dct['year'], dct['month'], dct['day'],
|
||||
dct['hour'], dct['minute'], dct['second'], dct['microsecond'])
|
||||
elif dct['__class__'] == 'date':
|
||||
return datetime.date(dct['year'], dct['month'], dct['day'])
|
||||
elif dct['__class__'] == 'time':
|
||||
return datetime.time(dct['hour'], dct['minute'], dct['second'],
|
||||
dct['microsecond'])
|
||||
elif dct['__class__'] == 'timedelta':
|
||||
return datetime.timedelta(seconds=dct['seconds'])
|
||||
elif dct['__class__'] == 'bytes':
|
||||
cast = bytearray if bytes == str else bytes
|
||||
return cast(base64.decodestring(dct['base64']))
|
||||
elif dct['__class__'] == 'Decimal':
|
||||
return Decimal(dct['decimal'])
|
||||
return dct
|
||||
|
||||
|
||||
class JSONEncoder(json.JSONEncoder):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(JSONEncoder, self).__init__(*args, **kwargs)
|
||||
# Force to use our custom decimal with simplejson
|
||||
self.use_decimal = False
|
||||
|
||||
def default(self, obj):
|
||||
if isinstance(obj, datetime.date):
|
||||
if isinstance(obj, datetime.datetime):
|
||||
return {'__class__': 'datetime',
|
||||
'year': obj.year,
|
||||
'month': obj.month,
|
||||
'day': obj.day,
|
||||
'hour': obj.hour,
|
||||
'minute': obj.minute,
|
||||
'second': obj.second,
|
||||
'microsecond': obj.microsecond,
|
||||
}
|
||||
return {'__class__': 'date',
|
||||
'year': obj.year,
|
||||
'month': obj.month,
|
||||
'day': obj.day,
|
||||
}
|
||||
elif isinstance(obj, datetime.time):
|
||||
return {'__class__': 'time',
|
||||
'hour': obj.hour,
|
||||
'minute': obj.minute,
|
||||
'second': obj.second,
|
||||
'microsecond': obj.microsecond,
|
||||
}
|
||||
elif isinstance(obj, datetime.timedelta):
|
||||
return {'__class__': 'timedelta',
|
||||
'seconds': obj.total_seconds(),
|
||||
}
|
||||
elif isinstance(obj, memoryview):
|
||||
return {'__class__': 'buffer',
|
||||
'base64': base64.encodestring(obj),
|
||||
}
|
||||
elif isinstance(obj, Decimal):
|
||||
return {'__class__': 'Decimal',
|
||||
'decimal': str(obj),
|
||||
}
|
||||
return super(JSONEncoder, self).default(obj)
|
||||
|
||||
|
||||
class JSONParser(object):
|
||||
|
||||
def __init__(self, target):
|
||||
self.__targer = target
|
||||
|
||||
def feed(self, data):
|
||||
self.__targer.feed(data)
|
||||
|
||||
def close(self):
|
||||
pass
|
||||
|
||||
|
||||
class JSONUnmarshaller(object):
|
||||
def __init__(self):
|
||||
self.data = []
|
||||
|
||||
def feed(self, data):
|
||||
self.data.append(data)
|
||||
|
||||
def close(self):
|
||||
# Convert data bytes to string
|
||||
data = self.data[0].decode('utf-8')
|
||||
return json.loads(data, object_hook=object_hook)
|
||||
|
||||
|
||||
class Transport(client.SafeTransport, client.Transport):
|
||||
|
||||
accept_gzip_encoding = True
|
||||
encode_threshold = 1400 # common MTU
|
||||
|
||||
def __init__(self, fingerprints=None, ca_certs=None, session=None):
|
||||
client.Transport.__init__(self)
|
||||
self._connection = (None, None)
|
||||
self.__fingerprints = fingerprints
|
||||
self.__ca_certs = ca_certs
|
||||
self.session = session
|
||||
|
||||
def getparser(self):
|
||||
target = JSONUnmarshaller()
|
||||
parser = JSONParser(target)
|
||||
return parser, target
|
||||
|
||||
def get_host_info(self, host):
|
||||
host, extra_headers, x509 = client.Transport.get_host_info(
|
||||
self, host)
|
||||
if extra_headers is None:
|
||||
extra_headers = []
|
||||
if self.session:
|
||||
auth = base64.encodestring(self.session)
|
||||
auth = string.join(string.split(auth), "") # get rid of whitespace
|
||||
extra_headers.append(
|
||||
('Authorization', 'Session ' + auth),
|
||||
)
|
||||
extra_headers.append(('Connection', 'keep-alive'))
|
||||
extra_headers.append(('Content-Type', 'text/json'))
|
||||
return host, extra_headers, x509
|
||||
|
||||
def send_content(self, connection, request_body):
|
||||
if (self.encode_threshold is not None and
|
||||
self.encode_threshold < len(request_body) and
|
||||
gzip):
|
||||
connection.putheader("Content-Encoding", "gzip")
|
||||
request_body = gzip.compress(request_body)
|
||||
connection.putheader("Content-Length", str(len(request_body)))
|
||||
connection.endheaders()
|
||||
if request_body:
|
||||
if PYTHON_VERSION == '3':
|
||||
request_body = bytes(request_body, 'UTF-8')
|
||||
connection.send(request_body)
|
||||
|
||||
def make_connection(self, host):
|
||||
if self._connection and host == self._connection[0]:
|
||||
return self._connection[1]
|
||||
host, self._extra_headers, x509 = self.get_host_info(host)
|
||||
ca_certs = self.__ca_certs
|
||||
cert_reqs = ssl.CERT_REQUIRED if ca_certs else ssl.CERT_NONE
|
||||
|
||||
class HTTPSConnection(httplib.HTTPSConnection):
|
||||
|
||||
def connect(self):
|
||||
sock = socket.create_connection((self.host, self.port),
|
||||
self.timeout)
|
||||
if self._tunnel_host:
|
||||
self.sock = sock
|
||||
self._tunnel()
|
||||
self.sock = ssl.wrap_socket(sock, self.key_file,
|
||||
self.cert_file, ca_certs=ca_certs, cert_reqs=cert_reqs)
|
||||
|
||||
def http_connection():
|
||||
self._connection = host, httplib.HTTPConnection(host,
|
||||
timeout=CONNECT_TIMEOUT)
|
||||
self._connection[1].connect()
|
||||
sock = self._connection[1].sock
|
||||
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
|
||||
|
||||
def https_connection():
|
||||
self._connection = host, HTTPSConnection(host,
|
||||
timeout=CONNECT_TIMEOUT)
|
||||
try:
|
||||
self._connection[1].connect()
|
||||
sock = self._connection[1].sock
|
||||
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
|
||||
try:
|
||||
peercert = sock.getpeercert(True)
|
||||
except socket.error:
|
||||
peercert = None
|
||||
|
||||
def format_hash(value):
|
||||
return reduce(lambda x, y: x + y[1].upper() +
|
||||
((y[0] % 2 and y[0] + 1 < len(value)) and ':' or ''),
|
||||
enumerate(value), '')
|
||||
return format_hash(hashlib.sha1(peercert).hexdigest())
|
||||
except ssl.SSLError:
|
||||
http_connection()
|
||||
|
||||
fingerprint = ''
|
||||
if self.__fingerprints is not None and host in self.__fingerprints:
|
||||
if self.__fingerprints[host]:
|
||||
fingerprint = https_connection()
|
||||
else:
|
||||
http_connection()
|
||||
else:
|
||||
fingerprint = https_connection()
|
||||
|
||||
if self.__fingerprints is not None:
|
||||
if host in self.__fingerprints and self.__fingerprints[host]:
|
||||
if self.__fingerprints[host] != fingerprint:
|
||||
self.close()
|
||||
raise ssl.SSLError('BadFingerprint')
|
||||
else:
|
||||
self.__fingerprints[host] = fingerprint
|
||||
self._connection[1].timeout = DEFAULT_TIMEOUT
|
||||
self._connection[1].sock.settimeout(DEFAULT_TIMEOUT)
|
||||
return self._connection[1]
|
||||
|
||||
|
||||
class ServerProxy(client.ServerProxy):
|
||||
__id = 0
|
||||
|
||||
def __init__(self, host, port, database='', verbose=0,
|
||||
fingerprints=None, ca_certs=None, session=None):
|
||||
self.__host = '%s:%s' % (host, port)
|
||||
if database:
|
||||
self.__handler = '/%s/' % database
|
||||
else:
|
||||
self.__handler = '/'
|
||||
self.__transport = Transport(fingerprints, ca_certs, session)
|
||||
self.__verbose = verbose
|
||||
|
||||
def __request(self, methodname, params):
|
||||
self.__id += 1
|
||||
id_ = self.__id
|
||||
request = json.dumps({
|
||||
'id': id_,
|
||||
'method': methodname,
|
||||
'params': params,
|
||||
}, cls=JSONEncoder)
|
||||
try:
|
||||
response = self.__transport.request(
|
||||
self.__host,
|
||||
self.__handler,
|
||||
request,
|
||||
verbose=self.__verbose
|
||||
)
|
||||
except (socket.error, httplib.HTTPException) as v:
|
||||
if (isinstance(v, socket.error)
|
||||
and v.args[0] == errno.EPIPE):
|
||||
raise
|
||||
# try one more time
|
||||
self.__transport.close()
|
||||
response = self.__transport.request(
|
||||
self.__host,
|
||||
self.__handler,
|
||||
request,
|
||||
verbose=self.__verbose
|
||||
)
|
||||
except:
|
||||
self.__transport.close()
|
||||
raise
|
||||
if response['id'] != id_:
|
||||
raise ResponseError('Invalid response id (%s) excpected %s' %
|
||||
(response['id'], id_))
|
||||
if response.get('error'):
|
||||
raise Fault(*response['error'])
|
||||
return response['result']
|
||||
|
||||
def close(self):
|
||||
self.__transport.close()
|
||||
|
||||
@property
|
||||
def ssl(self):
|
||||
return isinstance(self.__transport.make_connection(self.__host),
|
||||
httplib.HTTPSConnection)
|
||||
|
||||
|
||||
class ServerPool(object):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.ServerProxy = partial(ServerProxy, *args, **kwargs)
|
||||
self._lock = threading.Lock()
|
||||
self._pool = []
|
||||
self._used = {}
|
||||
self.session = None
|
||||
|
||||
def getconn(self):
|
||||
with self._lock:
|
||||
if self._pool:
|
||||
conn = self._pool.pop()
|
||||
else:
|
||||
conn = self.ServerProxy()
|
||||
self._used[id(conn)] = conn
|
||||
return conn
|
||||
|
||||
def putconn(self, conn):
|
||||
with self._lock:
|
||||
self._pool.append(conn)
|
||||
del self._used[id(conn)]
|
||||
|
||||
def close(self):
|
||||
with self._lock:
|
||||
for conn in self._pool + self._used.values():
|
||||
conn.close()
|
||||
|
||||
@property
|
||||
def ssl(self):
|
||||
for conn in self._pool + self._used.values():
|
||||
return conn.ssl
|
||||
return False
|
||||
|
||||
@contextmanager
|
||||
def __call__(self):
|
||||
conn = self.getconn()
|
||||
yield conn
|
||||
self.putconn(conn)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# For testing purposes
|
||||
connection = ServerProxy('127.0.0.1', '8000', 'DEMO41')
|
||||
result = connection.common.server.version()
|
||||
print(result)
|
||||
conn = connection()
|
||||
res = connection.common.db.login('admin', 'aa')
|
||||
print(res)
|
||||
tx = connection.common.model.res.user.get_preferences(res[0], res[1], True, {})
|
|
@ -0,0 +1,190 @@
|
|||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from decimal import Decimal
|
||||
from PyQt5.QtCore import Qt, pyqtSignal, QSize
|
||||
from PyQt5.QtGui import QIcon, QPixmap
|
||||
from PyQt5.QtWidgets import (QWidget, QHBoxLayout, QScroller, QVBoxLayout,
|
||||
QPushButton, QGridLayout, QScrollArea, QLabel)
|
||||
|
||||
from .custom_button import CustomButton
|
||||
|
||||
pkg_dir = str(Path(os.path.dirname(__file__)).parents[0])
|
||||
file_back_icon = os.path.join(pkg_dir, 'share', 'back.svg')
|
||||
file_menu_img = os.path.join(pkg_dir, 'share', 'menu.png')
|
||||
|
||||
__all__ = ['GridButtons', 'MenuDash']
|
||||
|
||||
|
||||
def money(v):
|
||||
return '${:9,}'.format(int(v))
|
||||
|
||||
|
||||
class GridButtons(QWidget):
|
||||
sigItem_selected = pyqtSignal(str)
|
||||
|
||||
def __init__(self, parent, rows, num_cols, action):
|
||||
"""
|
||||
rows: a list of lists
|
||||
num_cols: number of columns?
|
||||
"""
|
||||
QWidget.__init__(self)
|
||||
self.parent = parent
|
||||
self.layout = QGridLayout()
|
||||
self.setLayout(self.layout)
|
||||
self.button_size = parent.screen_size
|
||||
self.rows = rows
|
||||
self.action = action
|
||||
self.num_cols = num_cols
|
||||
self.layout.setSpacing(15)
|
||||
if rows:
|
||||
self.set_items(rows)
|
||||
|
||||
def action_selected(self, idx):
|
||||
self.action(idx)
|
||||
|
||||
def set_items(self, rows):
|
||||
self.rows = rows
|
||||
colx = 0
|
||||
rowy = 0
|
||||
for row in rows:
|
||||
if colx >= 2:
|
||||
colx = 0
|
||||
rowy += 1
|
||||
|
||||
if isinstance(row[3], Decimal):
|
||||
row[3] = money(int(row[3]))
|
||||
|
||||
item_button = CustomButton(
|
||||
parent=self,
|
||||
id=row[0],
|
||||
title=row[2],
|
||||
desc=str(row[3]),
|
||||
method='action_selected',
|
||||
target=row[0],
|
||||
size=self.button_size,
|
||||
name_style='product_button'
|
||||
)
|
||||
item_button.setMaximumHeight(110)
|
||||
item_button.setMinimumHeight(100)
|
||||
self.layout.addWidget(item_button, rowy, colx)
|
||||
colx += 1
|
||||
self.layout.setRowMinimumHeight(rowy, 110)
|
||||
self.layout.setRowStretch(rowy + 1, 1)
|
||||
|
||||
|
||||
class MenuDash(QVBoxLayout):
|
||||
|
||||
def __init__(self, parent, values, selected_method=None, title=None):
|
||||
"""
|
||||
parent: parent window
|
||||
values: is to list of list/tuples values for data model
|
||||
[('a' 'b', 'c'), ('d', 'e', 'f')...]
|
||||
on_selected: method to call when triggered the selection
|
||||
title: title of window
|
||||
"""
|
||||
super(MenuDash, self).__init__()
|
||||
|
||||
self.parent = parent
|
||||
self.values = values
|
||||
self.current_view = None
|
||||
|
||||
self.button_size = parent.screen_size
|
||||
self.method_on_selected = getattr(self.parent, selected_method)
|
||||
self.create_categories()
|
||||
|
||||
pixmap = QPixmap(file_menu_img)
|
||||
new_pixmap = pixmap.scaled(200, 60)
|
||||
label_menu = QLabel('')
|
||||
label_menu.setPixmap(new_pixmap)
|
||||
|
||||
widget_head = QWidget()
|
||||
widget_head.setStyleSheet("background-color: white;")
|
||||
self.layout_head = QHBoxLayout()
|
||||
widget_head.setLayout(self.layout_head)
|
||||
|
||||
self.addWidget(widget_head, 0)
|
||||
self.pushButtonBack = QPushButton()
|
||||
self.pushButtonBack.setIcon(QIcon(file_back_icon))
|
||||
self.pushButtonBack.setIconSize(QSize(25, 25))
|
||||
self.pushButtonBack.setMaximumWidth(35)
|
||||
|
||||
self.layout_head.addWidget(self.pushButtonBack, stretch=0)
|
||||
self.layout_head.addWidget(label_menu, stretch=1)
|
||||
self.addWidget(self.category_area, 0)
|
||||
self.pushButtonBack.clicked.connect(self.action_back)
|
||||
|
||||
def setState(self, args):
|
||||
if args.get('layout_invisible'):
|
||||
self.category_area.hide()
|
||||
self.removeWidget(self.current_view)
|
||||
else:
|
||||
self.category_area.show()
|
||||
self.addWidget(self.category_area)
|
||||
if self.current_view:
|
||||
self.current_view.hide()
|
||||
|
||||
if args.get('button'):
|
||||
view_id = args.get('button')
|
||||
if hasattr(self, 'view_' + str(view_id)):
|
||||
self.current_view = getattr(self, 'view_' + str(view_id))
|
||||
self.addWidget(self.current_view)
|
||||
self.current_view.show()
|
||||
|
||||
def create_categories(self):
|
||||
# set the list model
|
||||
self.category_area = QScrollArea()
|
||||
self.category_area.setWidgetResizable(True)
|
||||
self.category_area.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
|
||||
QScroller.grabGesture(self.category_area, QScroller.LeftMouseButtonGesture)
|
||||
|
||||
category = QWidget()
|
||||
self.category_area.setWidget(category)
|
||||
self.layout_category = QGridLayout()
|
||||
category.setLayout(self.layout_category)
|
||||
id_ = 1
|
||||
cols = 2
|
||||
row = 0
|
||||
col = 0
|
||||
for value in self.values:
|
||||
if not value:
|
||||
continue
|
||||
if col > cols - 1:
|
||||
col = 0
|
||||
row += 1
|
||||
name_button = 'button_' + str(id_)
|
||||
|
||||
button = CustomButton(
|
||||
parent=self,
|
||||
id=name_button,
|
||||
icon=value['icon'],
|
||||
desc=value['name'],
|
||||
method='selected_method',
|
||||
target=str(id_),
|
||||
size=self.button_size,
|
||||
name_style='category_button'
|
||||
)
|
||||
button.setMaximumHeight(100)
|
||||
self.layout_category.addWidget(button, row, col)
|
||||
grid_buttons = GridButtons(self.parent, value['items'], cols,
|
||||
action=self.method_on_selected)
|
||||
scroll_area = QScrollArea()
|
||||
scroll_area.setWidgetResizable(True)
|
||||
scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
|
||||
scroll_area.setWidget(grid_buttons)
|
||||
QScroller.grabGesture(scroll_area, QScroller.LeftMouseButtonGesture)
|
||||
setattr(self, 'view_' + str(id_), scroll_area)
|
||||
col += 1
|
||||
id_ += 1
|
||||
|
||||
def action_back(self):
|
||||
self.setState({
|
||||
'layout_invisible': False
|
||||
})
|
||||
|
||||
def selected_method(self, args):
|
||||
self.setState({
|
||||
'layout_invisible': True,
|
||||
'button': args,
|
||||
})
|
|
@ -0,0 +1,192 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: UTF-8 -*-
|
||||
from decimal import Decimal
|
||||
from PyQt5.QtCore import Qt, pyqtSignal, QSize
|
||||
from PyQt5.QtWidgets import QWidget, QHBoxLayout, QFrame, QScroller,\
|
||||
QVBoxLayout, QPushButton, QLabel, QGridLayout, QDialog, QScrollArea
|
||||
|
||||
|
||||
def money(v):
|
||||
return '${:20,}'.format(int(v))
|
||||
|
||||
|
||||
class Separator(QFrame):
|
||||
|
||||
def __init__(self):
|
||||
QFrame.__init__(self)
|
||||
self.setLineWidth(1)
|
||||
self.setFrameShape(QFrame.HLine)
|
||||
|
||||
|
||||
class TLabel(QLabel):
|
||||
# Category Label
|
||||
|
||||
def __init__(self, key, id_, parent):
|
||||
QLabel.__init__(self, key)
|
||||
self.parent = parent
|
||||
self.setAlignment(Qt.AlignCenter)
|
||||
self.id = id_
|
||||
|
||||
def mouseDoubleClickEvent(self, qmouse_event):
|
||||
self.parent.setState({
|
||||
'layout_invisible': True,
|
||||
'view': self.id,
|
||||
})
|
||||
super(TLabel, self).mouseDoubleClickEvent(qmouse_event)
|
||||
|
||||
|
||||
class RLabel(QLabel):
|
||||
#Item Label
|
||||
|
||||
def __init__(self, name, idx):
|
||||
super(RLabel, self).__init__(name)
|
||||
self.idx = idx
|
||||
|
||||
def mouseDoubleClickEvent(self, qmouse_event):
|
||||
self.parent().action_selected(self.idx)
|
||||
super(RLabel, self).mouseDoubleClickEvent(qmouse_event)
|
||||
|
||||
|
||||
class List(QWidget):
|
||||
sigItem_selected = pyqtSignal(str)
|
||||
|
||||
def __init__(self, rows, num_cols, show_code, action):
|
||||
"""
|
||||
rows: a list of lists
|
||||
num_cols: number of columns?
|
||||
"""
|
||||
QWidget.__init__(self)
|
||||
self.layout_list = QGridLayout()
|
||||
self.setLayout(self.layout_list)
|
||||
self.rows = rows
|
||||
self.show_code = show_code
|
||||
self.action = action
|
||||
self.num_cols = num_cols
|
||||
self.layout_list.setVerticalSpacing(5)
|
||||
self.layout_list.setColumnStretch(1, 1)
|
||||
if rows:
|
||||
self.set_items(rows)
|
||||
|
||||
def action_selected(self, idx):
|
||||
self.action(idx) #.selected_method(idx)
|
||||
|
||||
def set_items(self, rows):
|
||||
self.rows = rows
|
||||
idx = 0
|
||||
self.layout_list.addWidget(Separator(), 0, 0, 1, self.num_cols)
|
||||
|
||||
for row in rows:
|
||||
idx += 1
|
||||
separator = Separator()
|
||||
for col in range(self.num_cols):
|
||||
val = row[col]
|
||||
if isinstance(val, Decimal):
|
||||
val = money(int(val))
|
||||
if not self.show_code:
|
||||
if col == 0:
|
||||
val = ''
|
||||
item = RLabel(val, idx=row[0])
|
||||
self.layout_list.addWidget(item, idx, col)
|
||||
idx += 1
|
||||
self.layout_list.addWidget(separator, idx, 0, 1, self.num_cols)
|
||||
self.layout_list.setRowStretch(idx + 1, 1)
|
||||
|
||||
|
||||
class MenuWindow(QDialog):
|
||||
|
||||
def __init__(self, parent, values, selected_method=None, title=None):
|
||||
"""
|
||||
parent: parent window
|
||||
values: is to list of list/tuples values for data model
|
||||
[('a' 'b', 'c'), ('d', 'e', 'f')...]
|
||||
on_selected: method to call when triggered the selection
|
||||
title: title of window
|
||||
"""
|
||||
super(MenuWindow, self).__init__(parent)
|
||||
|
||||
self.parent = parent
|
||||
self.values = values
|
||||
self.current_view = None
|
||||
if not title:
|
||||
title = self.tr('Menu...')
|
||||
self.setWindowTitle(title)
|
||||
self.resize(QSize(200, 400))
|
||||
self.show_code = False
|
||||
self.create_categories()
|
||||
self.create_widgets()
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
self.layout = QVBoxLayout()
|
||||
self.layout_buttons = QHBoxLayout()
|
||||
self.layout.addLayout(self.layout_buttons, 0)
|
||||
self.layout_buttons.addWidget(self.pushButtonOk)
|
||||
self.layout_buttons.addWidget(self.pushButtonBack)
|
||||
self.layout.addWidget(self.category, 0)
|
||||
self.setLayout(self.layout)
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
|
||||
self.create_connections()
|
||||
self.method_on_selected = getattr(self.parent, selected_method)
|
||||
|
||||
def setState(self, args):
|
||||
if args.get('layout_invisible'):
|
||||
self.category.hide()
|
||||
else:
|
||||
self.category.show()
|
||||
self.layout.addWidget(self.category)
|
||||
if self.current_view:
|
||||
self.current_view.hide()
|
||||
|
||||
if args.get('view'):
|
||||
view_id = args.get('view')
|
||||
if hasattr(self, 'view_' + str(view_id)):
|
||||
self.current_view = getattr(self, 'view_' + str(view_id))
|
||||
self.layout.addWidget(self.current_view)
|
||||
self.current_view.show()
|
||||
|
||||
def create_categories(self):
|
||||
# set the list model
|
||||
self.category = QWidget()
|
||||
self.layout_category = QVBoxLayout()
|
||||
self.category.setLayout(self.layout_category)
|
||||
id_ = 1
|
||||
self.layout_category.addWidget(Separator())
|
||||
for k, values in self.values.items():
|
||||
separator = Separator()
|
||||
label = TLabel(k, id_, self)
|
||||
self.layout_category.addWidget(label)
|
||||
self.layout_category.addWidget(separator)
|
||||
list_item = List(values, 3, self.show_code, self.selected_method)
|
||||
scroll_area = QScrollArea()
|
||||
scroll_area.setWidgetResizable(True)
|
||||
scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
|
||||
scroll_area.setWidget(list_item)
|
||||
QScroller.grabGesture(scroll_area, QScroller.LeftMouseButtonGesture)
|
||||
setattr(self, 'view_'+ str(id_), scroll_area)
|
||||
id_ += 1
|
||||
|
||||
def create_widgets(self):
|
||||
self.pushButtonOk = QPushButton(self.tr("&ACCEPT"))
|
||||
self.pushButtonOk.setAutoDefault(True)
|
||||
self.pushButtonOk.setDefault(False)
|
||||
self.pushButtonBack = QPushButton(self.tr("&BACK"))
|
||||
|
||||
def create_layout(self):
|
||||
pass
|
||||
|
||||
def create_connections(self):
|
||||
self.pushButtonOk.clicked.connect(self.action_close)
|
||||
self.pushButtonBack.clicked.connect(self.action_back)
|
||||
|
||||
def action_back(self):
|
||||
self.setState({
|
||||
'layout_invisible': False,
|
||||
})
|
||||
|
||||
def action_close(self):
|
||||
self.close()
|
||||
|
||||
def selected_method(self, args):
|
||||
if self.parent and self.selected_method:
|
||||
self.method_on_selected(args)
|
|
@ -0,0 +1,46 @@
|
|||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtWidgets import QHBoxLayout, QLabel
|
||||
|
||||
|
||||
__all__ = ['MessageBar']
|
||||
|
||||
|
||||
class MessageBar(QHBoxLayout):
|
||||
|
||||
def __init__(self):
|
||||
super(MessageBar, self).__init__()
|
||||
|
||||
self.type = 'ready'
|
||||
self.setObjectName('layout_info')
|
||||
self.setContentsMargins(0, 0, 0, 0)
|
||||
self.label_info = QLabel('', alignment=Qt.AlignCenter)
|
||||
self.label_info.setObjectName('label_message')
|
||||
self.addWidget(self.label_info, stretch=0)
|
||||
self.update_style()
|
||||
|
||||
def update_style(self):
|
||||
font_style = "color: #ffffff;"
|
||||
min_height = "min-height: 50px;"
|
||||
bgr_attr = "background-color: "
|
||||
if self.type == 'info':
|
||||
color = "rgba(80, 190, 220, 0.8);"
|
||||
elif self.type == 'warning':
|
||||
color = "rgba(223, 38, 38, 0.8);"
|
||||
elif self.type in ('question', 'response'):
|
||||
color = "rgba(64, 158, 19, 0.8);"
|
||||
else:
|
||||
# type must be error so show red color
|
||||
color = "rgba(210, 84, 168, 0.8);"
|
||||
bgr_style = bgr_attr + color
|
||||
self.label_info.setStyleSheet(font_style + bgr_style + min_height)
|
||||
|
||||
def set(self, msg, additional_info=None):
|
||||
type_, msg_string = self.stack_messages.get(msg)
|
||||
if additional_info:
|
||||
msg_string = msg_string % additional_info
|
||||
self.label_info.setText(msg_string)
|
||||
self.type = type_
|
||||
self.update_style()
|
||||
|
||||
def load_stack(self, messages):
|
||||
self.stack_messages = messages
|
|
@ -0,0 +1,242 @@
|
|||
|
||||
from PyQt5.QtCore import Qt, QAbstractTableModel, QModelIndex
|
||||
|
||||
|
||||
__all__ = ['Modules', 'TableModel', 'TrytonModel']
|
||||
|
||||
|
||||
class Modules(object):
|
||||
'Load/Set target modules on context of mainwindow'
|
||||
|
||||
def __init__(self, parent=None, connection=None):
|
||||
self.parent = parent
|
||||
self.conn = connection
|
||||
|
||||
def set_models(self, mdict):
|
||||
for val in mdict:
|
||||
if val:
|
||||
model = TrytonModel(self.conn, val['model'],
|
||||
val['fields'], val.get('methods'))
|
||||
setattr(self.parent, val['name'], model)
|
||||
|
||||
def set_model(self, mdict):
|
||||
model = TrytonModel(self.conn, mdict['model'],
|
||||
mdict['fields'], mdict.get('methods'))
|
||||
return model
|
||||
|
||||
def permission_delete(self, target, ctx_groups):
|
||||
""" Check if the user has permissions for delete records """
|
||||
# FIXME
|
||||
model_data = TrytonModel(self.conn, 'ir.model',
|
||||
('values', 'fs_id'), [])
|
||||
groups_ids = model_data.setDomain([
|
||||
('fs_id', '=', target),
|
||||
])
|
||||
if groups_ids:
|
||||
group_id = eval(groups_ids[0]['values'])[0][1]
|
||||
if group_id in ctx_groups:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class TableModel(QAbstractTableModel):
|
||||
|
||||
def __init__(self, model, fields):
|
||||
super(TableModel, self).__init__()
|
||||
self._fields = fields
|
||||
self.model = model
|
||||
self._data = []
|
||||
|
||||
def reset(self):
|
||||
self.beginResetModel()
|
||||
self._data = []
|
||||
self.endResetModel()
|
||||
|
||||
def add_record(self, rec):
|
||||
length = len(self._data)
|
||||
self.beginInsertRows(QModelIndex(), length, length)
|
||||
self._data.append(rec)
|
||||
self.endInsertRows()
|
||||
return rec
|
||||
|
||||
def get_id(self):
|
||||
pass
|
||||
|
||||
def removeId(self, row, mdl_idx):
|
||||
self.beginRemoveRows(mdl_idx, row, row)
|
||||
id_ = self._data[row].get('id')
|
||||
self._data.pop(row)
|
||||
self.endRemoveRows()
|
||||
return id_
|
||||
|
||||
def deleteRecords(self, ids):
|
||||
pass
|
||||
|
||||
def rowCount(self, parent=None):
|
||||
return len(self._data)
|
||||
|
||||
def columnCount(self, parent=None):
|
||||
return len(self._fields)
|
||||
|
||||
def get_data(self, index):
|
||||
raw_value = self._data[index.row()]
|
||||
return raw_value
|
||||
|
||||
def data(self, index, role, field_name='name'):
|
||||
field = self._fields[index.column()]
|
||||
|
||||
if role == Qt.DisplayRole:
|
||||
index_row = self._data[index.row()]
|
||||
if not index_row.get(field.get(field_name)):
|
||||
return None
|
||||
|
||||
raw_value = index_row[field[field_name]]
|
||||
digits = None
|
||||
if field.get('digits'):
|
||||
digits = 0
|
||||
target_field = field.get('digits')[0]
|
||||
|
||||
if index_row.get(target_field):
|
||||
target = index_row[target_field]
|
||||
group_digits = field.get('digits')[1]
|
||||
if group_digits.get(target):
|
||||
digits = group_digits.get(target)
|
||||
|
||||
if not raw_value:
|
||||
return None
|
||||
|
||||
if field.get('format'):
|
||||
field_format = field['format']
|
||||
if digits or digits == 0:
|
||||
field_format = field['format'] % str(digits)
|
||||
if isinstance(raw_value, str):
|
||||
raw_value = float(raw_value)
|
||||
fmt_value = field_format.format(raw_value)
|
||||
else:
|
||||
fmt_value = raw_value
|
||||
return fmt_value
|
||||
|
||||
elif role == Qt.TextAlignmentRole:
|
||||
align = Qt.AlignmentFlag(Qt.AlignVCenter | field['align'])
|
||||
return align
|
||||
else:
|
||||
return None
|
||||
|
||||
def get_sum(self, field_target):
|
||||
res = sum([d[field_target] for d in self._data])
|
||||
return res
|
||||
|
||||
def update_record(self, rec, pos=None):
|
||||
if pos is None:
|
||||
pos = 0
|
||||
for d in self._data:
|
||||
if d['id'] == rec['id']:
|
||||
break
|
||||
pos += 1
|
||||
|
||||
self._data.pop(pos)
|
||||
self._data.insert(pos, rec)
|
||||
start_pos = self.index(pos, 0)
|
||||
end_pos = self.index(pos, len(self._fields) - 1)
|
||||
self.dataChanged.emit(start_pos, end_pos)
|
||||
return rec
|
||||
|
||||
def headerData(self, section, orientation, role):
|
||||
""" Set the headers to be displayed. """
|
||||
if role != Qt.DisplayRole:
|
||||
return None
|
||||
elements = [f['description'] for f in self._fields]
|
||||
if orientation == Qt.Horizontal:
|
||||
for i in range(len(elements)):
|
||||
if section == i:
|
||||
return elements[i]
|
||||
return None
|
||||
|
||||
|
||||
class TrytonModel(object):
|
||||
'Model interface for Tryton'
|
||||
|
||||
def __init__(self, connection, model, fields, methods=None):
|
||||
self._fields = fields
|
||||
self._methods = methods
|
||||
self._proxy = connection.get_proxy(model)
|
||||
self._context = connection.context
|
||||
self._data = []
|
||||
if self._methods:
|
||||
self.setMethods()
|
||||
|
||||
def setFields(self, fields):
|
||||
self._fields = fields
|
||||
|
||||
def setMethods(self):
|
||||
for name in self._methods:
|
||||
if not hasattr(self._proxy, name):
|
||||
continue
|
||||
setattr(self, name, getattr(self._proxy, name))
|
||||
|
||||
def find(self, domain, limit=None, order=None, context=None):
|
||||
if context:
|
||||
self._context.update(context)
|
||||
return self._setDomain(domain, limit, order)
|
||||
|
||||
def _setDomain(self, domain, limit=None, order=None):
|
||||
if domain and isinstance(domain[0], int):
|
||||
operator = 'in'
|
||||
operand = domain
|
||||
if len(domain) == 1:
|
||||
operator = '='
|
||||
operand = domain[0]
|
||||
domain = [('id', operator, operand)]
|
||||
if not order:
|
||||
order = [('id', 'ASC')]
|
||||
self._data = self._search_read(domain,
|
||||
fields_names=self._fields, limit=limit, order=order)
|
||||
return self._data
|
||||
|
||||
def _search_read(self, domain, offset=0, limit=None, order=None,
|
||||
fields_names=None):
|
||||
if order:
|
||||
ids = self._proxy.search(domain, offset, limit, order, self._context)
|
||||
records = self._proxy.read(ids, fields_names, self._context)
|
||||
rec_dict = {}
|
||||
for rec in records:
|
||||
rec_dict[rec['id']] = rec
|
||||
res = []
|
||||
for id_ in ids:
|
||||
res.append(rec_dict[id_])
|
||||
else:
|
||||
res = self._proxy.search_read(domain, offset, limit, order,
|
||||
fields_names, self._context)
|
||||
return res
|
||||
|
||||
def read(self, ids, fields_names=None):
|
||||
records = self._proxy.read(ids, fields_names, self._context)
|
||||
return records
|
||||
|
||||
def _search(self, domain, offset=0, limit=None, order=None):
|
||||
pass
|
||||
|
||||
def deleteRecords(self, ids):
|
||||
self._proxy.delete(ids, self._context)
|
||||
|
||||
def getRecord(self, id_):
|
||||
records = self.setDomain([('id', '=', id_)])
|
||||
if records:
|
||||
return records[0]
|
||||
|
||||
def update(self, id, pos=False):
|
||||
rec, = self._search_read([('id', '=', id)],
|
||||
fields_names=[x['name'] for x in self._fields])
|
||||
return rec
|
||||
|
||||
def create(self, values):
|
||||
records = self._proxy.create([values], self._context)
|
||||
return records[0]
|
||||
|
||||
def write(self, ids, values):
|
||||
self._proxy.write(ids, values, self._context)
|
||||
|
||||
def method(self, name):
|
||||
# TODO Add reuse self context (*values, self._context)
|
||||
print('Se ejecuta este metodo...', name)
|
||||
return getattr(self._proxy, name)
|
|
@ -0,0 +1,50 @@
|
|||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from functools import partial
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtWidgets import QLabel, QPushButton
|
||||
|
||||
css_small = 'product_button_small.css'
|
||||
css_high = 'product_button_hd.css'
|
||||
root_dir = str(Path(__file__).parent.parent)
|
||||
|
||||
__all__ = ['ProductButton']
|
||||
|
||||
|
||||
class ProductButton(QPushButton):
|
||||
|
||||
def __init__(self, parent, id, text_up, text_bottom, method, target, size='small'):
|
||||
super(ProductButton, self).__init__()
|
||||
|
||||
self.id = id
|
||||
styles = []
|
||||
if size == 'small':
|
||||
css_file_screen = css_small
|
||||
else:
|
||||
css_file_screen = css_high
|
||||
|
||||
css_file = os.path.join(root_dir, 'css', css_file_screen)
|
||||
|
||||
with open(css_file, 'r') as infile:
|
||||
styles.append(infile.read())
|
||||
|
||||
self.setStyleSheet(''.join(styles))
|
||||
self.setObjectName('product_button')
|
||||
if len(text_up) > 29:
|
||||
text_up = text_up[0:29]
|
||||
|
||||
label1 = QLabel(text_up, self)
|
||||
label1.setWordWrap(True)
|
||||
label1.setAlignment(Qt.AlignCenter | Qt.AlignCenter)
|
||||
label1.setObjectName('product_label_up')
|
||||
|
||||
label2 = QLabel(text_bottom, self)
|
||||
label2.setAlignment(Qt.AlignCenter | Qt.AlignCenter)
|
||||
label2.setObjectName('product_label_bottom')
|
||||
|
||||
method = getattr(parent, method)
|
||||
if target:
|
||||
method = partial(method, target)
|
||||
self.clicked.connect(method)
|
|
@ -0,0 +1,690 @@
|
|||
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
# this repository contains the full copyright notices and license terms.
|
||||
try:
|
||||
import simplejson as json
|
||||
except ImportError:
|
||||
import json
|
||||
import datetime
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from functools import reduce, wraps
|
||||
|
||||
__all__ = ['PYSONEncoder', 'PYSONDecoder', 'Eval', 'Not', 'Bool', 'And', 'Or',
|
||||
'Equal', 'Greater', 'Less', 'If', 'Get', 'In', 'Date', 'DateTime', 'Len']
|
||||
|
||||
|
||||
def reduced_type(types):
|
||||
types = types.copy()
|
||||
for k, r in [(long, int), (str, basestring), (unicode, basestring)]:
|
||||
if k in types:
|
||||
types.remove(k)
|
||||
types.add(r)
|
||||
return types
|
||||
|
||||
|
||||
def reduce_type(func):
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
return reduced_type(func(*args, **kwargs))
|
||||
return wrapper
|
||||
|
||||
|
||||
class PYSON(object):
|
||||
|
||||
def pyson(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def types(self):
|
||||
raise NotImplementedError
|
||||
|
||||
@staticmethod
|
||||
def eval(dct, context):
|
||||
raise NotImplementedError
|
||||
|
||||
def __invert__(self):
|
||||
if self.types() != set([bool]):
|
||||
return Not(Bool(self))
|
||||
else:
|
||||
return Not(self)
|
||||
|
||||
def __and__(self, other):
|
||||
if (isinstance(other, PYSON)
|
||||
and other.types() != set([bool])):
|
||||
other = Bool(other)
|
||||
if (isinstance(self, And)
|
||||
and not isinstance(self, Or)):
|
||||
self._statements.append(other)
|
||||
return self
|
||||
if self.types() != set([bool]):
|
||||
return And(Bool(self), other)
|
||||
else:
|
||||
return And(self, other)
|
||||
|
||||
def __or__(self, other):
|
||||
if (isinstance(other, PYSON)
|
||||
and other.types() != set([bool])):
|
||||
other = Bool(other)
|
||||
if isinstance(self, Or):
|
||||
self._statements.append(other)
|
||||
return self
|
||||
if self.types() != set([bool]):
|
||||
return Or(Bool(self), other)
|
||||
else:
|
||||
return Or(self, other)
|
||||
|
||||
def __eq__(self, other):
|
||||
return Equal(self, other)
|
||||
|
||||
def __ne__(self, other):
|
||||
return Not(Equal(self, other))
|
||||
|
||||
def __gt__(self, other):
|
||||
return Greater(self, other)
|
||||
|
||||
def __ge__(self, other):
|
||||
return Greater(self, other, True)
|
||||
|
||||
def __lt__(self, other):
|
||||
return Less(self, other)
|
||||
|
||||
def __le__(self, other):
|
||||
return Less(self, other, True)
|
||||
|
||||
def get(self, k, d=''):
|
||||
return Get(self, k, d)
|
||||
|
||||
def in_(self, obj):
|
||||
return In(self, obj)
|
||||
|
||||
def contains(self, k):
|
||||
return In(k, self)
|
||||
|
||||
def __repr__(self):
|
||||
klass = self.__class__.__name__
|
||||
return '%s(%s)' % (klass, ', '.join(map(repr, self.__repr_params__)))
|
||||
|
||||
@property
|
||||
def __repr_params__(self):
|
||||
return NotImplementedError
|
||||
|
||||
|
||||
class PYSONEncoder(json.JSONEncoder):
|
||||
|
||||
def default(self, obj):
|
||||
if isinstance(obj, PYSON):
|
||||
return obj.pyson()
|
||||
elif isinstance(obj, datetime.date):
|
||||
if isinstance(obj, datetime.datetime):
|
||||
return DateTime(obj.year, obj.month, obj.day,
|
||||
obj.hour, obj.minute, obj.second, obj.microsecond
|
||||
).pyson()
|
||||
else:
|
||||
return Date(obj.year, obj.month, obj.day).pyson()
|
||||
return super(PYSONEncoder, self).default(obj)
|
||||
|
||||
|
||||
class PYSONDecoder(json.JSONDecoder):
|
||||
|
||||
def __init__(self, context=None, noeval=False):
|
||||
self.__context = context or {}
|
||||
self.noeval = noeval
|
||||
super(PYSONDecoder, self).__init__(object_hook=self._object_hook)
|
||||
|
||||
def _object_hook(self, dct):
|
||||
if '__class__' in dct:
|
||||
klass = CONTEXT.get(dct['__class__'])
|
||||
if klass:
|
||||
if not self.noeval:
|
||||
return klass.eval(dct, self.__context)
|
||||
else:
|
||||
dct = dct.copy()
|
||||
del dct['__class__']
|
||||
return klass(**dct)
|
||||
return dct
|
||||
|
||||
|
||||
class Eval(PYSON):
|
||||
|
||||
def __init__(self, v, d=''):
|
||||
super(Eval, self).__init__()
|
||||
self._value = v
|
||||
self._default = d
|
||||
|
||||
@property
|
||||
def __repr_params__(self):
|
||||
return self._value, self._default
|
||||
|
||||
def pyson(self):
|
||||
return {
|
||||
'__class__': 'Eval',
|
||||
'v': self._value,
|
||||
'd': self._default,
|
||||
}
|
||||
|
||||
@reduce_type
|
||||
def types(self):
|
||||
if isinstance(self._default, PYSON):
|
||||
return self._default.types()
|
||||
else:
|
||||
return set([type(self._default)])
|
||||
|
||||
@staticmethod
|
||||
def eval(dct, context):
|
||||
return context.get(dct['v'], dct['d'])
|
||||
|
||||
|
||||
class Not(PYSON):
|
||||
|
||||
def __init__(self, v):
|
||||
super(Not, self).__init__()
|
||||
if isinstance(v, PYSON):
|
||||
assert v.types() == set([bool]), 'value must be boolean'
|
||||
else:
|
||||
assert isinstance(v, bool), 'value must be boolean'
|
||||
self._value = v
|
||||
|
||||
@property
|
||||
def __repr_params__(self):
|
||||
return (self._value,)
|
||||
|
||||
def pyson(self):
|
||||
return {
|
||||
'__class__': 'Not',
|
||||
'v': self._value,
|
||||
}
|
||||
|
||||
def types(self):
|
||||
return set([bool])
|
||||
|
||||
@staticmethod
|
||||
def eval(dct, context):
|
||||
return not dct['v']
|
||||
|
||||
|
||||
class Bool(PYSON):
|
||||
|
||||
def __init__(self, v):
|
||||
super(Bool, self).__init__()
|
||||
self._value = v
|
||||
|
||||
@property
|
||||
def __repr_params__(self):
|
||||
return (self._value,)
|
||||
|
||||
def pyson(self):
|
||||
return {
|
||||
'__class__': 'Bool',
|
||||
'v': self._value,
|
||||
}
|
||||
|
||||
def types(self):
|
||||
return set([bool])
|
||||
|
||||
@staticmethod
|
||||
def eval(dct, context):
|
||||
return bool(dct['v'])
|
||||
|
||||
|
||||
class And(PYSON):
|
||||
|
||||
def __init__(self, *statements, **kwargs):
|
||||
super(And, self).__init__()
|
||||
statements = list(statements) + kwargs.get('s', [])
|
||||
for statement in statements:
|
||||
if isinstance(statement, PYSON):
|
||||
assert statement.types() == set([bool]), \
|
||||
'statement must be boolean'
|
||||
else:
|
||||
assert isinstance(statement, bool), \
|
||||
'statement must be boolean'
|
||||
assert len(statements) >= 2, 'must have at least 2 statements'
|
||||
self._statements = statements
|
||||
|
||||
@property
|
||||
def __repr_params__(self):
|
||||
return tuple(self._statements)
|
||||
|
||||
def pyson(self):
|
||||
return {
|
||||
'__class__': 'And',
|
||||
's': self._statements,
|
||||
}
|
||||
|
||||
def types(self):
|
||||
return set([bool])
|
||||
|
||||
@staticmethod
|
||||
def eval(dct, context):
|
||||
return bool(reduce(lambda x, y: x and y, dct['s']))
|
||||
|
||||
|
||||
class Or(And):
|
||||
|
||||
def pyson(self):
|
||||
res = super(Or, self).pyson()
|
||||
res['__class__'] = 'Or'
|
||||
return res
|
||||
|
||||
@staticmethod
|
||||
def eval(dct, context):
|
||||
return bool(reduce(lambda x, y: x or y, dct['s']))
|
||||
|
||||
|
||||
class Equal(PYSON):
|
||||
|
||||
def __init__(self, s1, s2):
|
||||
statement1, statement2 = s1, s2
|
||||
super(Equal, self).__init__()
|
||||
if isinstance(statement1, PYSON):
|
||||
types1 = statement1.types()
|
||||
else:
|
||||
types1 = reduced_type(set([type(s1)]))
|
||||
if isinstance(statement2, PYSON):
|
||||
types2 = statement2.types()
|
||||
else:
|
||||
types2 = reduced_type(set([type(s2)]))
|
||||
assert types1 == types2, 'statements must have the same type'
|
||||
self._statement1 = statement1
|
||||
self._statement2 = statement2
|
||||
|
||||
@property
|
||||
def __repr_params__(self):
|
||||
return (self._statement1, self._statement2)
|
||||
|
||||
def pyson(self):
|
||||
return {
|
||||
'__class__': 'Equal',
|
||||
's1': self._statement1,
|
||||
's2': self._statement2,
|
||||
}
|
||||
|
||||
def types(self):
|
||||
return set([bool])
|
||||
|
||||
@staticmethod
|
||||
def eval(dct, context):
|
||||
return dct['s1'] == dct['s2']
|
||||
|
||||
|
||||
class Greater(PYSON):
|
||||
|
||||
def __init__(self, s1, s2, e=False):
|
||||
statement1, statement2, equal = s1, s2, e
|
||||
super(Greater, self).__init__()
|
||||
for i in (statement1, statement2):
|
||||
if isinstance(i, PYSON):
|
||||
assert i.types().issubset(set([int, long, float])), \
|
||||
'statement must be an integer or a float'
|
||||
else:
|
||||
assert isinstance(i, (int, long, float)), \
|
||||
'statement must be an integer or a float'
|
||||
if isinstance(equal, PYSON):
|
||||
assert equal.types() == set([bool])
|
||||
else:
|
||||
assert isinstance(equal, bool)
|
||||
self._statement1 = statement1
|
||||
self._statement2 = statement2
|
||||
self._equal = equal
|
||||
|
||||
@property
|
||||
def __repr_params__(self):
|
||||
return (self._statement1, self._statement2, self._equal)
|
||||
|
||||
def pyson(self):
|
||||
return {
|
||||
'__class__': 'Greater',
|
||||
's1': self._statement1,
|
||||
's2': self._statement2,
|
||||
'e': self._equal,
|
||||
}
|
||||
|
||||
def types(self):
|
||||
return set([bool])
|
||||
|
||||
@staticmethod
|
||||
def _convert(dct):
|
||||
for i in ('s1', 's2'):
|
||||
if not isinstance(dct[i], (int, long, float)):
|
||||
dct = dct.copy()
|
||||
dct[i] = float(dct[i])
|
||||
return dct
|
||||
|
||||
@staticmethod
|
||||
def eval(dct, context):
|
||||
dct = Greater._convert(dct)
|
||||
if dct['e']:
|
||||
return dct['s1'] >= dct['s2']
|
||||
else:
|
||||
return dct['s1'] > dct['s2']
|
||||
|
||||
|
||||
class Less(Greater):
|
||||
|
||||
def pyson(self):
|
||||
res = super(Less, self).pyson()
|
||||
res['__class__'] = 'Less'
|
||||
return res
|
||||
|
||||
@staticmethod
|
||||
def eval(dct, context):
|
||||
dct = Less._convert(dct)
|
||||
if dct['e']:
|
||||
return dct['s1'] <= dct['s2']
|
||||
else:
|
||||
return dct['s1'] < dct['s2']
|
||||
|
||||
|
||||
class If(PYSON):
|
||||
|
||||
def __init__(self, c, t, e=None):
|
||||
condition, then_statement, else_statement = c, t, e
|
||||
super(If, self).__init__()
|
||||
if isinstance(condition, PYSON):
|
||||
assert condition.types() == set([bool]), \
|
||||
'condition must be boolean'
|
||||
else:
|
||||
assert isinstance(condition, bool), 'condition must be boolean'
|
||||
if isinstance(then_statement, PYSON):
|
||||
then_types = then_statement.types()
|
||||
else:
|
||||
then_types = reduced_type(set([type(then_statement)]))
|
||||
if isinstance(else_statement, PYSON):
|
||||
else_types = else_statement.types()
|
||||
else:
|
||||
else_types = reduced_type(set([type(else_statement)]))
|
||||
assert then_types == else_types, \
|
||||
'then and else statements must be the same type'
|
||||
self._condition = condition
|
||||
self._then_statement = then_statement
|
||||
self._else_statement = else_statement
|
||||
|
||||
@property
|
||||
def __repr_params__(self):
|
||||
return (self._condition, self._then_statement, self._else_statement)
|
||||
|
||||
def pyson(self):
|
||||
return {
|
||||
'__class__': 'If',
|
||||
'c': self._condition,
|
||||
't': self._then_statement,
|
||||
'e': self._else_statement,
|
||||
}
|
||||
|
||||
@reduce_type
|
||||
def types(self):
|
||||
if isinstance(self._then_statement, PYSON):
|
||||
return self._then_statement.types()
|
||||
else:
|
||||
return set([type(self._then_statement)])
|
||||
|
||||
@staticmethod
|
||||
def eval(dct, context):
|
||||
if dct['c']:
|
||||
return dct['t']
|
||||
else:
|
||||
return dct['e']
|
||||
|
||||
|
||||
class Get(PYSON):
|
||||
|
||||
def __init__(self, v, k, d=''):
|
||||
obj, key, default = v, k, d
|
||||
super(Get, self).__init__()
|
||||
if isinstance(obj, PYSON):
|
||||
assert obj.types() == set([dict]), 'obj must be a dict'
|
||||
else:
|
||||
assert isinstance(obj, dict), 'obj must be a dict'
|
||||
self._obj = obj
|
||||
if isinstance(key, PYSON):
|
||||
assert key.types() == set([basestring]), 'key must be a string'
|
||||
else:
|
||||
assert isinstance(key, basestring), 'key must be a string'
|
||||
self._key = key
|
||||
self._default = default
|
||||
|
||||
@property
|
||||
def __repr_params__(self):
|
||||
return (self._obj, self._key, self._default)
|
||||
|
||||
def pyson(self):
|
||||
return {
|
||||
'__class__': 'Get',
|
||||
'v': self._obj,
|
||||
'k': self._key,
|
||||
'd': self._default,
|
||||
}
|
||||
|
||||
@reduce_type
|
||||
def types(self):
|
||||
if isinstance(self._default, PYSON):
|
||||
return self._default.types()
|
||||
else:
|
||||
return set([type(self._default)])
|
||||
|
||||
@staticmethod
|
||||
def eval(dct, context):
|
||||
return dct['v'].get(dct['k'], dct['d'])
|
||||
|
||||
|
||||
class In(PYSON):
|
||||
|
||||
def __init__(self, k, v):
|
||||
key, obj = k, v
|
||||
super(In, self).__init__()
|
||||
if isinstance(key, PYSON):
|
||||
assert key.types().issubset(set([basestring, int])), \
|
||||
'key must be a string or an integer or a long'
|
||||
else:
|
||||
assert isinstance(key, (basestring, int, long)), \
|
||||
'key must be a string or an integer or a long'
|
||||
if isinstance(obj, PYSON):
|
||||
assert obj.types().issubset(set([dict, list])), \
|
||||
'obj must be a dict or a list'
|
||||
if obj.types() == set([dict]):
|
||||
assert isinstance(key, basestring), 'key must be a string'
|
||||
else:
|
||||
assert isinstance(obj, (dict, list))
|
||||
if isinstance(obj, dict):
|
||||
assert isinstance(key, basestring), 'key must be a string'
|
||||
self._key = key
|
||||
self._obj = obj
|
||||
|
||||
@property
|
||||
def __repr_params__(self):
|
||||
return (self._key, self._obj)
|
||||
|
||||
def pyson(self):
|
||||
return {
|
||||
'__class__': 'In',
|
||||
'k': self._key,
|
||||
'v': self._obj,
|
||||
}
|
||||
|
||||
def types(self):
|
||||
return set([bool])
|
||||
|
||||
@staticmethod
|
||||
def eval(dct, context):
|
||||
return dct['k'] in dct['v']
|
||||
|
||||
|
||||
class Date(PYSON):
|
||||
|
||||
def __init__(self, year=None, month=None, day=None,
|
||||
delta_years=0, delta_months=0, delta_days=0, **kwargs):
|
||||
year = kwargs.get('y', year)
|
||||
month = kwargs.get('M', month)
|
||||
day = kwargs.get('d', day)
|
||||
delta_years = kwargs.get('dy', delta_years)
|
||||
delta_months = kwargs.get('dM', delta_months)
|
||||
delta_days = kwargs.get('dd', delta_days)
|
||||
super(Date, self).__init__()
|
||||
for i in (year, month, day, delta_years, delta_months, delta_days):
|
||||
if isinstance(i, PYSON):
|
||||
assert i.types().issubset(set([int, long, type(None)])), \
|
||||
'%s must be an integer or None' % (i,)
|
||||
else:
|
||||
assert isinstance(i, (int, long, type(None))), \
|
||||
'%s must be an integer or None' % (i,)
|
||||
self._year = year
|
||||
self._month = month
|
||||
self._day = day
|
||||
self._delta_years = delta_years
|
||||
self._delta_months = delta_months
|
||||
self._delta_days = delta_days
|
||||
|
||||
@property
|
||||
def __repr_params__(self):
|
||||
return (self._year, self._month, self._day,
|
||||
self._delta_years, self._delta_months, self._delta_days)
|
||||
|
||||
def pyson(self):
|
||||
return {
|
||||
'__class__': 'Date',
|
||||
'y': self._year,
|
||||
'M': self._month,
|
||||
'd': self._day,
|
||||
'dy': self._delta_years,
|
||||
'dM': self._delta_months,
|
||||
'dd': self._delta_days,
|
||||
}
|
||||
|
||||
def types(self):
|
||||
return set([datetime.date])
|
||||
|
||||
@staticmethod
|
||||
def eval(dct, context):
|
||||
return datetime.date.today() + relativedelta(
|
||||
year=dct['y'],
|
||||
month=dct['M'],
|
||||
day=dct['d'],
|
||||
years=dct['dy'],
|
||||
months=dct['dM'],
|
||||
days=dct['dd'],
|
||||
)
|
||||
|
||||
|
||||
class DateTime(Date):
|
||||
|
||||
def __init__(self, year=None, month=None, day=None,
|
||||
hour=None, minute=None, second=None, microsecond=None,
|
||||
delta_years=0, delta_months=0, delta_days=0,
|
||||
delta_hours=0, delta_minutes=0, delta_seconds=0,
|
||||
delta_microseconds=0, **kwargs):
|
||||
hour = kwargs.get('h', hour)
|
||||
minute = kwargs.get('m', minute)
|
||||
second = kwargs.get('s', second)
|
||||
microsecond = kwargs.get('ms', microsecond)
|
||||
delta_hours = kwargs.get('dh', delta_hours)
|
||||
delta_minutes = kwargs.get('dm', delta_minutes)
|
||||
delta_seconds = kwargs.get('ds', delta_seconds)
|
||||
delta_microseconds = kwargs.get('dms', delta_microseconds)
|
||||
super(DateTime, self).__init__(year=year, month=month, day=day,
|
||||
delta_years=delta_years, delta_months=delta_months,
|
||||
delta_days=delta_days, **kwargs)
|
||||
for i in (hour, minute, second, microsecond,
|
||||
delta_hours, delta_minutes, delta_seconds, delta_microseconds):
|
||||
if isinstance(i, PYSON):
|
||||
assert i.types() == set([int, type(None)]), \
|
||||
'%s must be an integer or None' % (i,)
|
||||
else:
|
||||
assert isinstance(i, (int, long, type(None))), \
|
||||
'%s must be an integer or None' % (i,)
|
||||
self._hour = hour
|
||||
self._minute = minute
|
||||
self._second = second
|
||||
self._microsecond = microsecond
|
||||
self._delta_hours = delta_hours
|
||||
self._delta_minutes = delta_minutes
|
||||
self._delta_seconds = delta_seconds
|
||||
self._delta_microseconds = delta_microseconds
|
||||
|
||||
@property
|
||||
def __repr_params__(self):
|
||||
date_params = super(DateTime, self).__repr_params__
|
||||
return (date_params[:3]
|
||||
+ (self._hour, self._minute, self._second, self._microsecond)
|
||||
+ date_params[3:]
|
||||
+ (self._delta_hours, self._delta_minutes, self._delta_seconds,
|
||||
self._delta_microseconds))
|
||||
|
||||
def pyson(self):
|
||||
res = super(DateTime, self).pyson()
|
||||
res['__class__'] = 'DateTime'
|
||||
res['h'] = self._hour
|
||||
res['m'] = self._minute
|
||||
res['s'] = self._second
|
||||
res['ms'] = self._microsecond
|
||||
res['dh'] = self._delta_hours
|
||||
res['dm'] = self._delta_minutes
|
||||
res['ds'] = self._delta_seconds
|
||||
res['dms'] = self._delta_microseconds
|
||||
return res
|
||||
|
||||
def types(self):
|
||||
return set([datetime.datetime])
|
||||
|
||||
@staticmethod
|
||||
def eval(dct, context):
|
||||
return datetime.datetime.now() + relativedelta(
|
||||
year=dct['y'],
|
||||
month=dct['M'],
|
||||
day=dct['d'],
|
||||
hour=dct['h'],
|
||||
minute=dct['m'],
|
||||
second=dct['s'],
|
||||
microsecond=dct['ms'],
|
||||
years=dct['dy'],
|
||||
months=dct['dM'],
|
||||
days=dct['dd'],
|
||||
hours=dct['dh'],
|
||||
minutes=dct['dm'],
|
||||
seconds=dct['ds'],
|
||||
microseconds=dct['dms'],
|
||||
)
|
||||
|
||||
|
||||
class Len(PYSON):
|
||||
|
||||
def __init__(self, v):
|
||||
super(Len, self).__init__()
|
||||
if isinstance(v, PYSON):
|
||||
assert v.types().issubset(set([dict, list, basestring])), \
|
||||
'value must be a dict or a list or a string'
|
||||
else:
|
||||
assert isinstance(v, (dict, list, basestring)), \
|
||||
'value must be a dict or list or a string'
|
||||
self._value = v
|
||||
|
||||
@property
|
||||
def __repr_params__(self):
|
||||
return (self._value,)
|
||||
|
||||
def pyson(self):
|
||||
return {
|
||||
'__class__': 'Len',
|
||||
'v': self._value,
|
||||
}
|
||||
|
||||
def types(self):
|
||||
return set([int])
|
||||
|
||||
@staticmethod
|
||||
def eval(dct, context):
|
||||
return len(dct['v'])
|
||||
|
||||
CONTEXT = {
|
||||
'Eval': Eval,
|
||||
'Not': Not,
|
||||
'Bool': Bool,
|
||||
'And': And,
|
||||
'Or': Or,
|
||||
'Equal': Equal,
|
||||
'Greater': Greater,
|
||||
'Less': Less,
|
||||
'If': If,
|
||||
'Get': Get,
|
||||
'In': In,
|
||||
'Date': Date,
|
||||
'DateTime': DateTime,
|
||||
'Len': Len,
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtGui import QStandardItemModel, QStandardItem
|
||||
|
||||
|
||||
def get_simple_model(obj, data, header=[]):
|
||||
model = QStandardItemModel(0, len(header), obj)
|
||||
if header:
|
||||
i = 0
|
||||
for head_name in header:
|
||||
model.setHeaderData(i, Qt.Horizontal, head_name)
|
||||
i += 1
|
||||
_insert_items(model, data)
|
||||
return model
|
||||
|
||||
|
||||
def _insert_items(model, data):
|
||||
for d in data:
|
||||
row = []
|
||||
for val in d:
|
||||
itemx = QStandardItem(str(val))
|
||||
itemx.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable)
|
||||
row.append(itemx)
|
||||
model.appendRow(row)
|
||||
model.sort(0, Qt.AscendingOrder)
|
||||
|
||||
|
||||
def set_selection_model(tryton_model, args):
|
||||
pass
|
|
@ -0,0 +1,55 @@
|
|||
# This file is part of Neo. The COPYRIGHT file at the top level of
|
||||
# this repository contains the full copyright notices and license terms.
|
||||
|
||||
import logging
|
||||
import socket
|
||||
from .jsonrpc import ServerProxy, Fault
|
||||
|
||||
CONNECTION = None
|
||||
_USER = None
|
||||
_USERNAME = ''
|
||||
_HOST = ''
|
||||
_PORT = None
|
||||
_DATABASE = ''
|
||||
CONTEXT = {}
|
||||
|
||||
|
||||
def server_version(host, port):
|
||||
try:
|
||||
connection = ServerProxy(host, port)
|
||||
logging.getLogger(__name__).info(
|
||||
'common.server.version(None, None)')
|
||||
result = connection.common.server.version()
|
||||
logging.getLogger(__name__).debug(repr(result))
|
||||
return result
|
||||
except (Fault, socket.error):
|
||||
raise
|
||||
|
||||
|
||||
def _execute(conn, *args):
|
||||
global CONNECTION, _USER
|
||||
name = '.'.join(args[:3])
|
||||
args = args[3:]
|
||||
result = getattr(conn.server, name)(*args)
|
||||
return result
|
||||
|
||||
def execute(conn, *args):
|
||||
return _execute(conn, *args)
|
||||
|
||||
|
||||
class RPCProgress(object):
|
||||
|
||||
def __init__(self, conn, method, args):
|
||||
self.method = method
|
||||
self.args = args
|
||||
self.conn = conn
|
||||
self.res = None
|
||||
self.error = False
|
||||
self.exception = None
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
res = execute(self.conn, *self.args)
|
||||
except:
|
||||
print('RPC progress... Unknown exception')
|
||||
return res
|
|
@ -0,0 +1,459 @@
|
|||
import os
|
||||
from operator import itemgetter
|
||||
from datetime import timedelta
|
||||
|
||||
from PyQt5.QtCore import Qt, QVariant, QAbstractTableModel, \
|
||||
pyqtSignal, QModelIndex, QSize
|
||||
from PyQt5.QtWidgets import QTableView, QVBoxLayout, \
|
||||
QAbstractItemView, QLineEdit, QDialog, QLabel, QScroller, \
|
||||
QHBoxLayout, QScrollArea, QItemDelegate
|
||||
from PyQt5.QtGui import QPixmap, QIcon
|
||||
|
||||
from neox.commons.buttons import ActionButton
|
||||
|
||||
__all__ = ['Item', 'SearchWindow', 'TableModel']
|
||||
|
||||
DELTA_LOCALE = -5 # deltatime col
|
||||
DIR = os.path.abspath(os.path.normpath(os.path.join(__file__,
|
||||
'..', '..')))
|
||||
|
||||
ICONS = {
|
||||
'image': os.path.join(DIR, 'share/icon-camera.svg'),
|
||||
'stock': os.path.join(DIR, 'share/icon-stock.svg'),
|
||||
}
|
||||
|
||||
|
||||
class Item(QItemDelegate):
|
||||
|
||||
def __init__(self, values, fields):
|
||||
super(Item, self).__init__()
|
||||
_ = [setattr(self, n, str(v)) for n, v in zip(fields, values)]
|
||||
|
||||
|
||||
class SearchWindow(QDialog):
|
||||
|
||||
def __init__(self, parent, headers, records, methods, filter_column=[],
|
||||
cols_width=[], title=None, fill=False):
|
||||
"""
|
||||
parent: parent window
|
||||
headers: is a ordered dict of data with name field-column as keys.
|
||||
records: is a tuple with two values: a key called 'objects' or 'values',
|
||||
and a list of instances values or plain values for build data model:
|
||||
[('a' 'b', 'c'), ('d', 'e', 'f')...]
|
||||
on_selected_method: method to call when triggered the selection
|
||||
filter_column: list of column to search values, eg: [0,2]
|
||||
title: title of window
|
||||
cols_width: list of width of columns, eg. [120, 60, 280]
|
||||
fill: Boolean that define if the table must be fill with all data and
|
||||
values and these are visibles
|
||||
"""
|
||||
super(SearchWindow, self).__init__(parent)
|
||||
|
||||
self.parent = parent
|
||||
self.headers = headers
|
||||
self.records = records
|
||||
self.fill = fill
|
||||
self.methods = methods
|
||||
self.on_selected_method = methods.get('on_selected_method')
|
||||
self.on_return_method = methods.get('on_return_method')
|
||||
self.filter_column = filter_column
|
||||
self.cols_width = cols_width
|
||||
self.rows = []
|
||||
self.current_row = None
|
||||
if not title:
|
||||
title = self.tr('SEARCH...')
|
||||
self.setWindowTitle(title)
|
||||
WIDTH = 550
|
||||
if cols_width:
|
||||
WIDTH = sum(cols_width) + 130
|
||||
|
||||
self.resize(QSize(WIDTH, 400))
|
||||
|
||||
self.create_table()
|
||||
self.create_widgets()
|
||||
self.create_layout()
|
||||
self.create_connections()
|
||||
if records:
|
||||
if records[0] == 'objects':
|
||||
self.set_from_objects(records[1])
|
||||
elif records[0] == 'values':
|
||||
self.set_from_values(records[1])
|
||||
elif records[0] == 'data':
|
||||
self.set_from_data(records[1])
|
||||
|
||||
def get_id(self):
|
||||
if self.current_row:
|
||||
return self.current_row['id']
|
||||
|
||||
def clear_rows(self):
|
||||
if self.fill:
|
||||
self.table_model.items = []
|
||||
self.table_model.currentItems = []
|
||||
self.table_model.layoutChanged.emit()
|
||||
|
||||
def clear_filter(self):
|
||||
self.filter_field.setText('')
|
||||
self.filter_field.setFocus()
|
||||
if self.fill:
|
||||
self.table_model.items = []
|
||||
self.table_view.selectRow(-1)
|
||||
self.table_model.currentItems = []
|
||||
self.table_model.layoutChanged.emit()
|
||||
|
||||
def set_from_data(self, values):
|
||||
if self.fill:
|
||||
self.clear_filter()
|
||||
self.table_model.set_rows(values, typedata='list')
|
||||
|
||||
def set_from_values(self, values):
|
||||
if self.fill:
|
||||
self.clear_rows()
|
||||
self.table_model.set_rows(values)
|
||||
self.update_count_field()
|
||||
|
||||
def update_count_field(self):
|
||||
values = self.table_model.currentItems
|
||||
self.label_count.setText(str(len(values)))
|
||||
|
||||
def activate_counter(self):
|
||||
self.label_control = QLabel('0')
|
||||
self.label_control.setObjectName('label_count')
|
||||
self.label_control.setAlignment(Qt.AlignCenter | Qt.AlignVCenter)
|
||||
self.filter_layout.addWidget(self.label_control)
|
||||
|
||||
def set_counter_control(self, val):
|
||||
self.label_control.setText(str(len(val)))
|
||||
|
||||
def set_from_objects(self, objects):
|
||||
self.rows = []
|
||||
for object_ in objects:
|
||||
row = []
|
||||
for field, data in self.headers.items():
|
||||
val = getattr(object_, field)
|
||||
if hasattr(val, 'name'):
|
||||
val = getattr(val, 'name')
|
||||
elif data['type'] == 'number':
|
||||
val = val
|
||||
elif data['type'] == 'int':
|
||||
val = str(val)
|
||||
elif data['type'] == 'date':
|
||||
val = val.strftime('%d/%m/%Y')
|
||||
row.append(val)
|
||||
self.rows.append(row)
|
||||
self.table_model.set_rows(self.rows)
|
||||
|
||||
def create_table(self):
|
||||
# set the table model
|
||||
self.table_model = TableModel(self, self.rows, self.headers,
|
||||
self.filter_column, fill=self.fill)
|
||||
|
||||
self.table_view = QTableView()
|
||||
self.table_view.setModel(self.table_model)
|
||||
self.table_view.setMinimumSize(450, 350)
|
||||
self.table_view.setColumnHidden(0, True)
|
||||
|
||||
self.table_view.setAlternatingRowColors(True)
|
||||
self.table_view.setSelectionBehavior(QAbstractItemView.SelectRows)
|
||||
self.table_view.setGridStyle(Qt.DotLine)
|
||||
for i in range(len(self.cols_width)):
|
||||
self.table_view.setColumnWidth(i, self.cols_width[i])
|
||||
|
||||
vh = self.table_view.verticalHeader()
|
||||
vh.setVisible(False)
|
||||
hh = self.table_view.horizontalHeader()
|
||||
hh.setStretchLastSection(True)
|
||||
|
||||
# enable sorting
|
||||
self.table_view.setSortingEnabled(True)
|
||||
|
||||
def create_widgets(self):
|
||||
self.filter_label = QLabel(self.tr("FILTER:"))
|
||||
self.filter_field = QLineEdit()
|
||||
self.label_count = QLabel('0')
|
||||
self.label_count.setObjectName('label_count')
|
||||
self.label_count.setAlignment(Qt.AlignCenter | Qt.AlignVCenter)
|
||||
self.pushButtonOk = ActionButton('ok', self.action_selection_changed)
|
||||
self.pushButtonCancel = ActionButton('cancel', self.action_close)
|
||||
|
||||
def create_layout(self):
|
||||
layout = QVBoxLayout()
|
||||
self.filter_layout = QHBoxLayout()
|
||||
self.filter_layout.addWidget(self.filter_label)
|
||||
self.filter_layout.addWidget(self.filter_field)
|
||||
self.filter_layout.addWidget(self.label_count)
|
||||
layout.addLayout(self.filter_layout)
|
||||
|
||||
scroll_area = QScrollArea()
|
||||
scroll_area.setWidgetResizable(True)
|
||||
scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
|
||||
scroll_area.setWidget(self.table_view)
|
||||
|
||||
layout.addWidget(scroll_area)
|
||||
buttons_layout = QHBoxLayout()
|
||||
buttons_layout.addWidget(self.pushButtonCancel)
|
||||
buttons_layout.addWidget(self.pushButtonOk)
|
||||
layout.addLayout(buttons_layout)
|
||||
|
||||
QScroller.grabGesture(scroll_area, QScroller.LeftMouseButtonGesture)
|
||||
|
||||
self.filter_field.setFocus()
|
||||
self.setLayout(layout)
|
||||
|
||||
def create_connections(self):
|
||||
self.filter_field.textChanged.connect(self.action_text_changed)
|
||||
self.filter_field.returnPressed.connect(self.action_filter_return_pressed)
|
||||
self.table_view.clicked.connect(self.action_selection_changed)
|
||||
self.table_view.activated.connect(self.action_table_activated)
|
||||
|
||||
def action_table_activated(self):
|
||||
pass
|
||||
|
||||
def execute(self):
|
||||
self.current_row = None
|
||||
self.parent.releaseKeyboard()
|
||||
self.filter_field.setFocus()
|
||||
return self.exec_()
|
||||
|
||||
def show(self):
|
||||
self.parent.releaseKeyboard()
|
||||
self.clear_filter()
|
||||
self.filter_field.setFocus()
|
||||
super(SearchWindow, self).show()
|
||||
|
||||
def hide(self):
|
||||
self.parent.grabKeyboard()
|
||||
self.parent.setFocus()
|
||||
super(SearchWindow, self).hide()
|
||||
|
||||
def action_close(self):
|
||||
self.close()
|
||||
|
||||
def action_selection_changed(self):
|
||||
selected = self.table_view.currentIndex()
|
||||
# current_row is a dict with values used on mainwindow
|
||||
self.current_row = self.table_model.getCurrentRow(selected)
|
||||
if selected.row() < 0:
|
||||
self.filter_field.setFocus()
|
||||
else:
|
||||
column = selected.column()
|
||||
name_field = self.table_model.header_fields[column]
|
||||
if self.methods.get(name_field):
|
||||
|
||||
parent_method = self.methods[name_field]
|
||||
parent_method()
|
||||
return
|
||||
self.hide()
|
||||
if self.parent:
|
||||
getattr(self.parent, self.on_selected_method)()
|
||||
self.filter_field.setText('')
|
||||
|
||||
def action_text_changed(self):
|
||||
self.table_model.setFilter(searchText=self.filter_field.text())
|
||||
self.update_count_field()
|
||||
self.table_model.layoutChanged.emit()
|
||||
|
||||
def action_filter_return_pressed(self):
|
||||
if hasattr(self.parent, self.on_return_method):
|
||||
method = getattr(self.parent, self.on_return_method)
|
||||
method()
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
key = event.key()
|
||||
selected = self.table_view.currentIndex()
|
||||
if key == Qt.Key_Down:
|
||||
if not self.table_view.hasFocus():
|
||||
self.table_view.setFocus()
|
||||
self.table_view.selectRow(selected.row() + 1)
|
||||
elif key == Qt.Key_Up:
|
||||
if selected.row() == 0:
|
||||
self.filter_field.setFocus()
|
||||
else:
|
||||
self.table_view.selectRow(selected.row() - 1)
|
||||
elif key == Qt.Key_Return:
|
||||
if selected.row() < 0:
|
||||
self.filter_field.setFocus()
|
||||
else:
|
||||
self.action_selection_changed()
|
||||
elif key == Qt.Key_Escape:
|
||||
self.hide()
|
||||
else:
|
||||
pass
|
||||
super(SearchWindow, self).keyPressEvent(event)
|
||||
|
||||
|
||||
class TableModel(QAbstractTableModel):
|
||||
sigItem_selected = pyqtSignal(str)
|
||||
|
||||
def __init__(self, parent, rows, headers, filter_column=[], fill=False, *args):
|
||||
"""
|
||||
rows: a list of dicts with values
|
||||
headers: a list of strings
|
||||
filter_column: list of index of columns for use as filter
|
||||
fill: If is True ever the rows will be visible
|
||||
"""
|
||||
QAbstractTableModel.__init__(self, parent, *args)
|
||||
self.rows = rows
|
||||
self.fill = fill
|
||||
self.headers = headers
|
||||
self.header_fields = [h for h in headers.keys()]
|
||||
self.header_name = [h['desc'] for h in headers.values()]
|
||||
self.rows = []
|
||||
self.currentItems = []
|
||||
self.items = []
|
||||
self.searchField = None
|
||||
self.mainColumn = 2
|
||||
self.filter_column = filter_column
|
||||
self.create_icons()
|
||||
if rows and fill:
|
||||
self.set_rows(rows)
|
||||
|
||||
def create_icons(self):
|
||||
pix_camera = QPixmap()
|
||||
pix_camera.load(ICONS['image'])
|
||||
icon_camera = QIcon()
|
||||
icon_camera.addPixmap(pix_camera)
|
||||
|
||||
pix_stock = QPixmap()
|
||||
pix_stock.load(ICONS['stock'])
|
||||
icon_stock = QIcon()
|
||||
icon_stock.addPixmap(pix_stock)
|
||||
self.icons = {
|
||||
'icon_image': icon_camera,
|
||||
'icon_stock': icon_stock,
|
||||
}
|
||||
|
||||
def _get_item(self, values):
|
||||
res = {}
|
||||
for name, data in self.headers.items():
|
||||
if '.' in name:
|
||||
attrs = name.split('.')
|
||||
val = values.get(attrs[0])
|
||||
if val:
|
||||
val = val.get(attrs[1])
|
||||
else:
|
||||
val = values[name]
|
||||
|
||||
if val:
|
||||
if data['type'] == 'date':
|
||||
val = val.strftime('%d-%m-%Y')
|
||||
elif data['type'] == 'number':
|
||||
val = '{0:,}'.format(val)
|
||||
elif data['type'] == 'datetime':
|
||||
mod_hours = val + timedelta(hours=DELTA_LOCALE)
|
||||
val = mod_hours.strftime('%d/%m/%Y %I:%M %p')
|
||||
elif data['type'] == 'icon':
|
||||
val = self.icons['icon_' + data['icon']]
|
||||
|
||||
res[name] = val
|
||||
return res
|
||||
|
||||
def set_rows(self, rows):
|
||||
self.beginResetModel()
|
||||
self.endResetModel()
|
||||
self.rows = rows
|
||||
for values in rows:
|
||||
self.insertRows(self._get_item(values))
|
||||
|
||||
if self.fill is True:
|
||||
self.currentItems = self.items
|
||||
|
||||
def set_rows_list(self, rows):
|
||||
self.beginResetModel()
|
||||
self.endResetModel()
|
||||
for values in rows:
|
||||
self.insertRows(Item(values, self.header_fields))
|
||||
|
||||
if self.fill is True:
|
||||
self.currentItems = self.items
|
||||
|
||||
def rowCount(self, parent):
|
||||
return len(self.rows)
|
||||
|
||||
def columnCount(self, parent):
|
||||
return len(self.header_fields)
|
||||
|
||||
def getCurrentRow(self, index):
|
||||
row = index.row()
|
||||
if self.currentItems and row >= 0 and len(self.currentItems) > row:
|
||||
return self.currentItems[row]
|
||||
|
||||
def data(self, index, role, col=None):
|
||||
if not index.isValid():
|
||||
return
|
||||
|
||||
row = index.row()
|
||||
if col is None:
|
||||
column = index.column()
|
||||
else:
|
||||
column = col
|
||||
item = None
|
||||
if self.currentItems and len(self.currentItems) > row:
|
||||
item = self.currentItems[row]
|
||||
|
||||
name_field = self.header_fields[column]
|
||||
|
||||
data = self.headers[name_field]
|
||||
if role == Qt.DisplayRole and item:
|
||||
if column is not None:
|
||||
return item.get(name_field)
|
||||
elif role == Qt.DecorationRole:
|
||||
if item:
|
||||
return item[name_field]
|
||||
elif role == Qt.TextAlignmentRole:
|
||||
if item:
|
||||
align = Qt.AlignmentFlag(Qt.AlignLeft)
|
||||
if data['type'] == 'icon':
|
||||
align = Qt.AlignmentFlag(Qt.AlignHCenter)
|
||||
elif data['type'] == 'number':
|
||||
align = Qt.AlignmentFlag(Qt.AlignRight)
|
||||
return align
|
||||
elif role == Qt.UserRole:
|
||||
return item
|
||||
|
||||
def headerData(self, col, orientation, role):
|
||||
if orientation == Qt.Horizontal and role == Qt.DisplayRole:
|
||||
return QVariant(self.header_name[col])
|
||||
return QVariant()
|
||||
|
||||
def insertRows(self, item, row=0, column=1, index=QModelIndex()):
|
||||
self.beginInsertRows(index, row, row + 1)
|
||||
self.items.append(item)
|
||||
self.endInsertRows()
|
||||
|
||||
def sort(self, column, order):
|
||||
name_field = self.header_fields[column]
|
||||
if 'icon-' in name_field:
|
||||
return
|
||||
data = [(value[name_field], value) for value in self.items]
|
||||
data.sort(key=itemgetter(0), reverse=order)
|
||||
self.currentItems = [v for k, v in data]
|
||||
self.layoutChanged.emit()
|
||||
|
||||
def setFilter(self, searchText=None, mainColumn=None, order=None):
|
||||
if not searchText:
|
||||
return
|
||||
|
||||
if mainColumn is not None:
|
||||
self.mainColumn = mainColumn
|
||||
self.order = order
|
||||
|
||||
self.currentItems = self.items
|
||||
|
||||
if searchText and self.filter_column:
|
||||
matchers = [t.lower() for t in searchText.split(' ')]
|
||||
self.filteredItems = []
|
||||
for item in self.currentItems:
|
||||
values_clear = list(filter(None, item.values()))
|
||||
exists = all(mt in ''.join(values_clear).lower() for mt in matchers)
|
||||
if exists:
|
||||
self.filteredItems.append(item)
|
||||
|
||||
self.currentItems = self.filteredItems
|
||||
|
||||
self.layoutChanged.emit()
|
||||
|
||||
def clear_filter(self):
|
||||
if self.fill:
|
||||
self.items = []
|
||||
self.currentItems = []
|
||||
self.layoutChanged.emit()
|
|
@ -0,0 +1,461 @@
|
|||
import os
|
||||
from operator import itemgetter
|
||||
from datetime import timedelta
|
||||
|
||||
from PyQt5.QtCore import Qt, QVariant, QAbstractTableModel, \
|
||||
pyqtSignal, QModelIndex, QSize
|
||||
from PyQt5.QtWidgets import QTableView, QVBoxLayout, \
|
||||
QAbstractItemView, QLineEdit, QDialog, QLabel, QScroller, \
|
||||
QHBoxLayout, QScrollArea, QItemDelegate
|
||||
from PyQt5.QtGui import QPixmap, QIcon
|
||||
|
||||
from .buttons import ActionButton
|
||||
|
||||
__all__ = ['Item', 'SearchWindow', 'TableModel']
|
||||
|
||||
DELTA_LOCALE = -5 # deltatime col
|
||||
DIR = os.path.abspath(os.path.normpath(os.path.join(__file__,
|
||||
'..', '..')))
|
||||
|
||||
ICONS = {
|
||||
'image': os.path.join(DIR, 'share/icon-camera.svg'),
|
||||
'stock': os.path.join(DIR, 'share/icon-stock.svg'),
|
||||
}
|
||||
|
||||
|
||||
class Item(QItemDelegate):
|
||||
|
||||
def __init__(self, values, fields):
|
||||
super(Item, self).__init__()
|
||||
_ = [setattr(self, n, str(v)) for n, v in zip(fields, values)]
|
||||
|
||||
|
||||
class SearchWindow(QDialog):
|
||||
|
||||
def __init__(self, parent, headers, records, methods, filter_column=[],
|
||||
cols_width=[], title=None, fill=False):
|
||||
"""
|
||||
parent: parent window
|
||||
headers: is a ordered dict of data with name field-column as keys.
|
||||
records: is a tuple with two values: a key called 'objects' or 'values',
|
||||
and a list of instances values or plain values for build data model:
|
||||
[('a' 'b', 'c'), ('d', 'e', 'f')...]
|
||||
on_selected_method: method to call when triggered the selection
|
||||
filter_column: list of column to search values, eg: [0,2]
|
||||
title: title of window
|
||||
cols_width: list of width of columns, eg. [120, 60, 280]
|
||||
fill: Boolean that define if the table must be fill with all data and
|
||||
values and these are visibles
|
||||
"""
|
||||
super(SearchWindow, self).__init__(parent)
|
||||
|
||||
self.parent = parent
|
||||
self.headers = headers
|
||||
self.records = records
|
||||
self.fill = fill
|
||||
self.methods = methods
|
||||
self.on_selected_method = methods.get('on_selected_method')
|
||||
self.on_return_method = methods.get('on_return_method')
|
||||
self.filter_column = filter_column
|
||||
self.cols_width = cols_width
|
||||
self.rows = []
|
||||
self.current_row = None
|
||||
if not title:
|
||||
title = self.tr('SEARCH...')
|
||||
self.setWindowTitle(title)
|
||||
WIDTH = 550
|
||||
if cols_width:
|
||||
WIDTH = sum(cols_width) + 130
|
||||
|
||||
self.resize(QSize(WIDTH, 400))
|
||||
|
||||
self.create_table()
|
||||
self.create_widgets()
|
||||
self.create_layout()
|
||||
self.create_connections()
|
||||
if records:
|
||||
if records[0] == 'objects':
|
||||
self.set_from_objects(records[1])
|
||||
elif records[0] == 'values':
|
||||
self.set_from_values(records[1])
|
||||
elif records[0] == 'data':
|
||||
self.set_from_data(records[1])
|
||||
|
||||
def get_id(self):
|
||||
if self.current_row:
|
||||
return self.current_row['id']
|
||||
|
||||
def clear_rows(self):
|
||||
if self.fill:
|
||||
self.table_model.items = []
|
||||
self.table_model.currentItems = []
|
||||
self.table_model.layoutChanged.emit()
|
||||
|
||||
def clear_filter(self):
|
||||
self.filter_field.setText('')
|
||||
self.filter_field.setFocus()
|
||||
if self.fill:
|
||||
self.table_model.items = []
|
||||
self.table_view.selectRow(-1)
|
||||
self.table_model.currentItems = []
|
||||
self.table_model.layoutChanged.emit()
|
||||
|
||||
def set_from_data(self, values):
|
||||
if self.fill:
|
||||
self.clear_filter()
|
||||
self.table_model.set_rows(values, typedata='list')
|
||||
|
||||
def set_from_values(self, values):
|
||||
if self.fill:
|
||||
self.clear_rows()
|
||||
self.table_model.set_rows(values)
|
||||
self.update_count_field()
|
||||
|
||||
def update_count_field(self):
|
||||
values = self.table_model.currentItems
|
||||
self.label_count.setText(str(len(values)))
|
||||
|
||||
def activate_counter(self):
|
||||
self.label_control = QLabel('0')
|
||||
self.label_control.setObjectName('label_count')
|
||||
self.label_control.setAlignment(Qt.AlignCenter | Qt.AlignVCenter)
|
||||
self.filter_layout.addWidget(self.label_control)
|
||||
|
||||
def set_counter_control(self, val):
|
||||
self.label_control.setText(str(len(val)))
|
||||
|
||||
def set_from_objects(self, objects):
|
||||
self.rows = []
|
||||
for object_ in objects:
|
||||
row = []
|
||||
for field, data in self.headers.items():
|
||||
val = getattr(object_, field)
|
||||
if hasattr(val, 'name'):
|
||||
val = getattr(val, 'name')
|
||||
elif data['type'] == 'number':
|
||||
val = val
|
||||
elif data['type'] == 'int':
|
||||
val = str(val)
|
||||
elif data['type'] == 'date':
|
||||
val = val.strftime('%d/%m/%Y')
|
||||
row.append(val)
|
||||
self.rows.append(row)
|
||||
self.table_model.set_rows(self.rows)
|
||||
|
||||
def create_table(self):
|
||||
# set the table model
|
||||
self.table_model = TableModel(self, self.rows, self.headers,
|
||||
self.filter_column, fill=self.fill)
|
||||
|
||||
self.table_view = QTableView()
|
||||
self.table_view.setModel(self.table_model)
|
||||
self.table_view.setMinimumSize(450, 350)
|
||||
self.table_view.setColumnHidden(0, True)
|
||||
|
||||
self.table_view.setAlternatingRowColors(True)
|
||||
self.table_view.setSelectionBehavior(QAbstractItemView.SelectRows)
|
||||
self.table_view.setGridStyle(Qt.DotLine)
|
||||
for i in range(len(self.cols_width)):
|
||||
self.table_view.setColumnWidth(i, self.cols_width[i])
|
||||
|
||||
vh = self.table_view.verticalHeader()
|
||||
vh.setVisible(False)
|
||||
hh = self.table_view.horizontalHeader()
|
||||
hh.setStretchLastSection(True)
|
||||
|
||||
# enable sorting
|
||||
self.table_view.setSortingEnabled(True)
|
||||
|
||||
def create_widgets(self):
|
||||
self.filter_label = QLabel(self.tr("FILTER:"))
|
||||
self.filter_field = QLineEdit()
|
||||
self.label_count = QLabel('0')
|
||||
self.label_count.setObjectName('label_count')
|
||||
self.label_count.setAlignment(Qt.AlignCenter | Qt.AlignVCenter)
|
||||
self.pushButtonOk = ActionButton('ok', self.action_selection_changed)
|
||||
self.pushButtonCancel = ActionButton('cancel', self.action_close)
|
||||
|
||||
def create_layout(self):
|
||||
layout = QVBoxLayout()
|
||||
self.filter_layout = QHBoxLayout()
|
||||
self.filter_layout.addWidget(self.filter_label)
|
||||
self.filter_layout.addWidget(self.filter_field)
|
||||
self.filter_layout.addWidget(self.label_count)
|
||||
layout.addLayout(self.filter_layout)
|
||||
|
||||
scroll_area = QScrollArea()
|
||||
scroll_area.setWidgetResizable(True)
|
||||
scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
|
||||
scroll_area.setWidget(self.table_view)
|
||||
|
||||
layout.addWidget(scroll_area)
|
||||
buttons_layout = QHBoxLayout()
|
||||
buttons_layout.addWidget(self.pushButtonCancel)
|
||||
buttons_layout.addWidget(self.pushButtonOk)
|
||||
layout.addLayout(buttons_layout)
|
||||
|
||||
QScroller.grabGesture(scroll_area, QScroller.LeftMouseButtonGesture)
|
||||
|
||||
self.filter_field.setFocus()
|
||||
self.setLayout(layout)
|
||||
|
||||
def create_connections(self):
|
||||
self.filter_field.textChanged.connect(self.action_text_changed)
|
||||
self.filter_field.returnPressed.connect(self.action_filter_return_pressed)
|
||||
self.table_view.clicked.connect(self.action_selection_changed)
|
||||
self.table_view.activated.connect(self.action_table_activated)
|
||||
|
||||
def action_table_activated(self):
|
||||
pass
|
||||
|
||||
def execute(self):
|
||||
self.current_row = None
|
||||
self.parent.releaseKeyboard()
|
||||
self.filter_field.setFocus()
|
||||
return self.exec_()
|
||||
|
||||
def show(self):
|
||||
self.parent.releaseKeyboard()
|
||||
self.clear_filter()
|
||||
self.filter_field.setFocus()
|
||||
super(SearchWindow, self).show()
|
||||
|
||||
def hide(self):
|
||||
self.parent.grabKeyboard()
|
||||
self.parent.setFocus()
|
||||
super(SearchWindow, self).hide()
|
||||
|
||||
def action_close(self):
|
||||
self.close()
|
||||
|
||||
def action_selection_changed(self):
|
||||
selected = self.table_view.currentIndex()
|
||||
# current_row is a dict with values used on mainwindow
|
||||
self.current_row = self.table_model.getCurrentRow(selected)
|
||||
if selected.row() < 0:
|
||||
self.filter_field.setFocus()
|
||||
else:
|
||||
column = selected.column()
|
||||
name_field = self.table_model.header_fields[column]
|
||||
if self.methods.get(name_field):
|
||||
|
||||
parent_method = self.methods[name_field]
|
||||
parent_method()
|
||||
return
|
||||
self.hide()
|
||||
if self.parent:
|
||||
getattr(self.parent, self.on_selected_method)()
|
||||
self.filter_field.setText('')
|
||||
|
||||
def action_text_changed(self):
|
||||
self.table_model.setFilter(searchText=self.filter_field.text())
|
||||
self.update_count_field()
|
||||
self.table_model.layoutChanged.emit()
|
||||
|
||||
def action_filter_return_pressed(self):
|
||||
if hasattr(self.parent, self.on_return_method):
|
||||
method = getattr(self.parent, self.on_return_method)
|
||||
method()
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
key = event.key()
|
||||
selected = self.table_view.currentIndex()
|
||||
if key == Qt.Key_Down:
|
||||
if not self.table_view.hasFocus():
|
||||
self.table_view.setFocus()
|
||||
self.table_view.selectRow(selected.row() + 1)
|
||||
elif key == Qt.Key_Up:
|
||||
if selected.row() == 0:
|
||||
self.filter_field.setFocus()
|
||||
else:
|
||||
self.table_view.selectRow(selected.row() - 1)
|
||||
elif key == Qt.Key_Return:
|
||||
if selected.row() < 0:
|
||||
self.filter_field.setFocus()
|
||||
else:
|
||||
self.action_selection_changed()
|
||||
elif key == Qt.Key_Escape:
|
||||
self.hide()
|
||||
else:
|
||||
pass
|
||||
super(SearchWindow, self).keyPressEvent(event)
|
||||
|
||||
|
||||
class TableModel(QAbstractTableModel):
|
||||
sigItem_selected = pyqtSignal(str)
|
||||
|
||||
def __init__(self, parent, rows, headers, filter_column=[], fill=False, *args):
|
||||
"""
|
||||
rows: a list of dicts with values
|
||||
headers: a list of strings
|
||||
filter_column: list of index of columns for use as filter
|
||||
fill: If is True ever the rows will be visible
|
||||
"""
|
||||
QAbstractTableModel.__init__(self, parent, *args)
|
||||
self.rows = rows
|
||||
self.fill = fill
|
||||
self.headers = headers
|
||||
self.header_fields = [h for h in headers.keys()]
|
||||
self.header_name = [h['desc'] for h in headers.values()]
|
||||
self.rows = []
|
||||
self.currentItems = []
|
||||
self.items = []
|
||||
self.searchField = None
|
||||
self.mainColumn = 2
|
||||
self.filter_column = filter_column
|
||||
self.create_icons()
|
||||
if rows and fill:
|
||||
self.set_rows(rows)
|
||||
|
||||
def create_icons(self):
|
||||
pix_camera = QPixmap()
|
||||
pix_camera.load(ICONS['image'])
|
||||
icon_camera = QIcon()
|
||||
icon_camera.addPixmap(pix_camera)
|
||||
|
||||
pix_stock = QPixmap()
|
||||
pix_stock.load(ICONS['stock'])
|
||||
icon_stock = QIcon()
|
||||
icon_stock.addPixmap(pix_stock)
|
||||
self.icons = {
|
||||
'icon_image': icon_camera,
|
||||
'icon_stock': icon_stock,
|
||||
}
|
||||
|
||||
def _get_item(self, values):
|
||||
res = {}
|
||||
for name, data in self.headers.items():
|
||||
if '.' in name:
|
||||
attrs = name.split('.')
|
||||
val = values.get(attrs[0])
|
||||
if val:
|
||||
val = val.get(attrs[1])
|
||||
else:
|
||||
val = values[name]
|
||||
|
||||
if val:
|
||||
if data['type'] == 'date':
|
||||
val = val.strftime('%d-%m-%Y')
|
||||
elif data['type'] == 'number':
|
||||
val = '{0:,}'.format(val)
|
||||
elif data['type'] == 'datetime':
|
||||
mod_hours = val + timedelta(hours=DELTA_LOCALE)
|
||||
val = mod_hours.strftime('%d/%m/%Y %I:%M %p')
|
||||
elif data['type'] == 'icon':
|
||||
val = self.icons['icon_' + data['icon']]
|
||||
|
||||
res[name] = val
|
||||
return res
|
||||
|
||||
def set_rows(self, rows):
|
||||
self.beginResetModel()
|
||||
self.endResetModel()
|
||||
self.rows = rows
|
||||
for values in rows:
|
||||
self.insertRows(self._get_item(values))
|
||||
|
||||
if self.fill is True:
|
||||
self.currentItems = self.items
|
||||
|
||||
def set_rows_list(self, rows):
|
||||
self.beginResetModel()
|
||||
self.endResetModel()
|
||||
for values in rows:
|
||||
self.insertRows(Item(values, self.header_fields))
|
||||
|
||||
if self.fill is True:
|
||||
self.currentItems = self.items
|
||||
|
||||
def rowCount(self, parent):
|
||||
return len(self.rows)
|
||||
|
||||
def columnCount(self, parent):
|
||||
return len(self.header_fields)
|
||||
|
||||
def getCurrentRow(self, index):
|
||||
row = index.row()
|
||||
if self.currentItems and row >= 0 and len(self.currentItems) > row:
|
||||
return self.currentItems[row]
|
||||
|
||||
def data(self, index, role, col=None):
|
||||
if not index.isValid():
|
||||
return
|
||||
|
||||
row = index.row()
|
||||
if col is None:
|
||||
column = index.column()
|
||||
else:
|
||||
column = col
|
||||
item = None
|
||||
if self.currentItems and len(self.currentItems) > row:
|
||||
item = self.currentItems[row]
|
||||
|
||||
name_field = self.header_fields[column]
|
||||
|
||||
data = self.headers[name_field]
|
||||
if role == Qt.DisplayRole and item:
|
||||
if column is not None:
|
||||
return item.get(name_field)
|
||||
elif role == Qt.DecorationRole:
|
||||
if item:
|
||||
return item[name_field]
|
||||
elif role == Qt.TextAlignmentRole:
|
||||
if item:
|
||||
align = Qt.AlignmentFlag(Qt.AlignLeft)
|
||||
if data['type'] == 'icon':
|
||||
align = Qt.AlignmentFlag(Qt.AlignHCenter)
|
||||
elif data['type'] == 'number':
|
||||
align = Qt.AlignmentFlag(Qt.AlignRight)
|
||||
return align
|
||||
elif role == Qt.UserRole:
|
||||
return item
|
||||
|
||||
def headerData(self, col, orientation, role):
|
||||
if orientation == Qt.Horizontal and role == Qt.DisplayRole:
|
||||
return QVariant(self.header_name[col])
|
||||
return QVariant()
|
||||
|
||||
def insertRows(self, item, row=0, column=1, index=QModelIndex()):
|
||||
self.beginInsertRows(index, row, row + 1)
|
||||
self.items.append(item)
|
||||
self.endInsertRows()
|
||||
|
||||
def sort(self, column, order):
|
||||
name_field = self.header_fields[column]
|
||||
if 'icon-' in name_field:
|
||||
return
|
||||
data = [(value[name_field], value) for value in self.items]
|
||||
data.sort(key=itemgetter(0), reverse=order)
|
||||
self.currentItems = [v for k, v in data]
|
||||
self.layoutChanged.emit()
|
||||
|
||||
def setFilter(self, searchText=None, mainColumn=None, order=None):
|
||||
if not searchText:
|
||||
return
|
||||
|
||||
if mainColumn is not None:
|
||||
self.mainColumn = mainColumn
|
||||
self.order = order
|
||||
|
||||
self.currentItems = self.items
|
||||
|
||||
if searchText and self.filter_column:
|
||||
matchers = [t.lower() for t in searchText.split(' ')]
|
||||
self.filteredItems = []
|
||||
for item in self.currentItems:
|
||||
values = item.values()
|
||||
values.pop(0)
|
||||
values_clear = list(filter(None, values))
|
||||
exists = all(mt in ''.join(values_clear).lower() for mt in matchers)
|
||||
if exists:
|
||||
self.filteredItems.append(item)
|
||||
|
||||
self.currentItems = self.filteredItems
|
||||
|
||||
self.layoutChanged.emit()
|
||||
|
||||
def clear_filter(self):
|
||||
if self.fill:
|
||||
self.items = []
|
||||
self.currentItems = []
|
||||
self.layoutChanged.emit()
|
|
@ -0,0 +1,65 @@
|
|||
from PyQt5.QtWidgets import QTableView, QHeaderView, QAbstractItemView
|
||||
from PyQt5.QtCore import Qt
|
||||
|
||||
STRETCH = QHeaderView.Stretch
|
||||
|
||||
|
||||
class TableView(QTableView):
|
||||
|
||||
def __init__(self, name, model, col_sizes=[], method_selected_row=None):
|
||||
super(TableView, self).__init__()
|
||||
self.setObjectName(name)
|
||||
self.verticalHeader().hide()
|
||||
self.setGridStyle(Qt.DotLine)
|
||||
self.setAlternatingRowColors(True)
|
||||
self.setEditTriggers(QAbstractItemView.NoEditTriggers)
|
||||
self.setSelectionBehavior(QAbstractItemView.SelectRows)
|
||||
self.setSelectionMode(QAbstractItemView.SingleSelection)
|
||||
self.setVerticalScrollMode(QAbstractItemView.ScrollPerItem)
|
||||
self.model = model
|
||||
self.method_selected_row = method_selected_row
|
||||
self.doubleClicked.connect(self.on_selected_row)
|
||||
self.setWordWrap(False)
|
||||
|
||||
if model:
|
||||
self.setModel(model)
|
||||
|
||||
header = self.horizontalHeader()
|
||||
if col_sizes:
|
||||
for i, size in enumerate(col_sizes):
|
||||
if type(size) == int:
|
||||
header.resizeSection(i, size)
|
||||
else:
|
||||
header.setSectionResizeMode(i, STRETCH)
|
||||
|
||||
def on_selected_row(self):
|
||||
selected_idx = self.currentIndex()
|
||||
if selected_idx:
|
||||
self.method_selected_row(self.model.get_data(selected_idx))
|
||||
|
||||
def rowsInserted(self, index, start, end):
|
||||
# Adjust scroll to last row (bottom)
|
||||
self.scrollToBottom()
|
||||
|
||||
def removeElement(self, index):
|
||||
if not index:
|
||||
return
|
||||
if index.row() >= 0 and self.hasFocus():
|
||||
item = self.model.get_data(index)
|
||||
id_ = self.model.removeId(index.row(), index)
|
||||
self.model.deleteRecords([id_])
|
||||
self.model.layoutChanged.emit()
|
||||
return item
|
||||
|
||||
def delete_item(self):
|
||||
item_removed = {}
|
||||
selected_idx = self.currentIndex()
|
||||
item_removed = self.removeElement(selected_idx)
|
||||
return item_removed
|
||||
|
||||
def moved_selection(self, key):
|
||||
selected_idx = self.currentIndex()
|
||||
if key == Qt.Key_Down:
|
||||
self.selectRow(selected_idx.row() + 1)
|
||||
elif key == Qt.Key_Up:
|
||||
self.selectRow(selected_idx.row() - 1)
|
|
@ -0,0 +1 @@
|
|||
|
|
@ -0,0 +1,109 @@
|
|||
|
||||
#WinMain {
|
||||
width : 100%;
|
||||
background-color: #e7e8e9;
|
||||
}
|
||||
|
||||
QListView {
|
||||
font: bold 46px;
|
||||
color: #383838;
|
||||
alignment : center;
|
||||
}
|
||||
|
||||
QScrollArea {
|
||||
background-color: rgb(255, 255, 255);
|
||||
}
|
||||
|
||||
QDialog {
|
||||
background-color: rgb(255, 255, 255);
|
||||
}
|
||||
|
||||
QLabel {
|
||||
font : 12pt;
|
||||
color : rgb(77, 77, 77);
|
||||
min-height : 10px;
|
||||
min-width : 10px;
|
||||
}
|
||||
|
||||
QAbstractButton {
|
||||
font-family: "DejaVu Sans";
|
||||
background-color: rgb(220, 220, 220);
|
||||
border-color: rgb(180, 180, 180);
|
||||
border-width: 0.5px;
|
||||
}
|
||||
|
||||
QAbstractButton:hover {
|
||||
background-color: rgb(210, 210, 210);
|
||||
border-color: rgb(180, 180, 180);
|
||||
border-width: 0.5px;
|
||||
}
|
||||
|
||||
QAbstractButton:pressed {
|
||||
background-color: rgb(200, 200, 200);
|
||||
border-width: 0px;
|
||||
}
|
||||
|
||||
#button_ok {
|
||||
background-color: rgb(55, 181, 228);
|
||||
border-width: 0px;
|
||||
color: white;
|
||||
font: 22pt;
|
||||
}
|
||||
|
||||
#button_ok:hover {
|
||||
background-color: rgb(41, 170, 218);
|
||||
}
|
||||
|
||||
#button_ok:pressed {
|
||||
background-color: rgb(29, 157, 205);
|
||||
}
|
||||
|
||||
#button_cancel {
|
||||
background-color: rgb(227, 44, 99);
|
||||
border-width: 0px;
|
||||
color: white;
|
||||
font: 22pt;
|
||||
}
|
||||
|
||||
#button_cancel:hover {
|
||||
background-color: rgb(205, 26, 80);
|
||||
}
|
||||
|
||||
#button_cancel:pressed {
|
||||
background-color: rgb(181, 31, 76);
|
||||
}
|
||||
|
||||
#login_msg_error {
|
||||
font : 9pt;
|
||||
color : rgb(191, 43, 28);
|
||||
min-height : 60px;
|
||||
}
|
||||
|
||||
#label_message {
|
||||
font : 14pt;
|
||||
min-height : 10px;
|
||||
min-width : 10px;
|
||||
}
|
||||
|
||||
#back_button {
|
||||
background-color: rgb(128, 187, 103);
|
||||
}
|
||||
|
||||
#label_count {
|
||||
font : 12pt;
|
||||
color : rgb(140, 140, 140);
|
||||
background-color: rgb(240, 240, 240);
|
||||
min-height : 10px;
|
||||
min-width : 40px;
|
||||
}
|
||||
|
||||
#dialog_login {
|
||||
background-color: rgb(255, 255, 255);
|
||||
min-width : 400px;
|
||||
}
|
||||
|
||||
#button_cancel, #button_ok {
|
||||
font : 12pt;
|
||||
min-height : 50px;
|
||||
min-width : 120px;
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
|
||||
#product_button {
|
||||
font-family: "DejaVu Sans";
|
||||
border-style: groove;
|
||||
font: 10pt;
|
||||
color: rgb(102, 102, 102);
|
||||
background-color : white;
|
||||
height: 60px;
|
||||
width : 130px;
|
||||
border-width: 0px;
|
||||
border-radius: 10px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#product_button::hover {
|
||||
background-color: rgb(255, 230, 160);
|
||||
}
|
||||
|
||||
#product_button::pressed {
|
||||
background-color: rgb(246, 232, 192);
|
||||
}
|
||||
|
||||
#label_title {
|
||||
font : 11pt;
|
||||
min-height : 32px;
|
||||
width: 120px;
|
||||
color: rgb(102, 102, 102);
|
||||
}
|
||||
|
||||
#label_desc {
|
||||
font : 10pt;
|
||||
height : 30px;
|
||||
width: 110px;
|
||||
color: rgb(152, 152, 152);
|
||||
}
|
||||
|
||||
#label_icon {
|
||||
height : 35px;
|
||||
width: 35px;
|
||||
}
|
||||
|
||||
#label_desc_small {
|
||||
font : 8pt;
|
||||
height : 25px;
|
||||
width: 120px;
|
||||
color: rgb(152, 152, 152);
|
||||
}
|
||||
|
||||
#category_button {
|
||||
font-family: "DejaVu Sans";
|
||||
border-style: groove;
|
||||
font: 10pt;
|
||||
color: rgb(102, 102, 102);
|
||||
background-color : white;
|
||||
height: 80px;
|
||||
width : 130px;
|
||||
border-width: 0px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
#category_button::hover {
|
||||
background-color: rgb(220, 220, 220);
|
||||
}
|
||||
|
||||
#category_button::pressed {
|
||||
background-color: rgb(205, 200, 200);
|
||||
}
|
||||
|
||||
#toolbar_button {
|
||||
font-family: "DejaVu Sans";
|
||||
border-style: groove;
|
||||
font: 9pt;
|
||||
background-color : white;
|
||||
height: 55px;
|
||||
width : 130px;
|
||||
border-width: 0px;
|
||||
}
|
||||
|
||||
#toolbar_button::hover {
|
||||
background-color: rgb(225, 240, 245);
|
||||
}
|
||||
|
||||
#toolbar_button::pressed {
|
||||
background-color: rgb(204, 227, 235);
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
|
||||
#product_button {
|
||||
font-family: "DejaVu Sans";
|
||||
border-style: groove;
|
||||
font: 10pt;
|
||||
color: rgb(102, 102, 102);
|
||||
background-color : white;
|
||||
height: 60px;
|
||||
width : 130px;
|
||||
border-width: 0px;
|
||||
border-radius: 10px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#product_button::hover {
|
||||
background-color: rgb(255, 230, 160);
|
||||
}
|
||||
|
||||
#product_button::pressed {
|
||||
background-color: rgb(246, 232, 192);
|
||||
}
|
||||
|
||||
#label_title {
|
||||
font : 11pt;
|
||||
min-height : 32px;
|
||||
width: 120px;
|
||||
color: rgb(102, 102, 102);
|
||||
}
|
||||
|
||||
#label_desc {
|
||||
font : 10pt;
|
||||
height : 30px;
|
||||
width: 110px;
|
||||
color: rgb(152, 152, 152);
|
||||
}
|
||||
|
||||
#label_icon {
|
||||
height : 35px;
|
||||
width: 35px;
|
||||
}
|
||||
|
||||
#category_button {
|
||||
font-family: "DejaVu Sans";
|
||||
border-style: groove;
|
||||
font: 10pt;
|
||||
color: rgb(102, 102, 102);
|
||||
background-color : white;
|
||||
height: 80px;
|
||||
width : 130px;
|
||||
border-width: 0px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
#category_button::hover {
|
||||
background-color: rgb(220, 220, 220);
|
||||
}
|
||||
|
||||
#category_button::pressed {
|
||||
background-color: rgb(205, 200, 200);
|
||||
}
|
||||
|
||||
#toolbar_button {
|
||||
font-family: "DejaVu Sans";
|
||||
border-style: groove;
|
||||
font: 9pt;
|
||||
background-color : white;
|
||||
height: 55px;
|
||||
width : 130px;
|
||||
border-width: 0px;
|
||||
}
|
||||
|
||||
#toolbar_button::hover {
|
||||
background-color: rgb(225, 240, 245);
|
||||
}
|
||||
|
||||
#toolbar_button::pressed {
|
||||
background-color: rgb(204, 227, 235);
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
|
||||
#product_button {
|
||||
font-family: "DejaVu Sans";
|
||||
border-style: groove;
|
||||
font: 10pt;
|
||||
color: rgb(102, 102, 102);
|
||||
background-color : white;
|
||||
height: 60px;
|
||||
width : 130px;
|
||||
border-width: 0px;
|
||||
border-radius: 10px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#product_button::hover {
|
||||
background-color: rgb(255, 230, 160);
|
||||
}
|
||||
|
||||
#product_button::pressed {
|
||||
background-color: rgb(246, 232, 192);
|
||||
}
|
||||
|
||||
#label_title {
|
||||
font : 11pt;
|
||||
min-height : 32px;
|
||||
width: 120px;
|
||||
color: rgb(102, 102, 102);
|
||||
}
|
||||
|
||||
#label_desc {
|
||||
font : 12pt;
|
||||
height : 30px;
|
||||
width: 110px;
|
||||
color: rgb(152, 152, 152);
|
||||
}
|
||||
|
||||
#label_icon {
|
||||
height : 30px;
|
||||
width : 30px;
|
||||
}
|
||||
|
||||
#category_button {
|
||||
font-family: "DejaVu Sans";
|
||||
border-style: groove;
|
||||
font: 10pt;
|
||||
color: rgb(102, 102, 102);
|
||||
background-color : white;
|
||||
height: 80px;
|
||||
width : 130px;
|
||||
border-width: 0px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
#category_button::hover {
|
||||
background-color: rgb(220, 220, 220);
|
||||
}
|
||||
|
||||
#category_button::pressed {
|
||||
background-color: rgb(205, 200, 200);
|
||||
}
|
||||
|
||||
#toolbar_button {
|
||||
font-family: "DejaVu Sans";
|
||||
border-style: groove;
|
||||
font: 9pt;
|
||||
background-color : white;
|
||||
height: 55px;
|
||||
width : 130px;
|
||||
border-width: 0px;
|
||||
}
|
||||
|
||||
#toolbar_button::hover {
|
||||
background-color: rgb(225, 240, 245);
|
||||
}
|
||||
|
||||
#toolbar_button::pressed {
|
||||
background-color: rgb(204, 227, 235);
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
|
||||
#product_button {
|
||||
font-family: "DejaVu Sans";
|
||||
border-style: groove;
|
||||
font: 10pt;
|
||||
color: rgb(102, 102, 102);
|
||||
background-color : white;
|
||||
height: 60px;
|
||||
width : 130px;
|
||||
border-width: 0px;
|
||||
border-radius: 10px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#product_button::hover {
|
||||
background-color: rgb(255, 230, 160);
|
||||
}
|
||||
|
||||
#product_button::pressed {
|
||||
background-color: rgb(246, 232, 192);
|
||||
}
|
||||
|
||||
#label_title {
|
||||
font : 11pt;
|
||||
min-height : 32px;
|
||||
width: 120px;
|
||||
color: rgb(102, 102, 102);
|
||||
}
|
||||
|
||||
#label_desc {
|
||||
font : 10pt;
|
||||
height : 30px;
|
||||
width: 110px;
|
||||
color: rgb(152, 152, 152);
|
||||
}
|
||||
|
||||
#label_icon {
|
||||
height : 35px;
|
||||
width: 35px;
|
||||
}
|
||||
|
||||
#category_button {
|
||||
font-family: "DejaVu Sans";
|
||||
border-style: groove;
|
||||
font: 10pt;
|
||||
color: rgb(102, 102, 102);
|
||||
background-color : white;
|
||||
height: 80px;
|
||||
width : 130px;
|
||||
border-width: 0px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
#category_button::hover {
|
||||
background-color: rgb(220, 220, 220);
|
||||
}
|
||||
|
||||
#category_button::pressed {
|
||||
background-color: rgb(205, 200, 200);
|
||||
}
|
||||
|
||||
#toolbar_button {
|
||||
font-family: "DejaVu Sans";
|
||||
border-style: groove;
|
||||
font: 9pt;
|
||||
background-color : white;
|
||||
height: 55px;
|
||||
width : 130px;
|
||||
border-width: 0px;
|
||||
}
|
||||
|
||||
#toolbar_button::hover {
|
||||
background-color: rgb(225, 240, 245);
|
||||
}
|
||||
|
||||
#toolbar_button::pressed {
|
||||
background-color: rgb(204, 227, 235);
|
||||
}
|
|
@ -0,0 +1,107 @@
|
|||
|
||||
#WinMain {
|
||||
width : 100%;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
QAbstractButton {
|
||||
font-family: "DejaVu Sans";
|
||||
}
|
||||
|
||||
QAbstractButton::pressed {
|
||||
background-color: rgb(190, 214, 224);
|
||||
}
|
||||
|
||||
QLabel {
|
||||
font : 12pt;
|
||||
color : rgb(102, 102, 102);
|
||||
min-height : 10px;
|
||||
min-width : 10px;
|
||||
}
|
||||
|
||||
TLabel {
|
||||
font : 25px;
|
||||
color : rgb(25, 60, 90);
|
||||
max-height : 40px;
|
||||
background-color: rgb(255, 255, 255);
|
||||
max-height : 70px;
|
||||
min-height : 70px;
|
||||
}
|
||||
|
||||
RLabel {
|
||||
font : 22px;
|
||||
color : rgb(95, 110, 120);
|
||||
max-height : 70px;
|
||||
min-height : 70px;
|
||||
}
|
||||
|
||||
RLabel:hover {
|
||||
background-color: rgb(196, 227, 245);
|
||||
}
|
||||
|
||||
Separator {
|
||||
background-color : rgb(255, 255, 255);
|
||||
color : rgb(198, 210, 220);
|
||||
max-height : 2px;
|
||||
}
|
||||
|
||||
List {
|
||||
background-color : rgb(255, 255, 255);
|
||||
}
|
||||
|
||||
#label_count {
|
||||
font : 12pt;
|
||||
color : rgb(140, 140, 140);
|
||||
background-color: rgb(240, 240, 240);
|
||||
min-height : 10px;
|
||||
min-width : 40px;
|
||||
}
|
||||
|
||||
QListView {
|
||||
font: bold 26px;
|
||||
color: #08090a;
|
||||
}
|
||||
|
||||
QScrollArea {
|
||||
background-color: rgb(255, 255, 255);
|
||||
}
|
||||
|
||||
#label_message {
|
||||
font : 9pt;
|
||||
min-height : 150px;
|
||||
min-width : 10px;
|
||||
}
|
||||
|
||||
#login_msg_error {
|
||||
font : 18pt;
|
||||
color : rgb(191, 43, 28);
|
||||
max-height : 50px;
|
||||
}
|
||||
|
||||
QDialog {
|
||||
max-height : 250px;
|
||||
max-width : 270px;
|
||||
background-color: rgb(255, 255, 255);
|
||||
}
|
||||
|
||||
|
||||
#dialog_login {
|
||||
background-color: rgb(255, 255, 255);
|
||||
min-height : 480px;
|
||||
max-height : 770px;
|
||||
max-width : 500px;
|
||||
}
|
||||
|
||||
#button_cancel, #button_ok {
|
||||
font : 16pt;
|
||||
min-height : 50px;
|
||||
min-width : 120px;
|
||||
}
|
||||
|
||||
#label_host, #label_database, #label_device_id, #label_user,
|
||||
#label_password, #label_mode, #field_host, #field_database,
|
||||
#field_device_id, #field_user, #field_password, #field_mode {
|
||||
font : 18pt;
|
||||
min-height : 40px;
|
||||
min-width : 10px;
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
import os
|
||||
|
||||
locale_path = path_trans = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'i18n_es')
|
|
@ -12,16 +12,16 @@ from PyQt5.QtGui import QTouchEvent
|
|||
from PyQt5.QtWidgets import (QLabel, QTextEdit, QHBoxLayout, QVBoxLayout,
|
||||
QWidget, QGridLayout, QLineEdit, QDoubleSpinBox)
|
||||
|
||||
from neox.commons.action import Action
|
||||
from neox.commons.forms import GridForm, FieldMoney, ComboBox
|
||||
from neox.commons.messages import MessageBar
|
||||
from neox.commons.image import Image
|
||||
from neox.commons.dialogs import QuickDialog
|
||||
from neox.commons.table import TableView
|
||||
from neox.commons.model import TableModel, Modules
|
||||
from neox.commons.search_window import SearchWindow
|
||||
from neox.commons.frontwindow import FrontWindow
|
||||
from neox.commons.menu_buttons import MenuDash
|
||||
from app.commons.action import Action
|
||||
from app.commons.forms import GridForm, FieldMoney, ComboBox
|
||||
from app.commons.messages import MessageBar
|
||||
from app.commons.image import Image
|
||||
from app.commons.dialogs import QuickDialog
|
||||
from app.commons.table import TableView
|
||||
from app.commons.model import TableModel, Modules
|
||||
from app.commons.search_window import SearchWindow
|
||||
from app.commons.frontwindow import FrontWindow
|
||||
from app.commons.menu_buttons import MenuDash
|
||||
|
||||
from .proxy import FastModel
|
||||
from .localdb import LocalStore
|
||||
|
@ -29,7 +29,7 @@ from .reporting import Receipt
|
|||
from .buttonpad import Buttonpad
|
||||
from .manage_tables import ManageTables
|
||||
from .states import STATES, RE_SIGN
|
||||
from .common import get_icon, to_float, to_numeric
|
||||
from .tools import get_icon, to_float, to_numeric
|
||||
from .constants import (PATH_PRINTERS, DELTA_LOCALE, STRETCH, alignRight,
|
||||
alignLeft, alignCenter, alignHCenter, alignVCenter, DIALOG_REPLY_NO,
|
||||
DIALOG_REPLY_YES, ZERO, FRACTIONS, RATE_CREDIT_LIMIT, SCREENS, FILE_BANNER,
|
||||
|
|
|
@ -42,7 +42,7 @@ class FastModel(object):
|
|||
'context': self.ctx,
|
||||
}
|
||||
data = json.dumps(args_, default=encoder)
|
||||
res = requests.get(route, data=data)
|
||||
res = requests.post(route, data=data)
|
||||
return res.json()
|
||||
|
||||
def write(self, ids, values):
|
||||
|
|
|
@ -0,0 +1,97 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
version="1.1"
|
||||
id="Capa_1"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 477.175 477.175"
|
||||
style="enable-background:new 0 0 477.175 477.175;"
|
||||
xml:space="preserve"
|
||||
sodipodi:docname="back.svg"
|
||||
inkscape:version="0.92.3 (2405546, 2018-03-11)"><metadata
|
||||
id="metadata41"><rdf:RDF><cc:Work
|
||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
|
||||
id="defs39" /><sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1853"
|
||||
inkscape:window-height="1053"
|
||||
id="namedview37"
|
||||
showgrid="false"
|
||||
inkscape:zoom="0.49457747"
|
||||
inkscape:cx="242.63135"
|
||||
inkscape:cy="238.58749"
|
||||
inkscape:window-x="67"
|
||||
inkscape:window-y="27"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="Capa_1" />
|
||||
<g
|
||||
id="g4"
|
||||
style="fill:#aaaaaa;fill-opacity:1">
|
||||
<path
|
||||
d="M145.188,238.575l215.5-215.5c5.3-5.3,5.3-13.8,0-19.1s-13.8-5.3-19.1,0l-225.1,225.1c-5.3,5.3-5.3,13.8,0,19.1l225.1,225 c2.6,2.6,6.1,4,9.5,4s6.9-1.3,9.5-4c5.3-5.3,5.3-13.8,0-19.1L145.188,238.575z"
|
||||
id="path2"
|
||||
style="fill:#aaaaaa;fill-opacity:1" />
|
||||
</g>
|
||||
<g
|
||||
id="g6">
|
||||
</g>
|
||||
<g
|
||||
id="g8">
|
||||
</g>
|
||||
<g
|
||||
id="g10">
|
||||
</g>
|
||||
<g
|
||||
id="g12">
|
||||
</g>
|
||||
<g
|
||||
id="g14">
|
||||
</g>
|
||||
<g
|
||||
id="g16">
|
||||
</g>
|
||||
<g
|
||||
id="g18">
|
||||
</g>
|
||||
<g
|
||||
id="g20">
|
||||
</g>
|
||||
<g
|
||||
id="g22">
|
||||
</g>
|
||||
<g
|
||||
id="g24">
|
||||
</g>
|
||||
<g
|
||||
id="g26">
|
||||
</g>
|
||||
<g
|
||||
id="g28">
|
||||
</g>
|
||||
<g
|
||||
id="g30">
|
||||
</g>
|
||||
<g
|
||||
id="g32">
|
||||
</g>
|
||||
<g
|
||||
id="g34">
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.1 KiB |
After Width: | Height: | Size: 8.3 KiB |
|
@ -0,0 +1,123 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
version="1.1"
|
||||
id="Capa_1"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 48 48"
|
||||
xml:space="preserve"
|
||||
inkscape:version="0.91 r13725"
|
||||
sodipodi:docname="photo-camera.svg"
|
||||
inkscape:export-filename="/mnt/Data/Seafile/Development/TRYTON/TRYTON-4.0/FRONTENDS/presik_pos/neo/share/photo-camera.svg.png"
|
||||
inkscape:export-xdpi="266"
|
||||
inkscape:export-ydpi="266"
|
||||
width="48"
|
||||
height="48"><metadata
|
||||
id="metadata59"><rdf:RDF><cc:Work
|
||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
|
||||
id="defs57" /><sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1855"
|
||||
inkscape:window-height="1056"
|
||||
id="namedview55"
|
||||
showgrid="false"
|
||||
inkscape:zoom="4.0689655"
|
||||
inkscape:cx="39.322034"
|
||||
inkscape:cy="23.5"
|
||||
inkscape:window-x="65"
|
||||
inkscape:window-y="24"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="Capa_1" /><path
|
||||
style="fill:#556080"
|
||||
d="m 37.37275,13.819329 -3.322022,-7.7515323 -19.932134,0 -3.322022,7.7515323 -7.6414814,0 C 1.4126899,13.819329 0,15.137865 0,16.764137 L 0,39.528838 C 0,41.169837 1.4251475,42.5 3.1833278,42.5 l 41.8018362,0 c 1.75818,0 3.183327,-1.330163 3.183327,-2.971162 l 0,-22.764701 c 0,-1.626272 -1.412689,-2.944808 -3.15509,-2.944808 l -7.640651,0 z"
|
||||
id="path3"
|
||||
inkscape:connector-curvature="0" /><ellipse
|
||||
style="fill:#424a60;stroke:#2b313d;stroke-width:1.6047045;stroke-linecap:round;stroke-miterlimit:10"
|
||||
cx="24.084661"
|
||||
cy="26.221781"
|
||||
id="circle5"
|
||||
rx="14.118594"
|
||||
ry="13.177606" /><ellipse
|
||||
style="fill:#7383bf"
|
||||
cx="24.084661"
|
||||
cy="26.221781"
|
||||
id="circle7"
|
||||
rx="9.135561"
|
||||
ry="8.5266857" /><rect
|
||||
x="4.9830332"
|
||||
y="9.9435625"
|
||||
style="fill:#38454f"
|
||||
width="3.3220222"
|
||||
height="3.8757663"
|
||||
id="rect9" /><ellipse
|
||||
style="fill:#cccccc"
|
||||
cx="42.355782"
|
||||
cy="19.245401"
|
||||
id="circle11"
|
||||
rx="2.4915166"
|
||||
ry="2.3254597" /><path
|
||||
style="fill:#879ad8"
|
||||
d="m 26.576178,26.221781 c 0,2.786676 1.177657,5.253989 2.98982,6.809722 2.214958,-1.555733 3.654224,-4.023046 3.654224,-6.809722 0,-2.786675 -1.439266,-5.253988 -3.654224,-6.809721 -1.812163,1.555733 -2.98982,4.023046 -2.98982,6.809721 z"
|
||||
id="path13"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
style="fill:#879ad8"
|
||||
d="m 14.9491,26.221781 c 0,2.786676 1.439266,5.253989 3.654224,6.809722 1.812163,-1.555733 2.98982,-4.023046 2.98982,-6.809722 0,-2.786675 -1.177657,-5.253988 -2.98982,-6.809721 -2.214958,1.555733 -3.654224,4.023046 -3.654224,6.809721 z"
|
||||
id="path15"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
style="fill:#556080"
|
||||
d="m 24.084661,35.523621 c -5.495455,0 -9.966067,-4.17265 -9.966067,-9.30184 0,-5.129189 4.470612,-9.301839 9.966067,-9.301839 5.495455,0 9.966067,4.17265 9.966067,9.301839 0,5.12919 -4.470612,9.30184 -9.966067,9.30184 z m 0,-17.053372 c -4.579408,0 -8.305056,3.477337 -8.305056,7.751532 0,4.274196 3.725648,7.751533 8.305056,7.751533 4.579408,0 8.305056,-3.477337 8.305056,-7.751533 0,-4.274195 -3.725648,-7.751532 -8.305056,-7.751532 z"
|
||||
id="path17"
|
||||
inkscape:connector-curvature="0" /><ellipse
|
||||
style="fill:#bccef7"
|
||||
cx="21.885229"
|
||||
cy="24.168938"
|
||||
id="circle23"
|
||||
rx="2.783601"
|
||||
ry="2.5980771" /><g
|
||||
id="g25"
|
||||
transform="translate(0,-10)" /><g
|
||||
id="g27"
|
||||
transform="translate(0,-10)" /><g
|
||||
id="g29"
|
||||
transform="translate(0,-10)" /><g
|
||||
id="g31"
|
||||
transform="translate(0,-10)" /><g
|
||||
id="g33"
|
||||
transform="translate(0,-10)" /><g
|
||||
id="g35"
|
||||
transform="translate(0,-10)" /><g
|
||||
id="g37"
|
||||
transform="translate(0,-10)" /><g
|
||||
id="g39"
|
||||
transform="translate(0,-10)" /><g
|
||||
id="g41"
|
||||
transform="translate(0,-10)" /><g
|
||||
id="g43"
|
||||
transform="translate(0,-10)" /><g
|
||||
id="g45"
|
||||
transform="translate(0,-10)" /><g
|
||||
id="g47"
|
||||
transform="translate(0,-10)" /><g
|
||||
id="g49"
|
||||
transform="translate(0,-10)" /><g
|
||||
id="g51"
|
||||
transform="translate(0,-10)" /><g
|
||||
id="g53"
|
||||
transform="translate(0,-10)" /></svg>
|
After Width: | Height: | Size: 4.8 KiB |
After Width: | Height: | Size: 2.8 KiB |
After Width: | Height: | Size: 3.8 KiB |
After Width: | Height: | Size: 3.4 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 3.8 KiB |
After Width: | Height: | Size: 2.8 KiB |
|
@ -0,0 +1,49 @@
|
|||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 448.8 448.8" style="enable-background:new 0 0 448.8 448.8;" xml:space="preserve">
|
||||
<g>
|
||||
<polygon style="fill:#D9AC80;" points="299.2,292.4 224.4,248.4 299.2,204.8 374,160.8 448.8,204.8 374,248.4 "/>
|
||||
<polygon style="fill:#A67E4F;" points="223.6,248 223.6,419.6 372,333.6 372,249.6 299.2,292.4 224.4,248.4 "/>
|
||||
<polygon style="fill:#D9AC80;" points="149.6,292.4 224.4,248.4 149.6,204.8 74.8,160.8 0,204.8 74.8,248.4 "/>
|
||||
<polygon style="fill:#643513;" points="224.4,72.8 299.2,116.8 374,160.8 299.2,204.8 224.4,248.4 149.6,204.8 74.8,160.8
|
||||
149.6,116.8 "/>
|
||||
<g>
|
||||
<polygon style="fill:#D9AC80;" points="149.6,29.2 224.4,72.8 149.6,116.8 74.8,160.8 0,116.8 74.8,72.8 "/>
|
||||
<polygon style="fill:#D9AC80;" points="299.2,29.2 224.4,72.8 299.2,116.8 374,160.8 448.8,116.8 374,72.8 "/>
|
||||
</g>
|
||||
<polygon style="fill:#926E43;" points="223.6,249.2 223.6,419.6 74.8,333.6 74.8,248.4 149.6,292.4 "/>
|
||||
<polygon style="fill:#8D5122;" points="149.6,116.8 74.8,160.8 149.6,204.8 173.6,218.8 224.4,189.2 224.4,72.8 "/>
|
||||
<polygon style="fill:#77411B;" points="224.4,72.8 224.4,189.2 275.2,218.8 299.2,204.8 374,160.8 299.2,116.8 "/>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 2.4 KiB |
After Width: | Height: | Size: 24 KiB |
After Width: | Height: | Size: 5.8 KiB |
|
@ -61,7 +61,7 @@
|
|||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
|
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
@ -0,0 +1,823 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!DOCTYPE TS>
|
||||
<TS version="2.1" language="es_CO">
|
||||
<context>
|
||||
<name>ButtonsFunction</name>
|
||||
<message>
|
||||
<location filename="../buttonpad.py" line="33"/>
|
||||
<source>SEARCH</source>
|
||||
<translation>BUSCAR</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../buttonpad.py" line="42"/>
|
||||
<source>CUSTOMER</source>
|
||||
<translation>CLIENTE</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../buttonpad.py" line="42"/>
|
||||
<source>CANCEL</source>
|
||||
<translation>CANCELAR</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../buttonpad.py" line="42"/>
|
||||
<source>PRINT</source>
|
||||
<translation>IMPRIMIR</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../buttonpad.py" line="36"/>
|
||||
<source>SALESMAN</source>
|
||||
<translation>VENDEDOR</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../buttonpad.py" line="42"/>
|
||||
<source>GLOBAL DISCOUNT</source>
|
||||
<translation>DESCUENTO GLOBAL</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../buttonpad.py" line="42"/>
|
||||
<source>ORDER</source>
|
||||
<translation>ENV. ORDEN</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../buttonpad.py" line="53"/>
|
||||
<source>NEW SALE</source>
|
||||
<translation>NUEVA VENTA</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../buttonpad.py" line="57"/>
|
||||
<source>PAY MODE</source>
|
||||
<translation>MEDIO DE PAGO</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../buttonpad.py" line="59"/>
|
||||
<source>PAY TERM</source>
|
||||
<translation>PLAZO DE PAGO</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../buttonpad.py" line="66"/>
|
||||
<source>POSITION</source>
|
||||
<translation>POSICION</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../buttonpad.py" line="66"/>
|
||||
<source>NOTE</source>
|
||||
<translation>NOTA</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../buttonpad.py" line="73"/>
|
||||
<source>TIP</source>
|
||||
<translation>PROPINA</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../buttonpad.py" line="73"/>
|
||||
<source>TABLES</source>
|
||||
<translation>MESAS</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../buttonpad.py" line="73"/>
|
||||
<source>RESERVATIONS</source>
|
||||
<translation>RESERVACIONES</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../buttonpad.py" line="42"/>
|
||||
<source>S. SALE</source>
|
||||
<translation>B. VENTA</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../buttonpad.py" line="39"/>
|
||||
<source>WAITER</source>
|
||||
<translation>MESERO</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>MainWindow</name>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="156"/>
|
||||
<source>SYSTEM READY...</source>
|
||||
<translation>SISTEMA LISTO...</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="156"/>
|
||||
<source>DO YOU WANT TO EXIT?</source>
|
||||
<translation>DESEA SALIR?</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="156"/>
|
||||
<source>PLEASE CONFIRM YOUR PAYMENT TERM AS CREDIT?</source>
|
||||
<translation>POR FAVOR CONFIRMAR SI SU PLAZO DE PAGO ES CREDITO?</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="156"/>
|
||||
<source>SALE ORDER / INVOICE NUMBER NOT FOUND!</source>
|
||||
<translation>ORDER / FACTURA DE VENTA NO ENCONTRADA!</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="156"/>
|
||||
<source>THIS SALE IS CLOSED, YOU CAN NOT TO MODIFY!</source>
|
||||
<translation>ESTA VENTA ESTA CERRADA, Y USTED NO PUEDE MODIFICARLA!</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="156"/>
|
||||
<source>DISCOUNT VALUE IS NOT VALID!</source>
|
||||
<translation>EL DESCUENTO NO ES VALIDO!</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="156"/>
|
||||
<source>YOU CAN NOT ADD PAYMENTS TO SALE ON DRAFT STATE!</source>
|
||||
<translation>NO PUEDE AGREGAR PAGOS A UNA VENTA EN BORRADOR!</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="156"/>
|
||||
<source>ENTER QUANTITY...</source>
|
||||
<translation>INGRESE LA CANTIDAD...</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="156"/>
|
||||
<source>ENTER DISCOUNT...</source>
|
||||
<translation>INGRESE EL DESCUENTO...</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="156"/>
|
||||
<source>ENTER PAYMENT AMOUNT BY: %s</source>
|
||||
<translation>INGRESE EL VALOR DEL PAGO EN: %s</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="156"/>
|
||||
<source>ENTER NEW PRICE...</source>
|
||||
<translation>INGRESE EL NUEVO PRECIO...</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="156"/>
|
||||
<source>ORDER SUCCESUFULLY SENT.</source>
|
||||
<translation>ORDEN ENVIADA EXITOSAMENTE.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="156"/>
|
||||
<source>FAILED SEND ORDER!</source>
|
||||
<translation>FALLO EL ENVIO DE LA ORDEN!</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="156"/>
|
||||
<source>MISSING AGENT!</source>
|
||||
<translation>FALTA EL AGENTE!</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="156"/>
|
||||
<source>THERE IS NOT SALESMAN FOR THE SALE!</source>
|
||||
<translation>NO SE DEFINIDO EL VENDEDOR EN LA VENTA!</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="156"/>
|
||||
<source>YOU CAN NOT CONFIRM A SALE WITHOUT PRODUCTS!</source>
|
||||
<translation>NO PUEDE CONFIRMAR UNA VENTA SIN PRODUCTOS!</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="156"/>
|
||||
<source>USER WITHOUT PERMISSION FOR SALE POS!</source>
|
||||
<translation>USUARIO SIN PERMISOS PARA VENTA POS!</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="156"/>
|
||||
<source>THE QUANTITY IS NOT VALID...!</source>
|
||||
<translation>LA CANTIDAD NO ES VALIDAD...!</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="156"/>
|
||||
<source>MISSING THE DEFAULT PARTY ON SHOP CONFIGURATION!</source>
|
||||
<translation>FALTA CONFIGURAR EL TERCERO EN LA TIENDA!</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="156"/>
|
||||
<source>MISSING SET THE JOURNAL ON DEVICE!</source>
|
||||
<translation>FALTA EL ESTADO DE CUENTA PARA LA CAJA!</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="156"/>
|
||||
<source>PRODUCT NOT FOUND!</source>
|
||||
<translation>PRODUCTO NO ENCONTRADO!</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="156"/>
|
||||
<source>DO YOU WANT CREATE NEW SALE?</source>
|
||||
<translation>DESEA CREAR UNA NUEVA VENTA?</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="156"/>
|
||||
<source>ARE YOU WANT TO CANCEL SALE?</source>
|
||||
<translation>DESEA CANCELAR LA VENTA?</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="156"/>
|
||||
<source>AGENT NOT FOUND!</source>
|
||||
<translation>AGENTE NO ENCONTRADO!</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="156"/>
|
||||
<source>COMMISSION NOT VALID!</source>
|
||||
<translation>LA COMISIÓN NO ES VÁLIDA!</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="156"/>
|
||||
<source>CREDIT LIMIT FOR CUSTOMER EXCEED!</source>
|
||||
<translation>EL CLIENTE SUPERA SU CUPO DE CREDITO!</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="156"/>
|
||||
<source>THE CUSTOMER CREDIT CAPACITY IS ABOVE 80%</source>
|
||||
<translation>EL CUPO DE CREDITO DEL CLIENTE ESTA SOBRE EL 80%</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="156"/>
|
||||
<source>YOU CAN NOT FORCE ASSIGN!</source>
|
||||
<translation>NO PUEDE FORZAR UNA ASIGACIÓN!</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="537"/>
|
||||
<source>INVOICE:</source>
|
||||
<translation>FACTURA:</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="1820"/>
|
||||
<source>INVOICE</source>
|
||||
<translation>FACTURA</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="1635"/>
|
||||
<source>PARTY</source>
|
||||
<translation>CLIENTE</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="1628"/>
|
||||
<source>DATE</source>
|
||||
<translation>FECHA</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="1629"/>
|
||||
<source>SALESMAN</source>
|
||||
<translation>VENDEDOR</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="1763"/>
|
||||
<source>PAYMENT TERM</source>
|
||||
<translation>PLAZO DE PAGO</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="597"/>
|
||||
<source>No ORDER</source>
|
||||
<translation>No PEDIDO</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="1864"/>
|
||||
<source>POSITION</source>
|
||||
<translation>POSICION</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="1869"/>
|
||||
<source>AGENT</source>
|
||||
<translation>AGENTE</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="630"/>
|
||||
<source>DELIVERY CHARGE</source>
|
||||
<translation>CARGO DOMICILIO</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="2222"/>
|
||||
<source>SUBTOTAL</source>
|
||||
<translation>SUBTOTAL</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="675"/>
|
||||
<source>TAXES</source>
|
||||
<translation>IMPUESTOS</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="682"/>
|
||||
<source>DISCOUNT</source>
|
||||
<translation>DESCUENTO</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="689"/>
|
||||
<source>TOTAL</source>
|
||||
<translation>TOTAL</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="696"/>
|
||||
<source>PAID</source>
|
||||
<translation>PAGADO</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="703"/>
|
||||
<source>CHANGE</source>
|
||||
<translation>CAMBIO</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="746"/>
|
||||
<source>SHOP</source>
|
||||
<translation>TIENDA</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="746"/>
|
||||
<source>DEVICE</source>
|
||||
<translation>CAJA</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="746"/>
|
||||
<source>DATABASE</source>
|
||||
<translation>BD</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="746"/>
|
||||
<source>USER</source>
|
||||
<translation>USUARIO</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="1812"/>
|
||||
<source>PRINTER</source>
|
||||
<translation>IMPRESORA</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="1869"/>
|
||||
<source>ID</source>
|
||||
<translation>ID</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="2264"/>
|
||||
<source>NUMBER</source>
|
||||
<translation>NUMERO</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="1631"/>
|
||||
<source>TOTAL AMOUNT</source>
|
||||
<translation>VALOR TOTAL</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="1637"/>
|
||||
<source>SEARCH SALES...</source>
|
||||
<translation>BUSCAR VENTAS...</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="1651"/>
|
||||
<source>CODE</source>
|
||||
<translation>CÓDIGO</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="1657"/>
|
||||
<source>STOCK</source>
|
||||
<translation>INVENTARIO</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="2188"/>
|
||||
<source>NAME</source>
|
||||
<translation>NOMBRE</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="2194"/>
|
||||
<source>DESCRIPTION</source>
|
||||
<translation>DESCRIPCIÓN</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="1670"/>
|
||||
<source>BRAND</source>
|
||||
<translation>MARCA</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="1676"/>
|
||||
<source>PRICE</source>
|
||||
<translation>PRECIO</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="1681"/>
|
||||
<source>LOCATION</source>
|
||||
<translation>LOCACIÓN</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="1684"/>
|
||||
<source>IMAGE</source>
|
||||
<translation>IMAGEN</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="1869"/>
|
||||
<source>ID NUMBER</source>
|
||||
<translation>NUMERO ID</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="1704"/>
|
||||
<source>PHONE</source>
|
||||
<translation>TELÉFONO</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="1720"/>
|
||||
<source>PAYMENT MODE:</source>
|
||||
<translation>MEDIO DE PAGO:</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="1722"/>
|
||||
<source>SELECT PAYMENT MODE:</source>
|
||||
<translation>SELECCIONE EL MEDIO DE PAGO:</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="1729"/>
|
||||
<source>WAREHOUSE</source>
|
||||
<translation>BODEGA</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="1729"/>
|
||||
<source>QUANTITY</source>
|
||||
<translation>CANTIDAD</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="1731"/>
|
||||
<source>STOCK BY PRODUCT:</source>
|
||||
<translation>INVENTARIO POR PRODUCTO:</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="1753"/>
|
||||
<source>Id</source>
|
||||
<translation>Id</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="1753"/>
|
||||
<source>Salesman</source>
|
||||
<translation>Vendedor</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="1742"/>
|
||||
<source>CHOOSE SALESMAN</source>
|
||||
<translation>ESCOGE EL VENDEDOR</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="1755"/>
|
||||
<source>CHOOSE TAX</source>
|
||||
<translation>ESCOJA EL IMPUESTO</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="1765"/>
|
||||
<source>SELECT PAYMENT TERM</source>
|
||||
<translation>SELECCIONE EL MODO DE PAGO</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="1811"/>
|
||||
<source>INVOICE NUMBER</source>
|
||||
<translation>NUMERO DE FACTURA</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="1820"/>
|
||||
<source>TYPE</source>
|
||||
<translation>TIPO</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="1820"/>
|
||||
<source>ORDER</source>
|
||||
<translation>PEDIDO</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="1833"/>
|
||||
<source>INSERT PASSWORD FOR CANCEL</source>
|
||||
<translation>INGRESE LA CONTRASEÑA PARA CANCELAR</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="1842"/>
|
||||
<source>GLOBAL DISCOUNT</source>
|
||||
<translation>DESCUENTO GLOBAL</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="1847"/>
|
||||
<source>PASSWORD FORCE ASSIGN</source>
|
||||
<translation>CONTRASEÑA PARA FORZAR ASIGNACIÓN</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="1853"/>
|
||||
<source>VOUCHER NUMBER</source>
|
||||
<translation>NÚMERO DE VOUCHER</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="1879"/>
|
||||
<source>COMMISSION</source>
|
||||
<translation>COMISIÓN</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="2264"/>
|
||||
<source>AMOUNT</source>
|
||||
<translation>VALOR</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="1890"/>
|
||||
<source>COMMENTS</source>
|
||||
<translation>COMENTARIOS</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="2105"/>
|
||||
<source>QUANTITY:</source>
|
||||
<translation>CANTIDAD:</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="2122"/>
|
||||
<source>UNIT PRICE:</source>
|
||||
<translation>PRECIO UNITARIO:</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="2182"/>
|
||||
<source>COD</source>
|
||||
<translation>COD</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="2200"/>
|
||||
<source>UNIT</source>
|
||||
<translation>UND</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="2207"/>
|
||||
<source>QTY</source>
|
||||
<translation>CANT</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="2215"/>
|
||||
<source>DISC</source>
|
||||
<translation>DESC</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="2228"/>
|
||||
<source>NOTE</source>
|
||||
<translation>NOTA</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="2236"/>
|
||||
<source>UNIT PRICE W TAX</source>
|
||||
<translation>PRECIO UNIT CON IMP</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="2264"/>
|
||||
<source>STATEMENT JOURNAL</source>
|
||||
<translation>ESTADO DE CUENTA</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="156"/>
|
||||
<source>THE USER HAVE NOT PERMISSIONS FOR ACCESS TO DEVICE!</source>
|
||||
<translation>EL USUARIO NO TIENE PERMISOS PARA ACCEDER A CAJA!</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="156"/>
|
||||
<source>THERE IS NOT A STATEMENT OPEN FOR THIS DEVICE!</source>
|
||||
<translation>NO HAY ESTADO DE CUENTA ABIERTOS POR ESTA CAJA!</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="156"/>
|
||||
<source>YOU HAVE NOT PERMISSIONS FOR DELETE THIS SALE!</source>
|
||||
<translation>NO TIENE PERMISOS PARA BORRAR ESTA VENTA!</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="156"/>
|
||||
<source>YOU HAVE NOT PERMISSIONS FOR CANCEL THIS SALE!</source>
|
||||
<translation>NO TIENE PERMISOS PARA CANCELAR LA VENTA!</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="156"/>
|
||||
<source>THE CUSTOMER HAS NOT CREDIT!</source>
|
||||
<translation>EL CLIENTE NO TIENE CREDITO!</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="630"/>
|
||||
<source>CUSTOMER</source>
|
||||
<translation>CLIENTE</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="630"/>
|
||||
<source>COMPANY</source>
|
||||
<translation>COMPAÑIA</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="1703"/>
|
||||
<source>ADDRESS</source>
|
||||
<translation>DIRECCION</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="1706"/>
|
||||
<source>SEARCH CUSTOMER</source>
|
||||
<translation>BUSCAR CLIENTE</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="647"/>
|
||||
<source>ASSIGNED TABLE</source>
|
||||
<translation>MESA ASIGNADA</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="156"/>
|
||||
<source>FIRST YOU MUST CREATE/LOAD A SALE!</source>
|
||||
<translation>PRIMERO DEBE AGREGAR/CARGAR UNA VENTA!</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="2095"/>
|
||||
<source>FRACTION:</source>
|
||||
<translation>FRACCIÓN:</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="2244"/>
|
||||
<source>FRAC</source>
|
||||
<translation>FRAC</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../mainwindow.py" line="1859"/>
|
||||
<source>DO YOU WANT TO CONFIRM THE SEND ORDER?</source>
|
||||
<translation>DESEAS CONFIRMAR EL ENVIO DE LA ORDEN?</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>ActionButton</name>
|
||||
<message>
|
||||
<location filename="../commons/buttons.py" line="54"/>
|
||||
<source>&ACCEPT</source>
|
||||
<translation>&ACEPTAR</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../commons/buttons.py" line="56"/>
|
||||
<source>&CANCEL</source>
|
||||
<translation>&CANCELAR</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>FrontWindow</name>
|
||||
<message>
|
||||
<location filename="../commons/frontwindow.py" line="28"/>
|
||||
<source>APPLICATION</source>
|
||||
<translation>APLICACION</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>HelpDialog</name>
|
||||
<message>
|
||||
<location filename="../commons/dialogs.py" line="313"/>
|
||||
<source>Keys Shortcuts...</source>
|
||||
<translation>Atajos de Teclado...</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../commons/dialogs.py" line="322"/>
|
||||
<source>Action</source>
|
||||
<translation>Acción</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../commons/dialogs.py" line="323"/>
|
||||
<source>Shortcut</source>
|
||||
<translation>Atajo</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>Login</name>
|
||||
<message>
|
||||
<location filename="../commons/dblogin.py" line="63"/>
|
||||
<source>HOST</source>
|
||||
<translation>SERVIDOR</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../commons/dblogin.py" line="63"/>
|
||||
<source>DATABASE</source>
|
||||
<translation>BASE DE DATOS</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../commons/dblogin.py" line="63"/>
|
||||
<source>USER</source>
|
||||
<translation>USUARIO</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../commons/dblogin.py" line="63"/>
|
||||
<source>PASSWORD</source>
|
||||
<translation>CONTRASEÑA</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../commons/dblogin.py" line="74"/>
|
||||
<source>C&ANCEL</source>
|
||||
<translation>C&ANCELAR</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../commons/dblogin.py" line="77"/>
|
||||
<source>&CONNECT</source>
|
||||
<translation>&CONECTAR</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../commons/dblogin.py" line="93"/>
|
||||
<source>Error: username or password invalid...!</source>
|
||||
<translation>Error: nombre de usuario o contraseña inválido!</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>MenuButtons</name>
|
||||
<message>
|
||||
<location filename="../commons/menu_buttons.py" line="213"/>
|
||||
<source>Menu...</source>
|
||||
<translation>Menu...</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../commons/menu_buttons.py" line="293"/>
|
||||
<source>&ACCEPT</source>
|
||||
<translation>&ACCEPT</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../commons/menu_buttons.py" line="296"/>
|
||||
<source>&BACK</source>
|
||||
<translation>&BACK</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>QuickDialog</name>
|
||||
<message>
|
||||
<location filename="../commons/dialogs.py" line="34"/>
|
||||
<source>Warning...</source>
|
||||
<translation>Advertencia...</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../commons/dialogs.py" line="35"/>
|
||||
<source>Information...</source>
|
||||
<translation>Información...</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../commons/dialogs.py" line="36"/>
|
||||
<source>Action...</source>
|
||||
<translation>Acción...</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../commons/dialogs.py" line="37"/>
|
||||
<source>Help...</source>
|
||||
<translation>Ayuda...</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../commons/dialogs.py" line="38"/>
|
||||
<source>Error...</source>
|
||||
<translation>Error...</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../commons/dialogs.py" line="39"/>
|
||||
<source>Question...</source>
|
||||
<translation>Pregunta...</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../commons/dialogs.py" line="40"/>
|
||||
<source>Selection...</source>
|
||||
<translation>Selección...</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../commons/dialogs.py" line="41"/>
|
||||
<source>Dialog...</source>
|
||||
<translation>Dialogo...</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>SQLModel</name>
|
||||
<message>
|
||||
<location filename="../commons/search_window.py" line="669"/>
|
||||
<source>Name</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../commons/search_window.py" line="670"/>
|
||||
<source>Salary</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>SearchDialog</name>
|
||||
<message>
|
||||
<location filename="../commons/dialogs.py" line="240"/>
|
||||
<source>Search Products...</source>
|
||||
<translation>Buscar Productos...</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>SearchWindow</name>
|
||||
<message>
|
||||
<location filename="../commons/search_window.py" line="68"/>
|
||||
<source>SEARCH...</source>
|
||||
<translation>BUSCAR...</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../commons/search_window.py" line="172"/>
|
||||
<source>FILTER:</source>
|
||||
<translation>FILTRO:</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>SelectionWindow</name>
|
||||
<message>
|
||||
<location filename="../commons/search_window.py" line="468"/>
|
||||
<source>SEARCH...</source>
|
||||
<translation>BUSCAR...</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../commons/search_window.py" line="490"/>
|
||||
<source>&ACCEPT</source>
|
||||
<translation>&ACEPTAR</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../commons/search_window.py" line="491"/>
|
||||
<source>&RETURN</source>
|
||||
<translation>&VOLVER</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>main</name>
|
||||
<message>
|
||||
<location filename="../commons/dblogin.py" line="188"/>
|
||||
<source>Enter your password:</source>
|
||||
<translation>Ingrese su password:</translation>
|
||||
</message>
|
||||
</context>
|
||||
</TS>
|
4
pospro
|
@ -5,7 +5,7 @@ import sys
|
|||
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PyQt5.QtCore import QTranslator
|
||||
from neox.commons.dblogin import Login
|
||||
from app.commons.dblogin import Login
|
||||
from app import mainwindow
|
||||
|
||||
try:
|
||||
|
@ -17,7 +17,7 @@ except NameError:
|
|||
pass
|
||||
|
||||
locale_app = os.path.join(os.path.abspath(
|
||||
os.path.dirname(__file__)), 'app', 'translations', 'i18n_es.qm')
|
||||
os.path.dirname(__file__)), 'app', 'locale', 'i18n_es.qm')
|
||||
|
||||
|
||||
class Client(object):
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
# Execute in terminal $pylupdate5 project.pro
|
||||
SOURCES = app/mainwindow.py app/reporting.py app/buttonpad.py
|
||||
TRANSLATIONS = app/translations/i18n_es.ts
|
||||
SOURCES = app/mainwindow.py app/reporting.py app/buttonpad.py
|
||||
TRANSLATIONS = app/locale/i18n_es.ts
|
||||
|
|