Merge with neox

This commit is contained in:
Oscar Alvarez 2020-06-11 21:36:27 -05:00
parent 2f1b558ede
commit fef231c557
61 changed files with 6411 additions and 19 deletions

1
.gitignore vendored
View File

@ -26,3 +26,4 @@ package-lock*
/__pycache__
/app/__pycache__
/app/commons/__pycache__

View File

@ -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"

View File

@ -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')))

1
app/commons/__init__.py Normal file
View File

@ -0,0 +1 @@

45
app/commons/action.py Normal file
View File

@ -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

22
app/commons/buttons.py Normal file
View File

@ -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)

20
app/commons/clock.py Normal file
View File

@ -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)

58
app/commons/colors.py Normal file
View File

@ -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,
}
}

63
app/commons/common.py Normal file
View File

@ -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

40
app/commons/config.py Normal file
View File

@ -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)

461
app/commons/connection.py Normal file
View File

@ -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)

View File

@ -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)

217
app/commons/dblogin.py Normal file
View File

@ -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)

347
app/commons/dialogs.py Normal file
View File

@ -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

293
app/commons/forms.py Normal file
View File

@ -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

152
app/commons/frontwindow.py Normal file
View File

@ -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()

View File

@ -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

68
app/commons/image.py Normal file
View File

@ -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()
)

369
app/commons/jsonrpc.py Normal file
View File

@ -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, {})

190
app/commons/menu_buttons.py Normal file
View File

@ -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,
})

192
app/commons/menu_list.py Normal file
View File

@ -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)

46
app/commons/messages.py Normal file
View File

@ -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

242
app/commons/model.py Normal file
View File

@ -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)

View File

@ -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)

690
app/commons/pyson_old.py Normal file
View File

@ -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,
}

29
app/commons/qt_models.py Normal file
View File

@ -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

55
app/commons/rpc.py Normal file
View File

@ -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

View File

@ -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()

View File

@ -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()

65
app/commons/table.py Normal file
View File

@ -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)

1
app/css/__init__.py Normal file
View File

@ -0,0 +1 @@

109
app/css/base.css Normal file
View File

@ -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;
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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);
}

107
app/css/tablet.css Normal file
View File

@ -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;
}

3
app/locale/__init__.py Normal file
View File

@ -0,0 +1,3 @@
import os
locale_path = path_trans = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'i18n_es')

View File

@ -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,

View File

@ -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):

97
app/share/back.svg Normal file
View File

@ -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

BIN
app/share/icon-camera.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

123
app/share/icon-camera.svg Normal file
View File

@ -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

BIN
app/share/icon-error.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

BIN
app/share/icon-help.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

BIN
app/share/icon-info.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

BIN
app/share/icon-print.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
app/share/icon-question.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

BIN
app/share/icon-stock.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

49
app/share/icon-stock.svg Normal file
View File

@ -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

BIN
app/share/icon-warning.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
app/share/login.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

BIN
app/share/menu.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

@ -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

Binary file not shown.

View File

@ -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>&amp;ACCEPT</source>
<translation>&amp;ACEPTAR</translation>
</message>
<message>
<location filename="../commons/buttons.py" line="56"/>
<source>&amp;CANCEL</source>
<translation>&amp;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&amp;ANCEL</source>
<translation>C&amp;ANCELAR</translation>
</message>
<message>
<location filename="../commons/dblogin.py" line="77"/>
<source>&amp;CONNECT</source>
<translation>&amp;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>&amp;ACCEPT</source>
<translation>&amp;ACCEPT</translation>
</message>
<message>
<location filename="../commons/menu_buttons.py" line="296"/>
<source>&amp;BACK</source>
<translation>&amp;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>&amp;ACCEPT</source>
<translation>&amp;ACEPTAR</translation>
</message>
<message>
<location filename="../commons/search_window.py" line="491"/>
<source>&amp;RETURN</source>
<translation>&amp;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
View File

@ -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):

View File

@ -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