├── README.md ├── runtime.txt ├── src ├── orders │ ├── __init__.py │ ├── schemas.py │ ├── views.py │ ├── resources.py │ └── models.py ├── user │ ├── __init__.py │ ├── views.py │ ├── schemas.py │ ├── models.py │ └── resources.py ├── products │ ├── __init__.py │ ├── views.py │ ├── schemas.py │ ├── models.py │ └── resources.py ├── admin_panel │ ├── __init__.py │ └── admin_manager.py ├── utils │ ├── blue_prints.py │ ├── security.py │ ├── __init__.py │ ├── methods.py │ ├── serializer_helper.py │ ├── factory.py │ ├── schema.py │ ├── admin.py │ ├── models.py │ ├── exceptions.py │ ├── operators.py │ ├── api.py │ └── resource.py ├── __init__.py └── config.py ├── Procfile ├── .env ├── .dockerignore ├── tests ├── __main__.py ├── run.py ├── __init__.py └── test_users.py ├── static └── admin │ └── logo.png ├── Dockerfile ├── templates ├── list_template.html └── admin │ └── base.html ├── manager.py ├── requirements.txt ├── .gitignore └── LICENSE /README.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.6.0 -------------------------------------------------------------------------------- /src/orders/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/user/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/products/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: gunicorn manager:app -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | PYTH_SRVR=dev 2 | MYSQL_ROOT_PASSWORD=root -------------------------------------------------------------------------------- /src/admin_panel/__init__.py: -------------------------------------------------------------------------------- 1 | from . import admin_manager 2 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Elastic Beanstalk Files 2 | .elasticbeanstalk/* 3 | .git 4 | .gitignore -------------------------------------------------------------------------------- /tests/__main__.py: -------------------------------------------------------------------------------- 1 | from .run import run 2 | 3 | if __name__ == '__main__': 4 | run() 5 | -------------------------------------------------------------------------------- /static/admin/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saurabh1e/open-pos-api/HEAD/static/admin/logo.png -------------------------------------------------------------------------------- /src/utils/blue_prints.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | 3 | bp = Blueprint('pos', __name__, url_prefix='/api/v1') 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | MAINTAINER "saurabh.1e1@gmail.com" 2 | FROM python:3.5-onbuild 3 | EXPOSE 5000 4 | ENV PYTH_SRVR dev 5 | CMD gunicorn -w 2 -b :5000 manager:app -------------------------------------------------------------------------------- /src/utils/security.py: -------------------------------------------------------------------------------- 1 | from flask_security import Security, SQLAlchemyUserDatastore 2 | from src.user import models 3 | 4 | user_data_store = SQLAlchemyUserDatastore(models.db, models.User, models.Role) 5 | 6 | security = Security(datastore=user_data_store) 7 | -------------------------------------------------------------------------------- /src/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from .api import api, BaseView, AssociationView 2 | from .models import db, ReprMixin, BaseMixin 3 | from .factory import create_app 4 | from .schema import ma, BaseSchema 5 | from .blue_prints import bp 6 | from .admin import admin 7 | from .resource import ModelResource, AssociationModelResource 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/utils/methods.py: -------------------------------------------------------------------------------- 1 | class Create: 2 | method = 'POST' 3 | slug = False 4 | 5 | 6 | class Update: 7 | method = 'PATCH' 8 | slug = True 9 | 10 | 11 | class BulkUpdate: 12 | method = 'PUT' 13 | slug = False 14 | 15 | 16 | class Fetch: 17 | method = 'GET' 18 | slug = True 19 | 20 | 21 | class List: 22 | method = 'GET' 23 | slug = False 24 | 25 | 26 | class Delete: 27 | method = 'DELETE' 28 | slug = True 29 | -------------------------------------------------------------------------------- /src/utils/serializer_helper.py: -------------------------------------------------------------------------------- 1 | # from itsdangerous import URLSafeTimedSerializer 2 | # from . import app 3 | # 4 | # key = app.config['SECRET_KEY'] 5 | # salt = app.config['SECURITY_LOGIN_SALT'] 6 | # time = app.config['MAX_AGE'] 7 | # 8 | # 9 | # def get_serializer(): 10 | # return URLSafeTimedSerializer(key, salt) 11 | # 12 | # 13 | # def serialize_data(data): 14 | # return get_serializer().dumps(data) 15 | # 16 | # 17 | # def deserialize_data(data): 18 | # return get_serializer().loads(data, time) 19 | -------------------------------------------------------------------------------- /src/utils/factory.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from flask_cors import CORS 3 | 4 | 5 | def create_app(package_name, config, blueprints=None, extensions=None): 6 | app = Flask(package_name) 7 | app.config.from_object(config) 8 | config.init_app(app) 9 | CORS(app) 10 | if blueprints: 11 | for bp in blueprints: 12 | app.register_blueprint(bp) 13 | if extensions: 14 | for extension in extensions: 15 | 16 | extension.init_app(app) 17 | 18 | return app 19 | -------------------------------------------------------------------------------- /tests/run.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import unittest 3 | 4 | from coverage import coverage 5 | 6 | 7 | def run(): 8 | cov = coverage(source=['flask_testing']) 9 | cov.start() 10 | 11 | from tests import suite 12 | result = unittest.TextTestRunner(verbosity=2).run(suite()) 13 | if not result.wasSuccessful(): 14 | sys.exit(1) 15 | cov.stop() 16 | print("\nCode Coverage") 17 | cov.report() 18 | cov.html_report(directory='cover') 19 | 20 | if __name__ == '__main__': 21 | run() 22 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from .test_users import TestSetup\ 3 | , TestSetupFailure, TestRole, TestUser, TestUserRole 4 | 5 | 6 | def suite(): 7 | test_suite = unittest.TestSuite() 8 | test_suite.addTest(unittest.makeSuite(TestSetup)) 9 | test_suite.addTest(unittest.makeSuite(TestSetupFailure)) 10 | test_suite.addTest(unittest.makeSuite(TestUser)) 11 | test_suite.addTest(unittest.makeSuite(TestRole)) 12 | test_suite.addTest(unittest.makeSuite(TestUserRole)) 13 | return test_suite 14 | -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- 1 | from .config import configs 2 | from .utils import api, db, ma, create_app, ReprMixin, bp, BaseMixin, admin, BaseSchema, BaseView, AssociationView 3 | 4 | 5 | from .products import models 6 | from .orders import models 7 | from .user import models 8 | from .products import schemas 9 | from .orders import schemas 10 | from .user import schemas 11 | from .products import views 12 | from .orders import views 13 | from .user import views 14 | from .utils.security import security 15 | from .admin_panel import admin_manager 16 | -------------------------------------------------------------------------------- /src/utils/schema.py: -------------------------------------------------------------------------------- 1 | from flask_marshmallow import Marshmallow 2 | from marshmallow_sqlalchemy import ModelSchema, ModelSchemaOpts 3 | from .models import db 4 | 5 | 6 | class FlaskMarshmallowFactory(Marshmallow): 7 | 8 | def __init__(self, *args, **kwargs): 9 | super(FlaskMarshmallowFactory, self).__init__(*args, **kwargs) 10 | 11 | 12 | ma = FlaskMarshmallowFactory() 13 | 14 | 15 | class BaseOpts(ModelSchemaOpts): 16 | def __init__(self, meta): 17 | if not hasattr(meta, 'sql_session'): 18 | meta.sqla_session = db.session 19 | super(BaseOpts, self).__init__(meta) 20 | 21 | 22 | class BaseSchema(ModelSchema): 23 | OPTIONS_CLASS = BaseOpts 24 | -------------------------------------------------------------------------------- /src/utils/admin.py: -------------------------------------------------------------------------------- 1 | from flask import url_for, redirect 2 | from flask_admin import Admin, AdminIndexView 3 | from flask_admin import expose 4 | from flask_security import current_user 5 | 6 | 7 | class MyAdminIndexView(AdminIndexView): 8 | 9 | def __init__(self, endpoint=None, url=None,): 10 | super(MyAdminIndexView, self).__init__(url=url, endpoint=endpoint) 11 | 12 | @expose('/') 13 | def index(self): 14 | if not current_user.is_authenticated: 15 | return redirect(url_for('security.login')) 16 | return super(MyAdminIndexView, self).index() 17 | 18 | admin = Admin(name="Open POS", template_mode='bootstrap3', index_view=MyAdminIndexView(url='/admin')) 19 | -------------------------------------------------------------------------------- /templates/list_template.html: -------------------------------------------------------------------------------- 1 | {% extends 'admin/model/list.html' %} 2 | 3 | {% block model_menu_bar %} 4 | {{ super() }} 5 |
6 |
7 |
8 |
9 |
11 |
12 | 13 |
14 |
15 | 16 |
17 |
18 |
19 |
20 |
21 | 22 | 23 | {% endblock %} 24 | -------------------------------------------------------------------------------- /manager.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from flask_migrate import Migrate, MigrateCommand 4 | import urllib.parse as up 5 | from flask_script import Manager 6 | from flask import url_for 7 | 8 | from src import api, db, ma, create_app, configs, bp, security, admin 9 | 10 | config = os.environ.get('PYTH_SRVR') 11 | 12 | config = configs.get(config, 'default') 13 | 14 | extensions = [api, db, ma, security, admin] 15 | bps = [bp] 16 | 17 | app = create_app(__name__, config, extensions=extensions, blueprints=bps) 18 | 19 | manager = Manager(app) 20 | migrate = Migrate(app, db) 21 | manager.add_command('db', MigrateCommand) 22 | 23 | 24 | @manager.shell 25 | def _shell_context(): 26 | return dict( 27 | app=app, 28 | db=db, 29 | ma=ma, 30 | config=config 31 | ) 32 | 33 | 34 | @manager.command 35 | def list_routes(): 36 | output = [] 37 | for rule in app.url_map.iter_rules(): 38 | options = {} 39 | for arg in rule.arguments: 40 | options[arg] = "[{0}]".format(arg) 41 | methods = ','.join(rule.methods) 42 | url = url_for(rule.endpoint, **options) 43 | line = up.unquote("{:50s} {:20s} {}".format(rule.endpoint, methods, url)) 44 | output.append(line) 45 | 46 | for line in sorted(output): 47 | print(line) 48 | 49 | if __name__ == "__main__": 50 | manager.run() 51 | -------------------------------------------------------------------------------- /src/utils/models.py: -------------------------------------------------------------------------------- 1 | import re 2 | from flask_sqlalchemy import SQLAlchemy 3 | from sqlalchemy.ext.declarative import declared_attr 4 | from sqlalchemy.dialects.postgresql import UUID 5 | from sqlalchemy import text 6 | 7 | db = SQLAlchemy() 8 | 9 | 10 | def to_underscore(name): 11 | 12 | s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name) 13 | return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower() 14 | 15 | 16 | class BaseMixin(object): 17 | 18 | @declared_attr 19 | def __tablename__(self): 20 | return to_underscore(self.__name__) 21 | 22 | __mapper_args__ = {'always_refresh': True} 23 | 24 | id = db.Column(UUID(as_uuid=False), index=True, primary_key=True, server_default=text("uuid_generate_v4()")) 25 | created_on = db.Column(db.TIMESTAMP, server_default=text("current_timestamp")) 26 | updated_on = db.Column(db.TIMESTAMP, onupdate=db.func.current_timestamp(), 27 | server_default=text("current_timestamp")) 28 | 29 | 30 | class ReprMixin(object): 31 | """Provides a string representible form for objects.""" 32 | 33 | __repr_fields__ = ['id', 'name'] 34 | 35 | def __repr__(self): 36 | fields = {f: getattr(self, f, '') for f in self.__repr_fields__} 37 | pattern = ['{0}={{{0}}}'.format(f) for f in self.__repr_fields__] 38 | pattern = ' '.join(pattern) 39 | pattern = pattern.format(**fields) 40 | return '<{} {}>'.format(self.__class__.__name__, pattern) 41 | -------------------------------------------------------------------------------- /src/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | basedir = os.path.abspath(os.path.dirname(__file__)) 4 | 5 | 6 | class BaseConfig: 7 | SQLALCHEMY_COMMIT_ON_TEARDOWN = True 8 | SQLALCHEMY_RECORD_QUERIES = False 9 | 10 | MARSHMALLOW_STRICT = True 11 | MARSHMALLOW_DATEFORMAT = 'rfc' 12 | 13 | SECRET_KEY = 'test_key' 14 | SECURITY_LOGIN_SALT = 'test' 15 | SECURITY_PASSWORD_HASH = 'pbkdf2_sha512' 16 | SECURITY_TRACKABLE = True 17 | SECURITY_PASSWORD_SALT = 'something_super_secret_change_in_production' 18 | WTF_CSRF_ENABLED = False 19 | SECURITY_LOGIN_URL = '/api/v1/login/' 20 | SECURITY_LOGOUT_URL = '/api/v1/logout/' 21 | SECURITY_REGISTER_URL = '/api/v1/register/' 22 | SECURITY_RESET_URL = '/api/v1/reset/' 23 | SECURITY_CONFIRM_URL = '/api/v1/confirm/' 24 | SECURITY_REGISTERABLE = True 25 | SECURITY_CONFIRMABLE = True 26 | SECURITY_RECOVERABLE = True 27 | SECURITY_POST_LOGIN_VIEW = '/admin/' 28 | SECURITY_TOKEN_AUTHENTICATION_HEADER = 'Authorization' 29 | MAX_AGE = 86400 30 | 31 | @staticmethod 32 | def init_app(app): 33 | pass 34 | 35 | SQLALCHEMY_TRACK_MODIFICATIONS = False 36 | 37 | 38 | class DevConfig(BaseConfig): 39 | DEBUG = True 40 | TESTING = True 41 | SQLALCHEMY_DATABASE_URI = 'postgresql+pygresql://postgres:@localhost/pos' 42 | 43 | 44 | class TestConfig(BaseConfig): 45 | TESTING = True 46 | DEBUG = True 47 | SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URI') 48 | 49 | 50 | class ProdConfig(BaseConfig): 51 | SQLALCHEMY_DATABASE_URI = os.environ.get('PROD_DATABASE_URI') or \ 52 | 'sqlite:///{}'.format(os.path.join(basedir, 'why-is-prod-here.db')) 53 | 54 | 55 | configs = { 56 | 'dev': DevConfig, 57 | 'testing': TestConfig, 58 | 'prod': ProdConfig, 59 | 'default': DevConfig 60 | } 61 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | ## The following requirements were added by pip freeze: 2 | alembic==0.9.1 3 | aniso8601==1.2.0 4 | appdirs==1.4.3 5 | backports.shutil-get-terminal-size==1.0.0 6 | base58==0.2.4 7 | bcrypt==3.1.3 8 | bleach==2.0.0 9 | blinker==1.4 10 | boto3==1.4.4 11 | botocore==1.5.24 12 | cffi==1.9.1 13 | click==6.7 14 | coverage==4.3.4 15 | decorator==4.0.11 16 | docutils==0.13.1 17 | entrypoints==0.2.2 18 | et-xmlfile==1.0.1 19 | Flask==0.12 20 | Flask-Admin==1.4.2 21 | -e git://github.com/saurabh1e/flask_admin_impexp.git@master#egg=flask_admin_impexp 22 | Flask-Cors==3.0.2 23 | Flask-Excel==0.0.5 24 | Flask-Login==0.3.2 25 | Flask-Mail==0.9.1 26 | flask-marshmallow==0.7.0 27 | Flask-Migrate==2.0.3 28 | Flask-Principal==0.4.0 29 | Flask-RESTful==0.3.5 30 | Flask-Script==2.0.5 31 | Flask-Security==1.7.5 32 | Flask-SQLAlchemy==2.2 33 | Flask-Testing==0.6.2 34 | Flask-WTF==0.14.2 35 | future==0.16.0 36 | futures==3.0.5 37 | gunicorn==19.7.0 38 | hjson==2.0.2 39 | html5lib==0.999999999 40 | ipykernel==4.5.2 41 | ipython==5.3.0 42 | ipython-genutils==0.1.0 43 | ipywidgets==6.0.0 44 | itsdangerous==0.24 45 | jdcal==1.3 46 | Jinja2==2.9.5 47 | jmespath==0.9.2 48 | jsonschema==2.6.0 49 | jupyter==1.0.0 50 | jupyter-client==5.0.0 51 | jupyter-console==5.1.0 52 | jupyter-core==4.3.0 53 | kappa==0.7.0 54 | lambda-packages==0.10.0 55 | Mako==1.0.6 56 | MarkupSafe==1.0 57 | marshmallow==2.2.0 58 | marshmallow-sqlalchemy==0.12.1 59 | mistune==0.7.3 60 | nbconvert==5.1.1 61 | nbformat==4.3.0 62 | notebook==4.4.1 63 | openpyxl==2.4.5 64 | packaging==16.8 65 | pandocfilters==1.4.1 66 | passlib==1.7.1 67 | pexpect==4.2.1 68 | pickleshare==0.7.4 69 | placebo==0.8.1 70 | prompt-toolkit==1.0.13 71 | ptyprocess==0.5.1 72 | py==1.4.32 73 | pycparser==2.17 74 | pycrypto==2.6.1 75 | pycryptodome==3.4.5 76 | pyexcel==0.4.4 77 | pyexcel-io==0.3.2 78 | pyexcel-webio==0.0.11 79 | Pygments==2.2.0 80 | PyGreSQL==5.0.3 81 | pyparsing==2.2.0 82 | pytest==3.0.6 83 | python-dateutil==2.6.0 84 | python-editor==1.0.3 85 | python-slugify==1.2.1 86 | pytz==2016.10 87 | PyYAML==3.12 88 | pyzmq==16.0.2 89 | qtconsole==4.2.1 90 | requests==2.13.0 91 | s3transfer==0.1.10 92 | simplegeneric==0.8.1 93 | six==1.10.0 94 | SQLAlchemy==1.1.6 95 | tablib==0.11.4 96 | terminado==0.6 97 | testpath==0.3 98 | texttable==0.8.7 99 | tornado==4.4.2 100 | traitlets==4.3.2 101 | troposphere==1.9.2 102 | Unidecode==0.4.20 103 | uuid==1.30 104 | wcwidth==0.1.7 105 | webencodings==0.5 106 | Werkzeug==0.12 107 | widgetsnbextension==2.0.0 108 | WTForms==2.1 109 | -------------------------------------------------------------------------------- /src/utils/exceptions.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.exc import OperationalError, IntegrityError 2 | 3 | 4 | class SQlOperationalError(OperationalError): 5 | def __init__(self, data, message, operation, status): 6 | 7 | self.message = self.construct_error_message(data, message, operation) 8 | self.status = status 9 | 10 | def _get_message(self): 11 | return self._message 12 | 13 | def _set_message(self, message): 14 | self._message = message 15 | 16 | message = property(_get_message, _set_message) 17 | 18 | def _get_status(self): 19 | return self._status 20 | 21 | def _set_status(self, status): 22 | self._status = status 23 | 24 | status = property(_get_status, _set_status) 25 | 26 | @staticmethod 27 | def construct_error_message(data, message, operation): 28 | return {'data': data, 'message': message, 'operation': operation} 29 | 30 | 31 | class SQLIntegrityError(IntegrityError): 32 | def __init__(self, data, message, operation, status): 33 | 34 | self.message = self.construct_error_message(data, message, operation) 35 | self.status = status 36 | 37 | def _get_message(self): 38 | return self._message 39 | 40 | def _set_message(self, message): 41 | self._message = message 42 | 43 | message = property(_get_message, _set_message) 44 | 45 | def _get_status(self): 46 | return self._status 47 | 48 | def _set_status(self, status): 49 | self._status = status 50 | 51 | status = property(_get_status, _set_status) 52 | 53 | @staticmethod 54 | def construct_error_message(data, message, operation): 55 | return {'data': data, 'message': message, 'operation': operation} 56 | 57 | 58 | class CustomException(Exception): 59 | 60 | def __init__(self, data, message, operation, status=400): 61 | 62 | self.message = self.construct_error_message(data, message, operation) 63 | self.status = status 64 | 65 | def _get_message(self): 66 | return self._message 67 | 68 | def _set_message(self, message): 69 | self._message = message 70 | 71 | message = property(_get_message, _set_message) 72 | 73 | def _get_status(self): 74 | return self._status 75 | 76 | def _set_status(self, status): 77 | self._status = status 78 | 79 | status = property(_get_status, _set_status) 80 | 81 | @staticmethod 82 | def construct_error_message(data, message, operation): 83 | return {'data': data, 'message': message, 'operation': operation} 84 | 85 | 86 | class ResourceNotFound(CustomException): 87 | 88 | status = 404 89 | 90 | 91 | class RequestNotAllowed(CustomException): 92 | 93 | status = 401 94 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Example user template template 2 | ### Example user template 3 | 4 | migrations/ 5 | *.db 6 | 7 | # IntelliJ project files 8 | .idea 9 | *.iml 10 | out 11 | gen### IPythonNotebook template 12 | # Temporary data 13 | .ipynb_checkpoints/ 14 | ### VirtualEnv template 15 | # Virtualenv 16 | # http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ 17 | .Python 18 | [Bb]in 19 | [Ii]nclude 20 | [Ll]ib 21 | [Ss]cripts 22 | pyvenv.cfg 23 | pip-selfcheck.json 24 | ### JetBrains template 25 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio 26 | 27 | *.iml 28 | 29 | ## Directory-based project format: 30 | .idea/ 31 | # if you remove the above rule, at least ignore the following: 32 | 33 | # User-specific stuff: 34 | # .idea/workspace.xml 35 | # .idea/tasks.xml 36 | # .idea/dictionaries 37 | 38 | # Sensitive or high-churn files: 39 | # .idea/dataSources.ids 40 | # .idea/dataSources.xml 41 | # .idea/sqlDataSources.xml 42 | # .idea/dynamic.xml 43 | # .idea/uiDesigner.xml 44 | 45 | # Gradle: 46 | # .idea/gradle.xml 47 | # .idea/libraries 48 | 49 | # Mongo Explorer plugin: 50 | # .idea/mongoSettings.xml 51 | 52 | ## File-based project format: 53 | *.ipr 54 | *.iws 55 | 56 | ## Plugin-specific files: 57 | 58 | # IntelliJ 59 | /out/ 60 | 61 | # mpeltonen/sbt-idea plugin 62 | .idea_modules/ 63 | 64 | # JIRA plugin 65 | atlassian-ide-plugin.xml 66 | 67 | # Crashlytics plugin (for Android Studio and IntelliJ) 68 | com_crashlytics_export_strings.xml 69 | crashlytics.properties 70 | crashlytics-build.properties 71 | ### Python template 72 | # Byte-compiled / optimized / DLL files 73 | __pycache__/ 74 | *.py[cod] 75 | *$py.class 76 | 77 | # C extensions 78 | *.so 79 | 80 | # Distribution / packaging 81 | .Python 82 | env/ 83 | build/ 84 | develop-eggs/ 85 | dist/ 86 | downloads/ 87 | eggs/ 88 | .eggs/ 89 | lib/ 90 | lib64/ 91 | parts/ 92 | sdist/ 93 | var/ 94 | *.egg-info/ 95 | .installed.cfg 96 | *.egg 97 | 98 | # PyInstaller 99 | # Usually these files are written by a python script from a template 100 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 101 | *.manifest 102 | *.spec 103 | 104 | # Installer logs 105 | pip-log.txt 106 | pip-delete-this-directory.txt 107 | 108 | # Unit test / coverage reports 109 | htmlcov/ 110 | .tox/ 111 | .coverage 112 | .coverage.* 113 | .cache 114 | nosetests.xml 115 | coverage.xml 116 | *,cover 117 | 118 | # Translations 119 | *.mo 120 | *.pot 121 | 122 | # Django stuff: 123 | *.log 124 | 125 | # Sphinx documentation 126 | docs/_build/ 127 | 128 | # PyBuilder 129 | target/ 130 | 131 | # Created by .ignore support plugin (hsz.mobi) 132 | venv 133 | 134 | tests/cover 135 | */migrations 136 | # Elastic Beanstalk Files 137 | .elasticbeanstalk/* 138 | !.elasticbeanstalk/*.cfg.yml 139 | !.elasticbeanstalk/*.global.yml 140 | -------------------------------------------------------------------------------- /src/products/views.py: -------------------------------------------------------------------------------- 1 | from src import BaseView, AssociationView 2 | from src import api 3 | from .resources import BrandResource, DistributorBillResource, DistributorResource, ProductResource, \ 4 | ProductTaxResource, StockResource, TaxResource, TagResource, ComboResource, AddOnResource, SaltResource,\ 5 | ProductDistributorResource, ProductTagResource, ProductSaltResource, BrandDistributorResource 6 | 7 | 8 | @api.register() 9 | class ProductView(BaseView): 10 | 11 | @classmethod 12 | def get_resource(cls): 13 | return ProductResource 14 | 15 | 16 | @api.register() 17 | class TagView(BaseView): 18 | 19 | @classmethod 20 | def get_resource(cls): 21 | return TagResource 22 | 23 | 24 | @api.register() 25 | class StockView(BaseView): 26 | 27 | @classmethod 28 | def get_resource(cls): 29 | return StockResource 30 | 31 | 32 | @api.register() 33 | class DistributorView(BaseView): 34 | 35 | @classmethod 36 | def get_resource(cls): 37 | return DistributorResource 38 | 39 | 40 | @api.register() 41 | class DistributorBillView(BaseView): 42 | 43 | @classmethod 44 | def get_resource(cls): 45 | return DistributorBillResource 46 | 47 | 48 | @api.register() 49 | class TaxView(BaseView): 50 | 51 | @classmethod 52 | def get_resource(cls): 53 | return TaxResource 54 | 55 | 56 | @api.register() 57 | class BrandView(BaseView): 58 | 59 | @classmethod 60 | def get_resource(cls): 61 | return BrandResource 62 | 63 | 64 | @api.register() 65 | class ComboView(BaseView): 66 | 67 | @classmethod 68 | def get_resource(cls): 69 | return ComboResource 70 | 71 | 72 | @api.register() 73 | class AddOnView(BaseView): 74 | 75 | @classmethod 76 | def get_resource(cls): 77 | return AddOnResource 78 | 79 | 80 | @api.register() 81 | class SaltView(BaseView): 82 | 83 | @classmethod 84 | def get_resource(cls): 85 | return SaltResource 86 | 87 | 88 | @api.register() 89 | class ProductTaxAssociationView(AssociationView): 90 | 91 | @classmethod 92 | def get_resource(cls): 93 | return ProductTaxResource 94 | 95 | 96 | @api.register() 97 | class ProductDistributorAssociationView(AssociationView): 98 | 99 | @classmethod 100 | def get_resource(cls): 101 | return ProductDistributorResource 102 | 103 | 104 | @api.register() 105 | class ProductTagAssociationView(AssociationView): 106 | 107 | @classmethod 108 | def get_resource(cls): 109 | return ProductTagResource 110 | 111 | 112 | @api.register() 113 | class ProductSaltAssociationView(AssociationView): 114 | 115 | @classmethod 116 | def get_resource(cls): 117 | return ProductSaltResource 118 | 119 | 120 | @api.register() 121 | class BrandDistributorAssociationView(AssociationView): 122 | 123 | @classmethod 124 | def get_resource(cls): 125 | return BrandDistributorResource 126 | -------------------------------------------------------------------------------- /templates/admin/base.html: -------------------------------------------------------------------------------- 1 | {% import 'admin/layout.html' as layout with context -%} 2 | {% import 'admin/static.html' as admin_static with context %} 3 | 4 | 5 | 6 | {% block title %}{% if admin_view.category %}{{ admin_view.category }} - {% endif %}{{ admin_view.name }} - {{ admin_view.admin.name }}{% endblock %} 7 | {% block head_meta %} 8 | 9 | 10 | 11 | 12 | 13 | {% endblock %} 14 | {% block head_css %} 15 | 16 | 17 | 18 | 23 | {% endblock %} 24 | {% block head %} 25 | {% endblock %} 26 | {% block head_tail %} 27 | {% endblock %} 28 | 29 | 30 | {% block page_body %} 31 |
32 | 73 | 74 | {% block messages %} 75 | {{ layout.messages() }} 76 | {% endblock %} 77 | 78 | {# store the jinja2 context for form_rules rendering logic #} 79 | {% set render_ctx = h.resolve_ctx() %} 80 | 81 | {% block body %}{% endblock %} 82 |
83 | {% endblock %} 84 | 85 | {% block tail_js %} 86 | 87 | 88 | 89 | 90 | {% endblock %} 91 | 92 | {% block tail %} 93 | {% endblock %} 94 | 95 | 96 | -------------------------------------------------------------------------------- /src/utils/operators.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractstaticmethod 2 | from datetime import datetime 3 | from sqlalchemy import func 4 | from sqlalchemy import cast, Date 5 | 6 | 7 | class Operators(ABC): 8 | op = 'equal' 9 | 10 | @staticmethod 11 | @abstractstaticmethod 12 | def prepare_queryset(query, model, key, value): 13 | return query.filter(getattr(model, key) == value) 14 | 15 | 16 | class In(Operators): 17 | op = 'in' 18 | 19 | @staticmethod 20 | def prepare_queryset(query, model, key, values): 21 | if len(values) == 1: 22 | values = values[0].split(',') 23 | return query.filter(getattr(model, key).in_(values)) 24 | 25 | 26 | class NotIn(Operators): 27 | op = 'not_in' 28 | 29 | @staticmethod 30 | def prepare_queryset(query, model, key, values): 31 | if len(values) == 1: 32 | values = values[0].split(',') 33 | return query.filter(~getattr(model, key).in_(values)) 34 | 35 | 36 | class Equal(Operators): 37 | op = 'equal' 38 | 39 | @staticmethod 40 | def prepare_queryset(query, model, key, value): 41 | return query.filter(getattr(model, key) == value[0]) 42 | 43 | 44 | class NotEqual(Operators): 45 | op = 'ne' 46 | 47 | @staticmethod 48 | def prepare_queryset(query, model, key, value): 49 | return query.filter(getattr(model, key) != value[0]) 50 | 51 | 52 | class Contains(Operators): 53 | op = 'contains' 54 | 55 | @staticmethod 56 | def prepare_queryset(query, model, key, value): 57 | return query.filter(func.lower(getattr(model, key)).contains(value[0].lower())) 58 | 59 | 60 | class Boolean(Operators): 61 | op = 'bool' 62 | 63 | @staticmethod 64 | def prepare_queryset(query, model, key, value): 65 | val = False if value[0] == 'false' else True 66 | return query.filter(getattr(model, key) == val) 67 | 68 | 69 | class Between(Operators): 70 | op = 'between' 71 | 72 | @staticmethod 73 | def prepare_queryset(query, model, key, value): 74 | val1 = value[0] 75 | val2 = value[1] 76 | return query.filter(getattr(model, key).between(val1, val2)) 77 | 78 | 79 | class Greater(Operators): 80 | op = 'gt' 81 | 82 | @staticmethod 83 | def prepare_queryset(query, model, key, value): 84 | return query.filter(getattr(model, key) > value[0]) 85 | 86 | 87 | class Lesser(Operators): 88 | op = 'lt' 89 | 90 | @staticmethod 91 | def prepare_queryset(query, model, key, value): 92 | return query.filter(getattr(model, key) < value[0]) 93 | 94 | 95 | class Greaterequal(Operators): 96 | op = 'gte' 97 | 98 | @staticmethod 99 | def prepare_queryset(query, model, key, value): 100 | return query.filter(getattr(model, key) >= value[0]) 101 | 102 | 103 | class LesserEqual(Operators): 104 | op = 'lte' 105 | 106 | @staticmethod 107 | def prepare_queryset(query, model, key, value): 108 | return query.filter(getattr(model, key) <= value[0]) 109 | 110 | 111 | class DateEqual(Operators): 112 | op = 'date_equal' 113 | 114 | @staticmethod 115 | def prepare_queryset(query, model, key, value): 116 | return query.filter(cast(getattr(model, key), Date) == datetime.strptime(value[0], '%Y-%m-%dT%H:%M:%S.%fZ').date()) 117 | 118 | 119 | class DateGreaterEqual(Operators): 120 | op = 'date_gte' 121 | 122 | @staticmethod 123 | def prepare_queryset(query, model, key, value): 124 | return query.filter(cast(getattr(model, key), Date) >= datetime.strptime(value[0], '%Y-%m-%dT%H:%M:%S.%fZ').date()) 125 | 126 | 127 | class DateLesserEqual(Operators): 128 | op = 'date_lte' 129 | 130 | @staticmethod 131 | def prepare_queryset(query, model, key, value): 132 | return query.filter(cast(getattr(model, key), Date) <= datetime.strptime(value[0], '%Y-%m-%dT%H:%M:%S.%fZ').date()) 133 | 134 | 135 | class DateBetween(Operators): 136 | op = 'date_btw' 137 | 138 | @staticmethod 139 | def prepare_queryset(query, model, key, values): 140 | if len(values) == 1: 141 | values = values[0].split(',') 142 | return query.filter(cast(getattr(model, key), Date) 143 | .between(datetime.strptime(values[0], '%Y-%m-%dT%H:%M:%S.%fZ').date(), 144 | datetime.strptime(values[1], '%Y-%m-%dT%H:%M:%S.%fZ').date())) 145 | -------------------------------------------------------------------------------- /src/orders/schemas.py: -------------------------------------------------------------------------------- 1 | from marshmallow import post_load 2 | from flask_security import current_user 3 | from sqlalchemy import func 4 | from src import ma, BaseSchema 5 | from .models import Order, Item, ItemTax, OrderDiscount, Status, ItemAddOn, Discount 6 | 7 | 8 | class OrderSchema(BaseSchema): 9 | class Meta: 10 | model = Order 11 | 12 | edit_stock = ma.Boolean() 13 | sub_total = ma.Float(precision=2) 14 | total = ma.Float(precision=2) 15 | 16 | retail_shop_id = ma.UUID(load=True, required=True) 17 | reference_number = ma.String(load=True, required=False, partial=True) 18 | customer_id = ma.UUID(load=True, required=False, allow_none=True) 19 | address_id = ma.UUID(load=True, required=False, partial=True, allow_none=True) 20 | discount_id = ma.UUID() 21 | current_status_id = ma.UUID(load=True) 22 | user_id = ma.UUID(dump_only=True) 23 | items_count = ma.Integer(dump_only=True) 24 | amount_due = ma.Integer() 25 | invoice_number = ma.Integer(dump_only=True, allow_none=True) 26 | 27 | items = ma.Nested('ItemSchema', many=True, exclude=('order', 'order_id'), load=True) 28 | retail_shop = ma.Nested('RetailShopSchema', many=False, only=('id', 'name')) 29 | customer = ma.Nested('CustomerSchema', many=False, dump_only=True, only=['id', 'name', 'mobile_number']) 30 | created_by = ma.Nested('UserSchema', many=False, dump_only=True, only=['id', 'name']) 31 | address = ma.Nested('AddressSchema', many=False, dump_only=True, only=['id', 'name']) 32 | discounts = ma.Nested('DiscountSchema', many=True, load=True) 33 | current_status = ma.Nested('StatusSchema', many=False, dump_only=True) 34 | 35 | @post_load 36 | def save_data(self, obj): 37 | obj.user_id = current_user.id 38 | print(Order.query.with_entities(func.Count(Order.id)).filter(Order.retail_shop_id == obj.retail_shop_id).scalar()+1) 39 | obj.invoice_number = Order.query.with_entities(func.Count(Order.id)).filter(Order.retail_shop_id == obj.retail_shop_id).scalar()+1 40 | if obj.current_status_id is None: 41 | obj.current_status_id = Status.query.with_entities(Status.id).filter(Status.name == 'PLACED').first() 42 | return obj 43 | 44 | 45 | class ItemSchema(BaseSchema): 46 | class Meta: 47 | model = Item 48 | exclude = ('created_on', 'updated_on') 49 | 50 | product_id = ma.UUID(load=True, required=True) 51 | unit_price = ma.Float(precision=2) 52 | quantity = ma.Float(precision=2) 53 | order_id = ma.UUID() 54 | stock_id = ma.UUID() 55 | discount = ma.Float() 56 | discounted_total_price = ma.Float(dump_only=True) 57 | discounted_unit_price = ma.Float(dump_only=True) 58 | total_price = ma.Float(dump_only=True) 59 | discount_amount = ma.Float(dump_only=True) 60 | children = ma.Nested('self', many=True, default=None, load=True, exclude=('parent',)) 61 | product = ma.Nested('ProductSchema', many=False, 62 | only=('id', 'name')) 63 | combo = ma.Nested('ComboSchema', many=False, only=('id', 'name')) 64 | taxes = ma.Nested('ItemTaxSchema', many=True, exclude=('item',)) 65 | add_ons = ma.Nested('AddOnSchema', many=True, exclude=('item',)) 66 | 67 | 68 | class ItemTaxSchema(BaseSchema): 69 | class Meta: 70 | model = ItemTax 71 | exclude = ('created_on', 'updated_on') 72 | 73 | tax_value = ma.Float(precision=2) 74 | 75 | item_id = ma.UUID(load=True) 76 | tax_id = ma.UUID(load=True) 77 | 78 | tax = ma.Nested('TaxSchema', many=False, only=('id', 'name')) 79 | item = ma.Nested('ItemSchema', many=False) 80 | 81 | 82 | class OrderDiscountSchema(BaseSchema): 83 | class Meta: 84 | model = OrderDiscount 85 | exclude = ('created_on', 'updated_on') 86 | 87 | name = ma.String() 88 | amount = ma.Float(precision=2) 89 | 90 | 91 | class ItemAddOnSchema(BaseSchema): 92 | class Meta: 93 | model = ItemAddOn 94 | exclude = ('created_on', 'updated_on') 95 | 96 | name = ma.String() 97 | amount = ma.Float(precision=2) 98 | 99 | 100 | class StatusSchema(BaseSchema): 101 | class Meta: 102 | model = Status 103 | exclude = ('created_on', 'updated_on') 104 | 105 | name = ma.String() 106 | amount = ma.Float(precision=2) 107 | 108 | 109 | class DiscountSchema(BaseSchema): 110 | class Meta: 111 | model = Discount 112 | exclude = ('created_on', 'updated_on') 113 | 114 | name = ma.String() 115 | amount = ma.Float(precision=2) 116 | -------------------------------------------------------------------------------- /src/user/views.py: -------------------------------------------------------------------------------- 1 | from flask_restful import Resource 2 | from flask_security.utils import verify_and_update_password, login_user 3 | from flask import request, jsonify, make_response, redirect 4 | from src import BaseView, AssociationView 5 | from src.utils.methods import List 6 | from .resources import UserResource, UserRoleResource, RoleResource,\ 7 | RetailBrandResource, RetailShopResource, UserRetailShopResource, CustomerResource, AddressResource,\ 8 | LocalityResource, CityResource, CustomerAddressResource, CustomerTransactionResource, \ 9 | UserPermissionResource, PermissionResource, PrinterConfigResource, RegistrationDetailResource 10 | from src import api 11 | from .models import User 12 | 13 | 14 | @api.register() 15 | class UserView(BaseView): 16 | 17 | @classmethod 18 | def get_resource(cls): 19 | return UserResource 20 | 21 | 22 | @api.register() 23 | class RoleView(BaseView): 24 | 25 | api_methods = [List] 26 | 27 | @classmethod 28 | def get_resource(cls): 29 | return RoleResource 30 | 31 | 32 | @api.register() 33 | class PermissionView(BaseView): 34 | 35 | api_methods = [List] 36 | 37 | @classmethod 38 | def get_resource(cls): 39 | return PermissionResource 40 | 41 | 42 | @api.register() 43 | class UserRoleAssociationView(AssociationView): 44 | 45 | @classmethod 46 | def get_resource(cls): 47 | return UserRoleResource 48 | 49 | 50 | @api.register() 51 | class UserPermissionAssociationView(AssociationView): 52 | 53 | @classmethod 54 | def get_resource(cls): 55 | return UserPermissionResource 56 | 57 | 58 | @api.register() 59 | class RetailShopView(BaseView): 60 | 61 | @classmethod 62 | def get_resource(cls): 63 | return RetailShopResource 64 | 65 | 66 | @api.register() 67 | class RetailBrandView(BaseView): 68 | 69 | @classmethod 70 | def get_resource(cls): 71 | return RetailBrandResource 72 | 73 | 74 | @api.register() 75 | class UserRetailShopAssociationView(AssociationView): 76 | 77 | @classmethod 78 | def get_resource(cls): 79 | return UserRetailShopResource 80 | 81 | 82 | class UserLoginResource(Resource): 83 | 84 | model = User 85 | 86 | def post(self): 87 | 88 | if request.json: 89 | data = request.json 90 | 91 | user = self.model.query.filter(self.model.email == data['email']).first() 92 | if user and verify_and_update_password(data['password'], user) and login_user(user): 93 | 94 | return jsonify({'id': user.id, 'authentication_token': user.get_auth_token()}) 95 | else: 96 | return make_response(jsonify({'meta': {'code': 403}}), 403) 97 | 98 | else: 99 | data = request.form 100 | user = self.model.query.filter(self.model.email == data['email']).first() 101 | if user and verify_and_update_password(data['password'], user) and login_user(user): 102 | return make_response(redirect('/admin/', 302)) 103 | else: 104 | return make_response(redirect('/api/v1/login', 403)) 105 | 106 | api.add_resource(UserLoginResource, '/login/', endpoint='login') 107 | 108 | 109 | @api.register() 110 | class CustomerView(BaseView): 111 | 112 | @classmethod 113 | def get_resource(cls): 114 | return CustomerResource 115 | 116 | 117 | @api.register() 118 | class AddressView(BaseView): 119 | 120 | @classmethod 121 | def get_resource(cls): 122 | return AddressResource 123 | 124 | 125 | @api.register() 126 | class LocalityView(BaseView): 127 | 128 | @classmethod 129 | def get_resource(cls): 130 | return LocalityResource 131 | 132 | 133 | @api.register() 134 | class CityView(BaseView): 135 | 136 | @classmethod 137 | def get_resource(cls): 138 | return CityResource 139 | 140 | 141 | @api.register() 142 | class CustomerAddressView(AssociationView): 143 | 144 | @classmethod 145 | def get_resource(cls): 146 | return CustomerAddressResource 147 | 148 | 149 | @api.register() 150 | class CustomerTransactionView(BaseView): 151 | 152 | @classmethod 153 | def get_resource(cls): 154 | return CustomerTransactionResource 155 | 156 | 157 | @api.register() 158 | class PrinterConfigView(BaseView): 159 | 160 | @classmethod 161 | def get_resource(cls): 162 | return PrinterConfigResource 163 | 164 | 165 | @api.register() 166 | class RegistrationDetailView(BaseView): 167 | 168 | @classmethod 169 | def get_resource(cls): 170 | return RegistrationDetailResource 171 | 172 | -------------------------------------------------------------------------------- /src/orders/views.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from sqlalchemy import func, and_, cast, Date, DateTime, TIMESTAMP, text, TEXT, Text 3 | from flask import make_response, jsonify, request 4 | from flask_restful import Resource 5 | 6 | from src import BaseView, api 7 | from src.user.models import Customer 8 | from src.products.models import Product 9 | from .resources import OrderDiscountResource, ItemResource, OrderResource, ItemTaxResource, StatusResource,\ 10 | ItemAddOnResource 11 | from .models import Order, Item 12 | 13 | 14 | @api.register() 15 | class OrderView(BaseView): 16 | 17 | @classmethod 18 | def get_resource(cls): 19 | return OrderResource 20 | 21 | 22 | @api.register() 23 | class ItemView(BaseView): 24 | 25 | @classmethod 26 | def get_resource(cls): 27 | return ItemResource 28 | 29 | 30 | @api.register() 31 | class OrderDiscountView(BaseView): 32 | 33 | @classmethod 34 | def get_resource(cls): 35 | return OrderDiscountResource 36 | 37 | 38 | @api.register() 39 | class ItemTaxView(BaseView): 40 | 41 | @classmethod 42 | def get_resource(cls): 43 | return ItemTaxResource 44 | 45 | 46 | @api.register() 47 | class ItemAddOnView(BaseView): 48 | 49 | @classmethod 50 | def get_resource(cls): 51 | return ItemAddOnResource 52 | 53 | 54 | @api.register() 55 | class StatusView(BaseView): 56 | 57 | @classmethod 58 | def get_resource(cls): 59 | return StatusResource 60 | 61 | 62 | class OrderStatResource(Resource): 63 | 64 | def get(self): 65 | 66 | shops = request.args.getlist('__retail_shop_id__in') 67 | if len(shops) == 1: 68 | shops = shops[0].split(',') 69 | from_date = datetime.strptime(request.args['__from'], '%Y-%m-%dT%H:%M:%S.%fZ').date() 70 | to_date = datetime.strptime(request.args['__to'], '%Y-%m-%dT%H:%M:%S.%fZ').date() 71 | 72 | days = (to_date - from_date).days 73 | 74 | collection_type = 'day' 75 | if days > 28: 76 | collection_type = 'week' 77 | if days > 140: 78 | collection_type = 'month' 79 | 80 | orders = Order.query.join(Item, and_(Item.order_id == Order.id)).filter(Order.retail_shop_id.in_(shops)) 81 | 82 | total_orders, total_sales, total_items, total_quantity, total_due = \ 83 | orders.with_entities(func.Count(Order.id), func.sum(Order.total), 84 | func.Count(func.Distinct(Item.product_id)), func.sum(Item.quantity), 85 | func.Sum(Order.amount_due)).all()[0] 86 | 87 | orders = Order.query.with_entities(func.count(Order.id), 88 | func.cast(func.date_trunc(collection_type, 89 | func.cast(Order.created_on, Date)), Text) 90 | .label('dateWeek'))\ 91 | .filter(Order.created_on.between(from_date, to_date))\ 92 | .group_by('dateWeek').all() 93 | 94 | # new_customers = orders.join(Customer, and_(Customer.id == Order.customer_id))\ 95 | # .with_entities(func.Count(Order.customer_id)).scalar() 96 | # 97 | # return_customers = orders.join(Customer, and_(Customer.id == Order.customer_id))\ 98 | # .with_entities(func.Count(Order.customer_id)).scalar() 99 | # 100 | items = Item.query.join(Order, and_(Order.id == Item.order_id))\ 101 | .filter(Order.retail_shop_id.in_(shops)) 102 | 103 | max_sold_items = items.join(Product, and_(Product.id == Item.product_id))\ 104 | .with_entities(func.Count(Item.id), Product.name, 105 | func.cast(func.date_trunc(collection_type, func.cast(Order.created_on, Date)), Text) 106 | .label('dateWeek'))\ 107 | .filter(Order.created_on.between(from_date, to_date))\ 108 | .group_by(Item.product_id, Product.name, 'dateWeek').order_by(-func.Count(Item.id)).limit(10).all() 109 | 110 | # min_sold_items = items.join(Product, and_(Product.id == Item.product_id))\ 111 | # .with_entities(func.Count(Item.id), Item.product_id, Product.name)\ 112 | # .group_by(Item.product_id, Product.name).order_by(func.Count(Item.id)).limit(10).all() 113 | 114 | return make_response(jsonify(dict(total_orders=total_orders, total_sales=total_sales, 115 | total_quantity=total_quantity, max_sold_items=max_sold_items, 116 | total_items=str(total_items), orders=orders)), 200) 117 | 118 | 119 | api.add_resource(OrderStatResource, '/order_stats/', endpoint='order_stats') 120 | -------------------------------------------------------------------------------- /src/orders/resources.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.sql import false 2 | from flask_security import current_user 3 | from src.utils import ModelResource, operators as ops 4 | from .models import Item, ItemAddOn, Order, OrderDiscount, ItemTax, Status 5 | from .schemas import ItemSchema, ItemTaxSchema, OrderSchema, OrderDiscountSchema, ItemAddOnSchema, StatusSchema 6 | 7 | 8 | class OrderResource(ModelResource): 9 | 10 | model = Order 11 | schema = OrderSchema 12 | 13 | optional = ('items', 'time_line') 14 | 15 | order_by = ('id', 'invoice_number') 16 | 17 | filters = { 18 | 'id': [ops.Equal], 19 | 'customer_id': [ops.Equal], 20 | 'retail_shop_id': [ops.Equal, ops.In], 21 | 'current_status_id': [ops.Equal], 22 | } 23 | 24 | auth_required = True 25 | 26 | def has_read_permission(self, qs): 27 | if current_user.has_permission('view_order'): 28 | return qs.filter(self.model.retail_shop_id.in_(current_user.retail_shop_ids)) 29 | else: 30 | return qs.filter(false()) 31 | 32 | def has_change_permission(self, obj): 33 | return current_user.has_shop_access(obj.retail_shop_id) and current_user.has_permission('change_order') 34 | 35 | def has_delete_permission(self, obj): 36 | return current_user.has_shop_access(obj.retail_shop_id) and current_user.has_permission('remove_order') 37 | 38 | def has_add_permission(self, objects): 39 | if not current_user.has_permission('create_order'): 40 | return False 41 | 42 | for obj in objects: 43 | if not current_user.has_shop_access(obj.retail_shop_id): 44 | return false 45 | return True 46 | 47 | 48 | class ItemTaxResource(ModelResource): 49 | model = ItemTax 50 | schema = ItemTaxSchema 51 | 52 | auth_required = True 53 | roles_required = ('admin',) 54 | 55 | def has_read_permission(self, qs): 56 | if current_user.has_permission('view_order_item'): 57 | return qs.filter(self.model.retail_shop_id.in_(current_user.retail_shop_ids)) 58 | else: 59 | return qs.filter(false()) 60 | 61 | def has_change_permission(self, obj): 62 | return current_user.has_shop_access(obj.retail_shop_id) and current_user.has_permission('change_order_item') 63 | 64 | def has_delete_permission(self, obj): 65 | return current_user.has_shop_access(obj.retail_shop_id) and current_user.has_permission('remove_order_item') 66 | 67 | def has_add_permission(self, obj): 68 | return current_user.has_shop_access(obj.retail_shop_id) and current_user.has_permission('create_order_item') 69 | 70 | 71 | class OrderDiscountResource(ModelResource): 72 | model = OrderDiscount 73 | schema = OrderDiscountSchema 74 | 75 | auth_required = True 76 | roles_required = ('admin',) 77 | 78 | def has_read_permission(self, qs): 79 | qs = qs.filter(self.model.retail_shop_id.in_(current_user.retail_shop_ids)) 80 | return qs 81 | 82 | def has_change_permission(self, obj): 83 | return current_user.has_shop_access(obj.retail_shop_id) 84 | 85 | def has_delete_permission(self, obj): 86 | return current_user.has_shop_access(obj.retail_shop_id) 87 | 88 | def has_add_permission(self, obj): 89 | return current_user.has_shop_access(obj.retail_shop_id) 90 | 91 | 92 | class ItemResource(ModelResource): 93 | 94 | model = Item 95 | schema = ItemSchema 96 | 97 | optional = ('add_ons', 'taxes') 98 | 99 | filters = { 100 | 'id': [ops.Equal, ops.In], 101 | 'order_id': [ops.Equal, ops.In], 102 | 'product_id': [ops.Equal, ops.In], 103 | 'retail_shop_id': [ops.Equal, ops.In], 104 | 'stock_id': [ops.Equal, ops.In], 105 | 'update_on': [ops.DateLesserEqual, ops.DateEqual, ops.DateGreaterEqual], 106 | 'created_on': [ops.DateLesserEqual, ops.DateEqual, ops.DateGreaterEqual] 107 | } 108 | 109 | order_by = ['id'] 110 | 111 | only = () 112 | 113 | exclude = () 114 | 115 | auth_required = True 116 | 117 | def has_read_permission(self, qs): 118 | qs = qs.filter(self.model.retail_shop_id.in_(current_user.retail_shop_ids)) 119 | return qs 120 | 121 | def has_change_permission(self, obj): 122 | return current_user.has_shop_access(obj.retail_shop_id) 123 | 124 | def has_delete_permission(self, obj): 125 | return current_user.has_shop_access(obj.retail_shop_id) 126 | 127 | def has_add_permission(self, obj): 128 | return current_user.has_shop_access(obj.retail_shop_id) 129 | 130 | 131 | class StatusResource(ModelResource): 132 | model = Status 133 | schema = StatusSchema 134 | 135 | auth_required = True 136 | 137 | roles_required = ('admin',) 138 | 139 | def has_read_permission(self, qs): 140 | qs = qs.filter(self.model.retail_shop_id.in_(current_user.retail_shop_ids)) 141 | return qs 142 | 143 | def has_change_permission(self, obj): 144 | return current_user.has_shop_access(obj.retail_shop_id) 145 | 146 | def has_delete_permission(self, obj): 147 | return current_user.has_shop_access(obj.retail_shop_id) 148 | 149 | def has_add_permission(self, obj): 150 | return current_user.has_shop_access(obj.retail_shop_id) 151 | 152 | 153 | class ItemAddOnResource(ModelResource): 154 | model = ItemAddOn 155 | schema = ItemAddOnSchema 156 | 157 | roles_required = ('admin',) 158 | 159 | auth_required = True 160 | 161 | def has_read_permission(self, qs): 162 | qs = qs.filter(self.model.retail_shop_id.in_(current_user.retail_shop_ids)) 163 | return qs 164 | 165 | def has_change_permission(self, obj): 166 | return current_user.has_shop_access(obj.retail_shop_id) 167 | 168 | def has_delete_permission(self, obj): 169 | return current_user.has_shop_access(obj.retail_shop_id) 170 | 171 | def has_add_permission(self, obj): 172 | return current_user.has_shop_access(obj.retail_shop_id) 173 | 174 | -------------------------------------------------------------------------------- /src/user/schemas.py: -------------------------------------------------------------------------------- 1 | from src import ma, BaseSchema 2 | from .models import User, Role, Permission, UserRole, RetailShop, RetailBrand, UserRetailShop, \ 3 | Customer, Address, Locality, City, RegistrationDetail, CustomerAddress, CustomerTransaction, \ 4 | UserPermission, PrinterConfig 5 | 6 | 7 | class UserSchema(BaseSchema): 8 | 9 | class Meta: 10 | model = User 11 | exclude = ('updated_on', 'password') 12 | 13 | id = ma.UUID(dump_only=True) 14 | email = ma.Email(unique=True, primary_key=True, required=True) 15 | username = ma.String(required=True) 16 | name = ma.String(load=True) 17 | brand_ids = ma.List(ma.UUID, dump_only=True) 18 | retail_shop_ids = ma.List(ma.UUID, dump_only=True) 19 | retail_shops = ma.Nested('RetailShopSchema', many=True, dump_only=True) 20 | 21 | _links = ma.Hyperlinks({'shops': ma.URLFor('pos.retail_shop_view', __id__in='')}) 22 | roles = ma.Nested('RoleSchema', many=True, dump_only=True, only=('id', 'name')) 23 | permissions = ma.Nested('PermissionSchema', many=True, dump_only=True, only=('id', 'name')) 24 | 25 | 26 | class RoleSchema(BaseSchema): 27 | 28 | class Meta: 29 | model = Role 30 | exclude = ('updated_on', 'created_on', 'users') 31 | 32 | id = ma.UUID() 33 | name = ma.String() 34 | permissions = ma.Nested('PermissionSchema', many=True, dump_only=True, only=('id', 'name')) 35 | 36 | 37 | class UserRoleSchema(BaseSchema): 38 | 39 | class Meta: 40 | model = UserRole 41 | exclude = ('created_on', 'updated_on') 42 | 43 | id = ma.UUID(load=True) 44 | user_id = ma.UUID(load=True) 45 | role_id = ma.UUID(load=True) 46 | user = ma.Nested('UserSchema', many=False) 47 | role = ma.Nested('RoleSchema', many=False) 48 | 49 | 50 | class PermissionSchema(BaseSchema): 51 | 52 | class Meta: 53 | model = Permission 54 | exclude = ('users', 'created_on', 'updated_on') 55 | 56 | role = ma.Nested('RoleSchema', dump_only=True, many=False) 57 | 58 | 59 | class RetailShopSchema(BaseSchema): 60 | 61 | class Meta: 62 | model = RetailShop 63 | exclude = ('created_on', 'updated_on', 'products', 'orders', 'users', 'brands', 'distributors', 'tags', 'taxes') 64 | 65 | _links = ma.Hyperlinks( 66 | { 67 | 'products': ma.URLFor('pos.product_view', __retail_shop_id__exact=''), 68 | 'brands': ma.URLFor('pos.brand_view', __retail_shop_id__exact=''), 69 | 'distributors': ma.URLFor('pos.distributor_view', __retail_shop_id__exact=''), 70 | 'tags': ma.URLFor('pos.tag_view', __retail_shop_id__exact=''), 71 | 'taxes': ma.URLFor('pos.tax_view', __retail_shop_id__exact='') 72 | } 73 | ) 74 | retail_brand_id = ma.UUID() 75 | retail_brand = ma.Nested('RetailBrandSchema', many=False) 76 | total_sales = ma.Dict() 77 | address = ma.Nested('AddressSchema', many=False) 78 | localities = ma.Nested('LocalitySchema', many=True) 79 | registration_details = ma.Nested('RegistrationDetailSchema', many=True) 80 | printer_config = ma.Nested('PrinterConfigSchema', load=True, many=False) 81 | 82 | 83 | class RetailBrandSchema(BaseSchema): 84 | class Meta: 85 | model = RetailBrand 86 | exclude = ('created_on', 'updated_on') 87 | 88 | 89 | class UserRetailShopSchema(BaseSchema): 90 | class Meta: 91 | model = UserRetailShop 92 | exclude = ('created_on', 'updated_on') 93 | 94 | user_id = ma.UUID(load=True, allow_none=False) 95 | retail_shop_id = ma.UUID(load=True, allow_none=False) 96 | 97 | 98 | class CustomerSchema(BaseSchema): 99 | class Meta: 100 | model = Customer 101 | exclude = ('updated_on',) 102 | 103 | mobile_number = ma.Integer() 104 | total_orders = ma.Integer(dump_only=True) 105 | total_billing = ma.Float(precison=2, dump_only=True) 106 | amount_due = ma.Float(precison=2, dump_only=True) 107 | addresses = ma.Nested('AddressSchema', many=True, load=False, partial=True) 108 | retail_brand_id = ma.UUID(load=True) 109 | retail_shop_id = ma.List(ma.UUID(), load=True) 110 | retail_brand = ma.Nested('RetailBrandSchema', many=False, only=('id', 'name')) 111 | transactions = ma.Nested('CustomerTransactionSchema', many=True, only=('id', 'amount', 'created_on')) 112 | 113 | 114 | class AddressSchema(BaseSchema): 115 | class Meta: 116 | model = Address 117 | exclude = ('created_on', 'updated_on', 'locality') 118 | 119 | name = ma.String(load=True, required=True) 120 | locality_id = ma.UUID(load_only=True) 121 | locality = ma.Nested('LocalitySchema', many=False, load=False, exclude=('city_id',)) 122 | 123 | 124 | class LocalitySchema(BaseSchema): 125 | class Meta: 126 | model = Locality 127 | exclude = ('created_on', 'updated_on') 128 | 129 | city_id = ma.UUID(load=True) 130 | city = ma.Nested('CitySchema', many=False, load=True) 131 | 132 | 133 | class CitySchema(BaseSchema): 134 | class Meta: 135 | model = City 136 | exclude = ('created_on', 'updated_on') 137 | 138 | 139 | class RegistrationDetailSchema(BaseSchema): 140 | class Meta: 141 | model = RegistrationDetail 142 | exclude = ('created_on', 'updated_on', 'retail_shop') 143 | 144 | retail_shop_id = ma.UUID(load=True, allow_none=False) 145 | 146 | 147 | class CustomerAddressSchema(BaseSchema): 148 | class Meta: 149 | model = CustomerAddress 150 | exclude = ('created_on', 'updated_on') 151 | 152 | address_id = ma.UUID(load=True, partial=False) 153 | customer_id = ma.UUID(load=True, partial=False) 154 | 155 | 156 | class CustomerTransactionSchema(BaseSchema): 157 | class Meta: 158 | model = CustomerTransaction 159 | exclude = ('updated_on',) 160 | 161 | amount = ma.Float(precision=2, load=True) 162 | customer_id = ma.UUID(load=True, partial=False, allow_none=False) 163 | 164 | 165 | class UserPermissionSchema(BaseSchema): 166 | class Meta: 167 | model = UserPermission 168 | exclude = ('created_on', 'updated_on') 169 | 170 | id = ma.UUID(load=True) 171 | user_id = ma.UUID(load=True) 172 | permission_id = ma.UUID(load=True) 173 | user = ma.Nested('UserSchema', many=False) 174 | permission = ma.Nested('PermissionSchema', many=False) 175 | 176 | 177 | class PrinterConfigSchema(BaseSchema): 178 | 179 | class Meta: 180 | model = PrinterConfig 181 | exclude = ('created_on', 'updated_on') 182 | 183 | retail_shop_id = ma.UUID(load=True, allow_none=False) 184 | have_bill_printer = ma.Boolean(load=True) 185 | -------------------------------------------------------------------------------- /tests/test_users.py: -------------------------------------------------------------------------------- 1 | try: 2 | from urllib2 import urlopen 3 | except ImportError: 4 | from urllib.request import urlopen 5 | from flask_testing import TestCase 6 | import json 7 | from manager import app, db 8 | from src import configs 9 | from src.user.models import User, Role, UserRole 10 | from src.user.schemas import UserSchema, RoleSchema 11 | 12 | 13 | class TestSetup(TestCase): 14 | 15 | def create_app(self): 16 | # pass in test configuration 17 | return app 18 | 19 | def setUp(self): 20 | config = configs.get('testing') 21 | app.config.from_object(config) 22 | db.create_all() 23 | 24 | def tearDown(self): 25 | 26 | db.session.remove() 27 | db.drop_all() 28 | 29 | def test_setup(self): 30 | self.assertTrue(self.app is not None) 31 | self.assertTrue(self.client is not None) 32 | self.assertTrue(self._ctx is not None) 33 | 34 | 35 | class TestUser(TestSetup): 36 | 37 | def test_delete_users(self): 38 | users = User.query.all() 39 | for user in users: 40 | db.session.delete(user) 41 | db.session.commit() 42 | self.assert404(self.client.get("/test/v1/user/")) 43 | 44 | def test_add_users(self): 45 | 46 | for i in range(0, 10): 47 | data = {'email': str(i)+'_saurabh@gmail.com', 'password': '123', 'username': 'saurabh_'+str(i)} 48 | user, error = UserSchema().load(data, session=db.session) 49 | self.assertEqual(error, {}, 'no errors') 50 | db.session.add(user) 51 | db.session.commit() 52 | 53 | self.assert200(self.client.get("/test/v1/user/"), 200) 54 | 55 | def test_user_api(self): 56 | 57 | response = self.client.post("/test/v1/user/", data=json.dumps({'username': 'sa1', 'email': 'sa@g.com', 58 | 'password': '12121212'}), 59 | headers={'Content-Type': 'application/json'}) 60 | 61 | self.assertEqual(response.status, '201 CREATED') 62 | user_id = response.json['data'][0]['id'] 63 | self.assert200(self.client.get('/test/v1/user/'+str(user_id)+'/'), 'user found') 64 | 65 | self.assertEqual(self.client.delete('/test/v1/user/'+str(user_id)+'/').status, '204 NO CONTENT') 66 | 67 | 68 | class TestRole(TestSetup): 69 | 70 | def test_delete_users(self): 71 | roles = Role.query.all() 72 | for role in roles: 73 | db.session.delete(role) 74 | db.session.commit() 75 | self.assert404(self.client.get("/test/v1/role/")) 76 | 77 | def test_add_roles(self): 78 | 79 | for i in range(0, 10): 80 | data = {'name': 'admin_'+str(i)} 81 | role, error = RoleSchema().load(data, session=db.session) 82 | self.assertEqual(error, {}, 'no errors') 83 | db.session.add(role) 84 | db.session.commit() 85 | 86 | self.assert200(self.client.get("/test/v1/role/"), 200) 87 | 88 | def test_role_api(self): 89 | 90 | response = self.client.post("/test/v1/role/", data=json.dumps({'name': 'admin_a'}), 91 | headers={'Content-Type': 'application/json'}) 92 | 93 | self.assertEqual(response.status, '201 CREATED') 94 | role_id = response.json['data'][0]['id'] 95 | self.assert200(self.client.get('/test/v1/role/'+str(role_id)+'/'), 'role found') 96 | 97 | self.assertEqual(self.client.delete('/test/v1/role/'+str(role_id)+'/').status, '204 NO CONTENT') 98 | 99 | 100 | class TestUserRole(TestSetup): 101 | 102 | def test_delete_users(self): 103 | roles = UserRole.query.all() 104 | for role in roles: 105 | db.session.delete(role) 106 | db.session.commit() 107 | self.assertEqual(UserRole.query.all(), []) 108 | 109 | def test_role_api(self): 110 | user = User() 111 | user.username = '1' 112 | user.email = '1@1.com' 113 | user.password = '123' 114 | db.session.add(user) 115 | role = Role() 116 | role.name = '1' 117 | db.session.add(role) 118 | db.session.commit() 119 | response = self.client.patch("/test/v1/user_role/", 120 | data=json.dumps([ 121 | { 122 | '__action': 'add', 123 | 'user_id': user.id, 124 | 'role_id': role.id 125 | }, 126 | { 127 | '__action': 'remove', 128 | 'user_id': user.id, 129 | 'role_id': role.id 130 | }, 131 | { 132 | '__action': 'add', 133 | 'user_id': user.id, 134 | 'role_id': role.id 135 | }, 136 | { 137 | '__action': 'remove', 138 | 'user_id': user.id, 139 | 'role_id': role.id 140 | } 141 | ]), 142 | headers={'Content-Type': 'application/json'}) 143 | 144 | self.assertEqual(response.status, '200 OK') 145 | 146 | response = self.client.patch("/test/v1/user_role/", 147 | data=json.dumps([ 148 | { 149 | '__action': 'add', 150 | 'user_id': user.id, 151 | 'role_id': role.id 152 | }, 153 | { 154 | '__action': 'add', 155 | 'user_id': user.id, 156 | 'role_id': role.id 157 | } 158 | ]), 159 | headers={'Content-Type': 'application/json'}) 160 | 161 | self.assertEqual(response.status, '400 BAD REQUEST') 162 | 163 | 164 | class TestSetupFailure(TestCase): 165 | 166 | def _pre_setup(self): 167 | pass 168 | 169 | def test_setup_failure(self): 170 | """Should not fail in _post_teardown if _pre_setup fails""" 171 | 172 | assert True 173 | -------------------------------------------------------------------------------- /src/orders/models.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.dialects.postgresql import UUID 2 | from sqlalchemy.ext.hybrid import hybrid_property 3 | from sqlalchemy import func, select 4 | from src import db, BaseMixin, ReprMixin 5 | 6 | 7 | class OrderStatus(BaseMixin, db.Model, ReprMixin): 8 | __repr_fields__ = ['order_id', 'status_id'] 9 | 10 | order_id = db.Column(UUID, db.ForeignKey('order.id'), nullable=False, index=True) 11 | status_id = db.Column(UUID, db.ForeignKey('status.id'), nullable=False, index=True) 12 | 13 | order = db.relationship('Order', foreign_keys=[order_id]) 14 | status = db.relationship('Status', foreign_keys=[status_id]) 15 | 16 | 17 | class Status(BaseMixin, db.Model, ReprMixin): 18 | 19 | name = db.Column(db.String(20), unique=True, nullable=False) 20 | code = db.Column(db.SmallInteger, unique=True, nullable=False) 21 | 22 | 23 | class Order(BaseMixin, db.Model, ReprMixin): 24 | 25 | __repr_fields__ = ['id', 'customer_id'] 26 | 27 | edit_stock = db.Column(db.Boolean(), default=True) 28 | sub_total = db.Column(db.Float(precision=2), default=0, nullable=True) 29 | total = db.Column(db.Float(precision=2), default=0, nullable=True) 30 | amount_paid = db.Column(db.Float(precision=2), default=0, nullable=True) 31 | auto_discount = db.Column(db.Float(precision=2), default=0, nullable=True) 32 | is_void = db.Column(db.Boolean(), default=False) 33 | invoice_number = db.Column(db.Integer) 34 | reference_number = db.Column(db.String(12), nullable=True) 35 | 36 | customer_id = db.Column(UUID, db.ForeignKey('customer.id'), nullable=True, index=True) 37 | user_id = db.Column(UUID, db.ForeignKey('user.id'), nullable=False, index=True) 38 | address_id = db.Column(UUID, db.ForeignKey('address.id'), nullable=True, index=True) 39 | retail_shop_id = db.Column(UUID, db.ForeignKey('retail_shop.id'), nullable=False, index=True) 40 | current_status_id = db.Column(UUID, db.ForeignKey('status.id'), nullable=True, index=True) 41 | 42 | items = db.relationship('Item', uselist=True, back_populates='order', lazy='dynamic', cascade="all, delete-orphan") 43 | customer = db.relationship('Customer', foreign_keys=[customer_id]) 44 | created_by = db.relationship('User', foreign_keys=[user_id]) 45 | address = db.relationship('Address', foreign_keys=[address_id]) 46 | retail_shop = db.relationship('RetailShop', foreign_keys=[retail_shop_id]) 47 | discounts = db.relationship('Discount', secondary='order_discount', uselist=True) 48 | denominations = db.relationship('Denomination', secondary='order_denomination', uselist=False) 49 | current_status = db.relationship('Status', uselist=False, foreign_keys=[current_status_id]) 50 | time_line = db.relationship('Status', secondary='order_status') 51 | 52 | @hybrid_property 53 | def total_discount(self): 54 | return sum([discount.value if discount.type == 'VALUE' else float(self.total*discount/100) 55 | for discount in self.discounts]) 56 | 57 | @hybrid_property 58 | def items_count(self): 59 | return self.items.with_entities(func.Count(Item.id)).scalar() 60 | 61 | @items_count.expression 62 | def items_count(cls): 63 | return select([func.Count(Item.id)]).where(Item.order_id == cls.id).as_scalar() 64 | 65 | @hybrid_property 66 | def amount_due(self): 67 | if self.total and self.amount_paid: 68 | return self.total - self.amount_paid 69 | return self.total 70 | 71 | 72 | class Item(BaseMixin, db.Model, ReprMixin): 73 | 74 | __repr_fields__ = ['id', 'order_id', 'product_id'] 75 | 76 | unit_price = db.Column(db.Float(precision=2)) 77 | quantity = db.Column(db.Float(precision=2)) 78 | discount = db.Column(db.FLOAT(precision=2), default=0, nullable=False) 79 | 80 | parent_id = db.Column(UUID, db.ForeignKey('item.id'), nullable=True, index=True) 81 | product_id = db.Column(UUID, db.ForeignKey('product.id'), nullable=True, index=True) 82 | order_id = db.Column(UUID, db.ForeignKey('order.id'), nullable=True, index=True) 83 | stock_id = db.Column(UUID, db.ForeignKey('stock.id'), nullable=True, index=True) 84 | combo_id = db.Column(UUID, db.ForeignKey('combo.id'), nullable=True, index=True) 85 | 86 | parent = db.relationship('Item', uselist=False, remote_side='Item.id') 87 | children = db.relationship('Item', remote_side='Item.parent_id') 88 | 89 | product = db.relationship('Product', foreign_keys=[product_id]) 90 | order = db.relationship('Order', foreign_keys=[order_id], single_parent=True, back_populates='items', 91 | cascade="all, delete-orphan") 92 | taxes = db.relationship('ItemTax', uselist=True, cascade='all, delete-orphan', 93 | back_populates='item') 94 | add_ons = db.relationship('ItemAddOn', uselist=True, cascade='all, delete-orphan', 95 | back_populates='item') 96 | stock = db.relationship('Stock', foreign_keys=[stock_id], single_parent=True, back_populates='order_items') 97 | 98 | @hybrid_property 99 | def total_price(self): 100 | return float(self.unit_price * self.quantity) 101 | 102 | @hybrid_property 103 | def discounted_total_price(self): 104 | return float(self.discounted_unit_price * self.quantity) 105 | 106 | @hybrid_property 107 | def discounted_unit_price(self): 108 | return float(self.unit_price-(self.unit_price * self.discount)/100) 109 | 110 | @hybrid_property 111 | def discount_amount(self): 112 | return float((self.total_price*self.discount)/100) 113 | 114 | @hybrid_property 115 | def is_combo(self): 116 | return self.combo_id is not None 117 | 118 | @hybrid_property 119 | def retail_shop_id(self): 120 | return self.order.retail_shop_id 121 | 122 | @retail_shop_id.expression 123 | def retail_shop_id(self): 124 | return select([Order.retail_shop_id]).where(Order.id == self.order_id).as_scalar() 125 | 126 | 127 | class ItemAddOn(BaseMixin, db.Model, ReprMixin): 128 | 129 | item_id = db.Column(UUID, db.ForeignKey('item.id'), index=True) 130 | add_on_id = db.Column(UUID, db.ForeignKey('add_on.id'), index=True) 131 | 132 | add_on = db.relationship('AddOn', foreign_keys=[add_on_id]) 133 | item = db.relationship('Item', back_populates='add_ons', foreign_keys=[item_id]) 134 | 135 | 136 | class ItemTax(BaseMixin, db.Model): 137 | 138 | tax_value = db.Column(db.Float(precision=2)) 139 | tax_amount = db.Column(db.Float(precision=2)) 140 | item_id = db.Column(UUID, db.ForeignKey('item.id'), index=True) 141 | tax_id = db.Column(UUID, db.ForeignKey('tax.id'), index=True) 142 | 143 | tax = db.relationship('Tax', foreign_keys=[tax_id]) 144 | item = db.relationship('Item', back_populates='taxes', foreign_keys=[item_id]) 145 | 146 | 147 | class OrderDiscount(BaseMixin, db.Model, ReprMixin): 148 | __repr_fields__ = ['order_id', 'discount_id'] 149 | 150 | order_id = db.Column(UUID, db.ForeignKey('order.id'), nullable=False, index=True) 151 | discount_id = db.Column(UUID, db.ForeignKey('discount.id'), nullable=False, index=True) 152 | 153 | order = db.relationship('Order', foreign_keys=[order_id]) 154 | discount = db.relationship('Discount', foreign_keys=[discount_id]) 155 | 156 | 157 | class Discount(BaseMixin, db.Model, ReprMixin): 158 | 159 | name = db.Column(db.String(55), nullable=True) 160 | value = db.Column(db.Float(precision=2), nullable=False) 161 | type = db.Column(db.Enum('PERCENTAGE', 'FIXED', name='varchar'), nullable=False, default='PERCENTAGE') 162 | 163 | orders = db.relationship('Order', secondary='order_discount') 164 | 165 | 166 | class Denomination(BaseMixin, db.Model, ReprMixin): 167 | 168 | value = db.Column(db.SmallInteger, default=0) 169 | name = db.Column(db.String, nullable=False, default='zero') 170 | 171 | 172 | class OrderDenomination(BaseMixin, db.Model, ReprMixin): 173 | 174 | __repr_fields__ = ['order_id', 'denomination_id'] 175 | 176 | order_id = db.Column(UUID, db.ForeignKey('order.id'), nullable=False, index=True) 177 | denomination_id = db.Column(UUID, db.ForeignKey('denomination.id'), nullable=False, index=True) 178 | 179 | order = db.relationship('Order', foreign_keys=[order_id]) 180 | denomination = db.relationship('Denomination', foreign_keys=[denomination_id]) 181 | 182 | -------------------------------------------------------------------------------- /src/products/schemas.py: -------------------------------------------------------------------------------- 1 | from src import ma, BaseSchema 2 | from .models import Brand, Distributor, DistributorBill, Product, Tag, Stock, ProductTax, Tax, \ 3 | Combo, AddOn, Salt, ProductSalt, ProductDistributor, ProductTag, BrandDistributor 4 | 5 | 6 | class BrandSchema(BaseSchema): 7 | class Meta: 8 | model = Brand 9 | exclude = ('created_on', 'updated_on') 10 | 11 | name = ma.String() 12 | retail_shop_id = ma.UUID() 13 | retail_shop = ma.Nested('RetailShopSchema', many=False, dump_only=True, only=('id', 'name')) 14 | distributors = ma.Nested('DistributorSchema', many=True, dump_only=True, only=('id', 'name')) 15 | products = ma.Nested('ProductSchema', many=True, dump_only=True, only=('id', 'name')) 16 | 17 | 18 | class TagSchema(BaseSchema): 19 | class Meta: 20 | model = Tag 21 | exclude = ('created_on', 'updated_on') 22 | 23 | id = ma.UUID() 24 | name = ma.String() 25 | retail_shop_id = ma.UUID() 26 | retail_shop = ma.Nested('RetailShopSchema', many=False, dump_only=True, only=('id', 'name')) 27 | 28 | 29 | class ProductTaxSchema(BaseSchema): 30 | class Meta: 31 | model = ProductTax 32 | exclude = ('created_on', 'updated_on') 33 | fields = ('tax_id', 'product_id') 34 | 35 | tax_id = ma.UUID(load=True) 36 | product_id = ma.UUID(load=True) 37 | 38 | 39 | class TaxSchema(BaseSchema): 40 | class Meta: 41 | model = Tax 42 | exclude = ('created_on', 'updated_on') 43 | 44 | name = ma.String(load=True) 45 | value = ma.Float(precision=2, load=True) 46 | retail_shop_id = ma.UUID(load=True) 47 | retail_shop = ma.Nested('RetailShopSchema', many=False, dump_only=True, only=('id', 'name')) 48 | 49 | 50 | class DistributorSchema(BaseSchema): 51 | class Meta: 52 | model = Distributor 53 | exclude = ('created_on', 'updated_on') 54 | 55 | id = ma.UUID() 56 | name = ma.String() 57 | phone_numbers = ma.List(ma.Integer()) 58 | emails = ma.List(ma.Email()) 59 | retail_shop_id = ma.UUID() 60 | products = ma.Nested('ProductSchema', many=True, dump_only=True, only=('id', 'name', 'last_selling_amount', 61 | 'barcode', 'last_purchase_amount', 62 | 'stock_required', 'quantity_label')) 63 | 64 | retail_shop = ma.Nested('RetailShopSchema', many=False, dump_only=True, only=('id', 'name')) 65 | bills = ma.Nested('DistributorBillSchema', many=True, exclude=('distributor', 'distributor_id')) 66 | 67 | 68 | class DistributorBillSchema(BaseSchema): 69 | class Meta: 70 | model = DistributorBill 71 | exclude = ('created_on', 'updated_on') 72 | 73 | purchase_date = ma.Date(load=True) 74 | distributor_id = ma.UUID(load=True) 75 | total_items = ma.Integer(dump_only=True) 76 | bill_amount = ma.Integer(dump_only=True) 77 | 78 | distributor = ma.Nested('DistributorSchema', many=False, only=('id', 'name', 'retail_shop')) 79 | purchased_items = ma.Nested('StockSchema', many=True, exclude=('distributor_bill', 'order_items', 'product'), load=True) 80 | 81 | 82 | class ProductSchema(BaseSchema): 83 | class Meta: 84 | model = Product 85 | exclude = ('created_on', 'updated_on') 86 | 87 | name = ma.String() 88 | description = ma.List(ma.Dict(), allow_none=True) 89 | sub_description = ma.String(allow_none=True) 90 | brand_id = ma.UUID() 91 | retail_shop_id = ma.UUID() 92 | default_quantity = ma.Float(precision=2, partila=True) 93 | quantity_label = ma.String(load=True, allow_none=True) 94 | is_loose = ma.Boolean(load=True, allow_none=True) 95 | mrp = ma.Integer(dump_only=True) 96 | available_stock = ma.Integer(dump_only=True) 97 | barcode = ma.String(max_length=13, min_length=8, load=True, allow_none=False) 98 | similar_products = ma.List(ma.Integer, dump_only=True) 99 | 100 | last_selling_amount = ma.Float(precision=2, dump_only=True) 101 | last_purchase_amount = ma.Float(precision=2, dump_only=True) 102 | stock_required = ma.Integer(dump_only=True) 103 | is_short = ma.Boolean(dump_only=True) 104 | distributors = ma.Nested('DistributorSchema', many=True, dump_only=True, only=('id', 'name')) 105 | brand = ma.Nested('BrandSchema', many=False, dump_only=True, only=('id', 'name')) 106 | retail_shop = ma.Nested('RetailShopSchema', many=False, dump_only=True, only=('id', 'name')) 107 | tags = ma.Nested('TagSchema', many=True, only=('id', 'name'), dump_only=True) 108 | salts = ma.Nested('SaltSchema', many=True, only=('id', 'name'), dump_only=True) 109 | 110 | _links = ma.Hyperlinks( 111 | { 112 | 'distributor': ma.URLFor('pos.distributor_view', __product_id__exact=''), 113 | 'retail_shop': ma.URLFor('pos.retail_shop_view', slug=''), 114 | 'brand': ma.URLFor('pos.brand_view', slug=''), 115 | 'stocks': ma.URLFor('pos.stock_view', __product_id__exact='') 116 | } 117 | ) 118 | 119 | stocks = ma.Nested('StockSchema', many=True, only=('purchase_amount', 'selling_amount', 'units_purchased', 120 | 'units_sold', 'expiry_date', 'purchase_date', 'id')) 121 | taxes = ma.Nested('TaxSchema', many=True, dump_only=True, only=('id', 'name', 'value')) 122 | available_stocks = ma.Nested('StockSchema', many=True, dump_only=True, 123 | only=('purchase_amount', 'selling_amount', 'units_purchased', 124 | 'units_sold', 'expiry_date', 'purchase_date', 'id', 'default_stock')) 125 | 126 | 127 | class StockSchema(BaseSchema): 128 | class Meta: 129 | model = Stock 130 | exclude = ('order_items', 'created_on', 'updated_on') 131 | 132 | purchase_amount = ma.Float(precision=2) 133 | selling_amount = ma.Float(precision=2) 134 | units_purchased = ma.Integer() 135 | batch_number = ma.String(load=True) 136 | expiry_date = ma.Date() 137 | product_name = ma.String() 138 | product_id = ma.UUID(load=True) 139 | distributor_bill_id = ma.UUID(allow_none=True) 140 | units_sold = ma.Integer(dump_only=True, load=False) 141 | expired = ma.Boolean(dump_only=True) 142 | brand_name = ma.String(dump_only=True) 143 | quantity_label = ma.String(dump_only=True) 144 | default_stock = ma.Boolean(load=True, allow_none=True) 145 | 146 | distributor_bill = ma.Nested('DistributorBillSchema', many=False, dump_only=True, only=('id', 'distributor', 147 | 'reference_number')) 148 | product = ma.Nested('ProductSchema', many=False, only=('id', 'name', 'retail_shop'), dump_only=True) 149 | 150 | 151 | class SaltSchema(BaseSchema): 152 | class Meta: 153 | model = Salt 154 | exclude = ('created_on', 'updated_on') 155 | 156 | retail_shop_id = ma.UUID() 157 | retail_shop = ma.Nested('RetailShopSchema', many=False, dump_only=True, only=('id', 'name')) 158 | 159 | 160 | class ComboSchema(BaseSchema): 161 | class Meta: 162 | model = Combo 163 | exclude = ('created_on', 'updated_on') 164 | retail_shop_id = ma.UUID() 165 | 166 | 167 | class AddOnSchema(BaseSchema): 168 | class Meta: 169 | model = AddOn 170 | exclude = ('created_on', 'updated_on') 171 | retail_shop_id = ma.UUID() 172 | 173 | 174 | class ProductSaltSchema(BaseSchema): 175 | 176 | class Meta: 177 | model = ProductSalt 178 | exclude = ('created_on', 'updated_on') 179 | 180 | salt_id = ma.UUID(load=True) 181 | product_id = ma.UUID(load=True) 182 | 183 | 184 | class ProductDistributorSchema(BaseSchema): 185 | 186 | class Meta: 187 | model = ProductDistributor 188 | 189 | distributor_id = ma.UUID(load=True) 190 | product_id = ma.UUID(load=True) 191 | 192 | 193 | class ProductTagSchema(BaseSchema): 194 | 195 | class Meta: 196 | model = ProductTag 197 | exclude = ('created_on', 'updated_on') 198 | 199 | tag_id = ma.UUID(load=True) 200 | product_id = ma.UUID(load=True) 201 | 202 | 203 | class BrandDistributorSchema(BaseSchema): 204 | 205 | class Meta: 206 | model = BrandDistributor 207 | exclude = ('created_on', 'updated_on') 208 | 209 | brand_id = ma.UUID(load=True) 210 | distributor_id = ma.UUID(load=True) 211 | -------------------------------------------------------------------------------- /src/utils/api.py: -------------------------------------------------------------------------------- 1 | import re 2 | from typing import TypeVar 3 | from abc import abstractproperty 4 | 5 | from flask_restful import Api 6 | from flask_restful import Resource 7 | from flask import request, jsonify, make_response 8 | from flask_security import auth_token_required, roles_accepted, roles_required 9 | from flask_excel import make_response_from_records 10 | 11 | from .models import db 12 | from .blue_prints import bp 13 | from .resource import ModelResource, AssociationModelResource 14 | from .exceptions import ResourceNotFound, SQLIntegrityError, SQlOperationalError, CustomException 15 | from .methods import BulkUpdate, List, Fetch, Create, Delete, Update 16 | 17 | ModelResourceType = TypeVar('ModelResourceType', bound=ModelResource) 18 | AssociationModelResource = TypeVar('AssociationModelResource', bound=AssociationModelResource) 19 | 20 | 21 | def to_underscore(name): 22 | s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name) 23 | return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower() 24 | 25 | 26 | class ApiFactory(Api): 27 | def init_app(self, app): 28 | super(ApiFactory, self).init_app(app) 29 | 30 | def register(self, **kwargs): 31 | 32 | def decorator(klass): 33 | document_name = klass.get_resource().model.__name__.lower() 34 | name = kwargs.pop('name', document_name) 35 | url = kwargs.pop('url', '/%s' % to_underscore(klass.get_resource().model.__name__)) 36 | endpoint = to_underscore(klass.__name__) 37 | view_func = klass.as_view(name) 38 | methods = klass.api_methods 39 | 40 | for method in methods: 41 | if method.slug: 42 | self.app.add_url_rule(url + '/', endpoint=endpoint, view_func=view_func, 43 | methods=[method.method], **kwargs) 44 | else: 45 | self.app.add_url_rule(url, endpoint=endpoint, view_func=view_func, 46 | methods=[method.method], **kwargs) 47 | return klass 48 | 49 | return decorator 50 | 51 | 52 | api = ApiFactory(bp) 53 | 54 | 55 | class BaseView(Resource): 56 | 57 | api_methods = [BulkUpdate, List, Fetch, Create, Delete, Update] 58 | 59 | def __init__(self): 60 | if self.get_resource() is not None: 61 | self.resource = self.get_resource()() 62 | self.add_method_decorator() 63 | 64 | @classmethod 65 | @abstractproperty 66 | def get_resource(cls) -> ModelResourceType: 67 | pass 68 | 69 | def add_method_decorator(self): 70 | self.method_decorators = [] 71 | if self.resource.auth_required: 72 | self.method_decorators.append(roles_required(*[i for i in self.resource.roles_required])) 73 | self.method_decorators.append(roles_accepted(*[i for i in self.resource.roles_accepted])) 74 | self.method_decorators.append(auth_token_required) 75 | 76 | def get(self, slug=None): 77 | if slug: 78 | obj = self.resource.model.query.filter(self.resource.model.id == slug) 79 | obj = self.resource.has_read_permission(obj).first() 80 | if obj: 81 | return make_response(jsonify(self.resource.schema(exclude=tuple(self.resource.obj_exclude), 82 | only=tuple(self.resource.obj_only)).dump( 83 | obj, many=False).data), 200) 84 | 85 | return make_response(jsonify({'error': True, 'message': 'Resource not found'}), 404) 86 | 87 | else: 88 | objects = self.resource.apply_filters(queryset=self.resource.model.query, **request.args) 89 | objects = self.resource.has_read_permission(objects) 90 | 91 | if '__order_by' in request.args: 92 | objects = self.resource.apply_ordering(objects, request.args['__order_by']) 93 | 94 | if '__export__' in request.args and self.resource.export is True: 95 | objects = objects.paginate(page=self.resource.page, per_page=self.resource.max_export_limit) 96 | return make_response_from_records( 97 | self.resource.schema(exclude=tuple(self.resource.obj_exclude), only=tuple(self.resource.obj_only)) 98 | .dump(objects.items, many=True).data, 'csv', 200, self.resource.model.__name__) 99 | 100 | resources = objects.paginate(page=self.resource.page, per_page=self.resource.limit) 101 | if resources.items: 102 | return make_response(jsonify({'success': True, 103 | 'data': self.resource.schema(exclude=tuple(self.resource.obj_exclude), 104 | only=tuple(self.resource.obj_only)) 105 | .dump(resources.items, many=True).data, 'total': resources.total}), 200) 106 | return make_response(jsonify({'error': True, 'message': 'No Resource Found'}), 404) 107 | 108 | def post(self): 109 | try: 110 | data, status = self.resource.save_resource() 111 | except (SQLIntegrityError, SQlOperationalError) as e: 112 | db.session.rollback() 113 | e.message['error'] = True 114 | return make_response(jsonify(e.message), e.status) 115 | return make_response(jsonify(data), status) 116 | 117 | def put(self): 118 | 119 | try: 120 | data, status = self.resource.update_resource() 121 | except (SQLIntegrityError, SQlOperationalError) as e: 122 | db.session.rollback() 123 | e.message['error'] = True 124 | return make_response(jsonify(e.message), e.status) 125 | return make_response(jsonify(data), status) 126 | 127 | def patch(self, slug): 128 | obj = self.resource.model.query.get(slug) 129 | if not obj: 130 | return make_response(jsonify({'error': True, 'message': 'Resource not found'}), 404) 131 | try: 132 | data, status = self.resource.patch_resource(obj) 133 | except (SQLIntegrityError, SQlOperationalError) as e: 134 | db.session.rollback() 135 | e.message['error'] = True 136 | return make_response(jsonify(e.message), e.status) 137 | return make_response(jsonify(data), status) 138 | 139 | def delete(self, slug): 140 | 141 | obj = self.resource.model.query.get(slug) 142 | if obj: 143 | if self.resource.has_delete_permission(obj): 144 | db.session.delete(obj) 145 | db.session.commit() 146 | return make_response(jsonify({}), 204) 147 | else: 148 | return make_response( 149 | jsonify({'error': True, 'message': 'Forbidden Permission Denied To Delete Resource'}), 403) 150 | return make_response(jsonify({'error': True, 'message': 'Resource not found'}), 404) 151 | 152 | 153 | class AssociationView(Resource): 154 | 155 | api_methods = [Create, List, Fetch] 156 | 157 | def __init__(self): 158 | if self.get_resource is not None: 159 | self.resource = self.get_resource()() 160 | self.add_method_decorator() 161 | 162 | @abstractproperty 163 | def get_resource(self) -> AssociationModelResource: 164 | pass 165 | 166 | def add_method_decorator(self): 167 | self.method_decorators = [] 168 | if self.resource.auth_required: 169 | self.method_decorators.append(roles_required(*[i for i in self.resource.roles_required])) 170 | self.method_decorators.append(roles_accepted(*[i for i in self.resource.roles_accepted])) 171 | self.method_decorators.append(auth_token_required) 172 | 173 | def get(self, slug=None): 174 | if slug: 175 | obj = self.resource.model.query.filter(self.resource.model.id == slug) 176 | obj = self.resource.has_read_permission(obj).first() 177 | if obj: 178 | return make_response(jsonify(self.resource.schema(exclude=tuple(self.resource.obj_exclude), 179 | only=tuple(self.resource.obj_only)).dump( 180 | obj, many=False).data), 200) 181 | 182 | return make_response(jsonify({'error': True, 'message': 'Resource not found'}), 404) 183 | 184 | else: 185 | objects = self.resource.apply_filters(queryset=self.resource.model.query, **request.args) 186 | objects = self.resource.has_read_permission(objects) 187 | 188 | if '__order_by' in request.args: 189 | objects = self.resource.apply_ordering(objects, request.args['__order_by']) 190 | resources = objects.paginate(page=self.resource.page, per_page=self.resource.limit) 191 | if resources.items: 192 | return make_response(jsonify({'success': True, 193 | 'data': self.resource.schema(exclude=tuple(self.resource.obj_exclude), 194 | only=tuple(self.resource.obj_only)) 195 | .dump(resources.items, many=True).data, 'total': resources.total}), 200) 196 | return make_response(jsonify({'error': True, 'message': 'No Resource Found'}), 404) 197 | 198 | def post(self): 199 | data = request.json if isinstance(request.json, list) else [request.json] 200 | for d in data: 201 | try: 202 | db.session.begin_nested() 203 | if d['__action'] == 'add': 204 | self.resource.add_relation(d) 205 | if d['__action'] == 'update': 206 | self.resource.update_relation(d) 207 | elif d['__action'] == 'remove': 208 | self.resource.remove_relation(d) 209 | except (ResourceNotFound, SQLIntegrityError, SQlOperationalError, CustomException) as e: 210 | db.session.rollback() 211 | e.message['error'] = True 212 | return make_response(jsonify(e.message), e.status) 213 | db.session.commit() 214 | 215 | return make_response(jsonify({'success': True, 'message': 'Updated Successfully', 'data': data}), 200) 216 | -------------------------------------------------------------------------------- /src/admin_panel/admin_manager.py: -------------------------------------------------------------------------------- 1 | from flask_admin.contrib.sqla.ajax import QueryAjaxModelLoader 2 | from flask_admin_impexp.admin_impexp import AdminImportExport 3 | from flask_security import current_user 4 | from src import admin, db 5 | from src.user.models import User, Role, Permission, UserRole, RetailBrand, RetailShop, UserRetailShop, \ 6 | Address, Locality, City, Customer, RegistrationDetail, CustomerAddress, CustomerTransaction, PrinterConfig 7 | from src.orders.models import OrderDiscount, Status, Item, ItemAddOn, Order, Discount, ItemTax, OrderStatus 8 | from src.products.models import ProductTax, Tax, Product, ProductType, Stock, Distributor,\ 9 | DistributorBill, Tag, Brand, Salt, AddOn, Combo, ProductSalt, BrandDistributor, ProductTag 10 | 11 | 12 | class MyModel(AdminImportExport): 13 | page_size = 100 14 | can_set_page_size = True 15 | can_view_details = True 16 | 17 | def is_accessible(self): 18 | return current_user.has_role('admin') 19 | 20 | 21 | class RetailShopAdmin(MyModel): 22 | 23 | column_filters = ('name', 'products.name') 24 | form_excluded_columns = ('products', 'brands', 'orders') 25 | 26 | form_ajax_refs = { 27 | 'registration_details': QueryAjaxModelLoader('registration_details', db.session, RegistrationDetail, 28 | fields=['name'], page_size=10), 29 | 'printer_config': QueryAjaxModelLoader('printer_config', db.session, PrinterConfig, fields=['id'], page_size=10), 30 | 'users': QueryAjaxModelLoader('users', db.session, User, fields=['name'], page_size=10), 31 | 'orders': QueryAjaxModelLoader('orders', db.session, Order, fields=['id'], page_size=10), 32 | 'retail_brand': QueryAjaxModelLoader('retail_brand', db.session, RetailBrand, fields=['name'], page_size=10), 33 | 'brands': QueryAjaxModelLoader('brands', db.session, Brand, fields=['name'], page_size=10), 34 | 'tags': QueryAjaxModelLoader('tags', db.session, Tag, fields=['name'], page_size=10), 35 | 'salts': QueryAjaxModelLoader('salts', db.session, Salt, fields=['name'], page_size=10), 36 | 'taxes': QueryAjaxModelLoader('taxes', db.session, Tax, fields=['name'], page_size=10), 37 | 'products': QueryAjaxModelLoader('products', db.session, Product, fields=['name'], page_size=10), 38 | 'distributors': QueryAjaxModelLoader('distributors', db.session, Distributor, fields=['name'], page_size=10), 39 | 40 | } 41 | 42 | 43 | class DistributorBillAdmin(MyModel): 44 | 45 | column_searchable_list = ('id', 'purchase_date', 'reference_number') 46 | column_filters = ('distributor.name', 'distributor.retail_shop.name', 'purchase_date', 'reference_number') 47 | 48 | form_ajax_refs = { 49 | 'distributor': QueryAjaxModelLoader('distributor', db.session, Distributor, fields=['name'], page_size=10), 50 | 'purchased_items': QueryAjaxModelLoader('purchased_items', db.session, Stock, fields=['product_name'], 51 | page_size=10), 52 | } 53 | 54 | 55 | class ProductAdmin(MyModel): 56 | 57 | column_filters = ('retail_shop', 'brand', 'tags', 'taxes', 'salts') 58 | 59 | column_searchable_list = ('id', 'retail_shop_id', 'name', 'quantity_label') 60 | column_editable_list = ('retail_shop_id', 'name', 'quantity_label', 'default_quantity', 'sub_description', 61 | 'is_loose', 'is_disabled', 'barcode', 'min_stock', 'auto_discount') 62 | 63 | form_ajax_refs = { 64 | 'brand': QueryAjaxModelLoader('brand', db.session, Brand, fields=['name'], page_size=10), 65 | 'retail_shop': QueryAjaxModelLoader('retail_shop', db.session, RetailShop, fields=['name'], page_size=10), 66 | 'stocks': QueryAjaxModelLoader('stocks', db.session, Stock, fields=['product_name'], page_size=10), 67 | 'tags': QueryAjaxModelLoader('tags', db.session, Tag, fields=['name'], page_size=10), 68 | 'salts': QueryAjaxModelLoader('salts', db.session, Salt, fields=['name'], page_size=10), 69 | 'taxes': QueryAjaxModelLoader('taxes', db.session, Tax, fields=['name'], page_size=10), 70 | } 71 | 72 | inline_models = (Tag, Tax, Salt) 73 | 74 | 75 | class DistributorAdmin(MyModel): 76 | 77 | column_editable_list = ('name',) 78 | column_filters = ('brands', 'retail_shop') 79 | form_ajax_refs = { 80 | 'brands': QueryAjaxModelLoader('brands', db.session, Brand, fields=['name'], page_size=10), 81 | } 82 | 83 | 84 | class TaxAdmin(MyModel): 85 | 86 | column_sortable_list = ('name',) 87 | column_searchable_list = ('id', 'retail_shop_id', 'name') 88 | column_editable_list = ('name', 'retail_shop_id') 89 | column_filters = ('products.name', 'retail_shop') 90 | 91 | form_ajax_refs = { 92 | 'products': QueryAjaxModelLoader('products', db.session, Product, fields=['name'], page_size=10), 93 | 'retail_shop': QueryAjaxModelLoader('retail_shop', db.session, RetailShop, fields=['name'], page_size=10), 94 | } 95 | 96 | 97 | class SaltAdmin(MyModel): 98 | column_sortable_list = ('name',) 99 | column_searchable_list = ('id', 'retail_shop_id', 'name') 100 | column_editable_list = ('name', 'retail_shop_id') 101 | column_filters = ('products.name', 'retail_shop') 102 | 103 | form_ajax_refs = { 104 | 'products': QueryAjaxModelLoader('products', db.session, Product, fields=['name'], page_size=10), 105 | 'retail_shop': QueryAjaxModelLoader('retail_shop', db.session, RetailShop, fields=['name'], page_size=10), 106 | } 107 | 108 | 109 | class TagAdmin(MyModel): 110 | column_sortable_list = ('name',) 111 | column_searchable_list = ('id', 'retail_shop_id', 'name') 112 | column_editable_list = ('name', 'retail_shop_id') 113 | column_filters = ('products.name', 'retail_shop') 114 | 115 | form_ajax_refs = { 116 | 'product': QueryAjaxModelLoader('product', db.session, Product, fields=['name'], page_size=10), 117 | 'retail_shop': QueryAjaxModelLoader('retail_shop', db.session, RetailShop, fields=['name'], page_size=10), 118 | } 119 | 120 | 121 | class BrandAdmin(MyModel): 122 | 123 | column_sortable_list = ('name',) 124 | column_searchable_list = ('id', 'retail_shop_id', 'name') 125 | column_editable_list = ('name', 'retail_shop_id') 126 | column_filters = ('products.name', 'retail_shop') 127 | 128 | form_ajax_refs = { 129 | 'products': QueryAjaxModelLoader('products', db.session, Product, fields=['name'], page_size=10), 130 | 'retail_shop': QueryAjaxModelLoader('retail_shop', db.session, RetailShop, fields=['name'], page_size=10), 131 | 'distributors': QueryAjaxModelLoader('distributors', db.session, Distributor, fields=['name'], page_size=10) 132 | } 133 | 134 | 135 | class BrandDistributorAdmin(MyModel): 136 | 137 | column_searchable_list = ('id', 'brand_id', 'distributor_id') 138 | 139 | 140 | class StockAdmin(MyModel): 141 | 142 | column_filters = ('product.name', 'product.retail_shop') 143 | column_editable_list = ('selling_amount', 'purchase_amount', 'batch_number', 'expiry_date', 144 | 'is_sold', 'default_stock', 'units_purchased') 145 | 146 | form_ajax_refs = { 147 | 'product': QueryAjaxModelLoader('product', db.session, Product, fields=['name'], page_size=10), 148 | 'distributor_bill': QueryAjaxModelLoader('distributor_bill', db.session, DistributorBill, 149 | fields=['distributor_name', 'retail_shop_name'], page_size=10) 150 | } 151 | 152 | 153 | class ProductTaxAdmin(MyModel): 154 | column_filters = ('product.name', 'product.retail_shop', 'tax') 155 | column_searchable_list = ('product.name', 'product.retail_shop_id', 'product.retail_shop.name') 156 | form_ajax_refs = { 157 | 'tax': QueryAjaxModelLoader('tax', db.session, Tax, fields=['name'], page_size=10), 158 | 'products': QueryAjaxModelLoader('products', db.session, Product, fields=['name'], page_size=10), 159 | } 160 | 161 | 162 | class ProductSaltAdmin(MyModel): 163 | column_filters = ('product.name', 'product.retail_shop', 'salt') 164 | column_searchable_list = ('product.name', 'product.retail_shop_id', 'product.retail_shop.name') 165 | 166 | form_ajax_refs = { 167 | 'salt': QueryAjaxModelLoader('salt', db.session, Salt, fields=['name'], page_size=10), 168 | 'product': QueryAjaxModelLoader('product', db.session, Product, fields=['name'], page_size=10), 169 | } 170 | 171 | 172 | class ProductTagAdmin(MyModel): 173 | column_filters = ('product.name', 'product.retail_shop', 'tag') 174 | column_searchable_list = ('product.name', 'product.retail_shop_id', 'product.retail_shop.name') 175 | 176 | form_ajax_refs = { 177 | 'tag': QueryAjaxModelLoader('tag', db.session, Tag, fields=['name'], page_size=10), 178 | 'product': QueryAjaxModelLoader('product', db.session, Product, fields=['name'], page_size=10), 179 | } 180 | 181 | 182 | class RoleAdmin(MyModel): 183 | column_filters = ('users.name', 'name') 184 | column_searchable_list = ('id',) 185 | 186 | 187 | class PermissionAdmin(MyModel): 188 | column_filters = ('users.name', 'name') 189 | column_searchable_list = ('id',) 190 | 191 | 192 | admin.add_view(MyModel(User, session=db.session)) 193 | admin.add_view(MyModel(Customer, session=db.session)) 194 | admin.add_view(MyModel(CustomerTransaction, session=db.session)) 195 | admin.add_view(RoleAdmin(Role, session=db.session)) 196 | admin.add_view(MyModel(UserRole, session=db.session)) 197 | admin.add_view(PermissionAdmin(Permission, session=db.session)) 198 | 199 | admin.add_view(RetailShopAdmin(RetailShop, session=db.session)) 200 | admin.add_view(MyModel(RetailBrand, session=db.session)) 201 | admin.add_view(MyModel(UserRetailShop, session=db.session)) 202 | admin.add_view(MyModel(RegistrationDetail, session=db.session)) 203 | admin.add_view(MyModel(PrinterConfig, session=db.session)) 204 | admin.add_view(MyModel(Address, session=db.session)) 205 | admin.add_view(MyModel(CustomerAddress, session=db.session)) 206 | admin.add_view(MyModel(Locality, session=db.session)) 207 | admin.add_view(MyModel(City, session=db.session)) 208 | 209 | 210 | admin.add_view(ProductAdmin(Product, session=db.session)) 211 | admin.add_view(TagAdmin(Tag, session=db.session)) 212 | admin.add_view(SaltAdmin(Salt, session=db.session)) 213 | admin.add_view(BrandAdmin(Brand, session=db.session)) 214 | admin.add_view(StockAdmin(Stock, session=db.session)) 215 | admin.add_view(DistributorAdmin(Distributor, session=db.session)) 216 | admin.add_view(TaxAdmin(Tax, session=db.session)) 217 | 218 | admin.add_view(ProductTaxAdmin(ProductTax, session=db.session)) 219 | admin.add_view(ProductSaltAdmin(ProductSalt, session=db.session)) 220 | admin.add_view(ProductTagAdmin(ProductTag, session=db.session)) 221 | admin.add_view(MyModel(AddOn, session=db.session)) 222 | admin.add_view(MyModel(Combo, session=db.session)) 223 | admin.add_view(MyModel(ProductType, session=db.session)) 224 | admin.add_view(DistributorBillAdmin(DistributorBill, session=db.session)) 225 | admin.add_view(BrandDistributorAdmin(BrandDistributor, session=db.session)) 226 | 227 | 228 | admin.add_view(MyModel(Order, session=db.session)) 229 | admin.add_view(MyModel(OrderStatus, session=db.session)) 230 | admin.add_view(MyModel(Item, session=db.session)) 231 | admin.add_view(MyModel(ItemTax, session=db.session)) 232 | admin.add_view(MyModel(ItemAddOn, session=db.session)) 233 | admin.add_view(MyModel(Status, session=db.session)) 234 | admin.add_view(MyModel(OrderDiscount, session=db.session)) 235 | admin.add_view(MyModel(Discount, session=db.session)) 236 | -------------------------------------------------------------------------------- /src/user/models.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.ext.hybrid import hybrid_property, hybrid_method 2 | from sqlalchemy.dialects.postgresql import UUID 3 | 4 | from flask_security import RoleMixin, UserMixin 5 | from sqlalchemy import UniqueConstraint, func 6 | 7 | from src.orders.models import Order 8 | from src import db, BaseMixin, ReprMixin 9 | 10 | 11 | class UserRetailShop(BaseMixin, db.Model): 12 | 13 | user_id = db.Column(UUID, db.ForeignKey('user.id', ondelete='CASCADE'), index=True, nullable=False) 14 | retail_shop_id = db.Column(UUID, db.ForeignKey('retail_shop.id', ondelete='CASCADE'), index=True, nullable=False) 15 | 16 | user = db.relationship('User', foreign_keys=[user_id]) 17 | retail_shop = db.relationship('RetailShop', foreign_keys=[retail_shop_id]) 18 | 19 | UniqueConstraint(user_id, retail_shop_id) 20 | 21 | 22 | class RetailShopLocality(BaseMixin, db.Model): 23 | 24 | locality_id = db.Column(UUID, db.ForeignKey('locality.id', ondelete='CASCADE'), index=True) 25 | retail_shop_id = db.Column(UUID, db.ForeignKey('retail_shop.id', ondelete='CASCADE'), index=True) 26 | 27 | locality = db.relationship('Locality', foreign_keys=[locality_id]) 28 | retail_shop = db.relationship('RetailShop', foreign_keys=[retail_shop_id]) 29 | 30 | UniqueConstraint(locality_id, retail_shop_id) 31 | 32 | 33 | class CustomerAddress(BaseMixin, db.Model, ReprMixin): 34 | 35 | customer_id = db.Column(UUID, db.ForeignKey('customer.id'), index=True) 36 | address_id = db.Column(UUID, db.ForeignKey('address.id'), unique=True) 37 | 38 | customer = db.relationship('Customer', foreign_keys=[customer_id]) 39 | address = db.relationship('Address', foreign_keys=[address_id]) 40 | 41 | UniqueConstraint(customer_id, address_id) 42 | 43 | 44 | class RetailBrandAddress(BaseMixin, db.Model, ReprMixin): 45 | retail_brand_id = db.Column(UUID, db.ForeignKey('retail_brand.id'), index=True) 46 | address_id = db.Column(UUID, db.ForeignKey('address.id'), index=True) 47 | 48 | retail_brand = db.relationship('RetailBrand', foreign_keys=[retail_brand_id]) 49 | address = db.relationship('Address', foreign_keys=[address_id]) 50 | 51 | UniqueConstraint(retail_brand_id, address_id) 52 | 53 | 54 | class RetailBrand(BaseMixin, db.Model, ReprMixin): 55 | name = db.Column(db.String(80), unique=True) 56 | 57 | retail_shops = db.relationship('RetailShop', back_populates='retail_brand', uselist=True, 58 | cascade='all, delete-orphan') 59 | users = db.relationship('User', back_populates='retail_brand', uselist=True, cascade='all, delete-orphan') 60 | addresses = db.relationship('Address', secondary='retail_brand_address') 61 | 62 | 63 | class RegistrationDetail(BaseMixin, db.Model, ReprMixin): 64 | 65 | name = db.Column(db.String(55), nullable=False) 66 | value = db.Column(db.String(20), nullable=False) 67 | 68 | retail_shop_id = db.Column(UUID, db.ForeignKey('retail_shop.id', ondelete='CASCADE'), index=True) 69 | retail_shop = db.relationship('RetailShop', foreign_keys=[retail_shop_id], back_populates='registration_details') 70 | 71 | 72 | class RetailShop(BaseMixin, db.Model, ReprMixin): 73 | name = db.Column(db.String(80), unique=False) 74 | identity = db.Column(db.String(80), unique=False) 75 | 76 | retail_brand_id = db.Column(UUID, db.ForeignKey('retail_brand.id'), index=True) 77 | address_id = db.Column(UUID, db.ForeignKey('address.id'), unique=True, index=True) 78 | invoice_number = db.Column(db.Integer, default=0, nullable=False) 79 | separate_offline_billing = db.Column(db.Boolean, default=False) 80 | 81 | retail_brand = db.relationship('RetailBrand', foreign_keys=[retail_brand_id], back_populates='retail_shops') 82 | users = db.relationship('User', back_populates='retail_shops', secondary='user_retail_shop', lazy='dynamic') 83 | products = db.relationship('Product', uselist=True, back_populates='retail_shop') 84 | orders = db.relationship('Order', uselist=True, back_populates='retail_shop', lazy='dynamic') 85 | address = db.relationship('Address', foreign_keys=[address_id], uselist=False) 86 | localities = db.relationship('Locality', secondary='retail_shop_locality') 87 | registration_details = db.relationship('RegistrationDetail', uselist=True, lazy='dynamic') 88 | printer_config = db.relationship('PrinterConfig', uselist=False) 89 | 90 | @hybrid_property 91 | def total_sales(self): 92 | data = self.orders.with_entities(func.Sum(Order.total), func.Count(Order.id), func.Sum(Order.items_count))\ 93 | .filter(Order.retail_shop_id == self.id).all()[0] 94 | 95 | return {'total_sales': data[0], 'total_orders': data[1], 'total_items': str(data[2])} 96 | 97 | 98 | class UserRole(BaseMixin, db.Model): 99 | 100 | user_id = db.Column(UUID, db.ForeignKey('user.id', ondelete='CASCADE'), index=True) 101 | role_id = db.Column(UUID, db.ForeignKey('role.id', ondelete='CASCADE'), index=True) 102 | 103 | user = db.relationship('User', foreign_keys=[user_id]) 104 | role = db.relationship('Role', foreign_keys=[role_id]) 105 | 106 | UniqueConstraint(user_id, role_id) 107 | 108 | 109 | class UserPermission(BaseMixin, db.Model): 110 | 111 | user_id = db.Column(UUID, db.ForeignKey('user.id', ondelete='CASCADE'), index=True) 112 | permission_id = db.Column(UUID, db.ForeignKey('permission.id', ondelete='CASCADE'), index=True) 113 | 114 | user = db.relationship('User', foreign_keys=[user_id]) 115 | permission = db.relationship('Permission', foreign_keys=[permission_id]) 116 | 117 | UniqueConstraint(user_id, permission_id) 118 | 119 | 120 | class Role(BaseMixin, db.Model, RoleMixin, ReprMixin): 121 | name = db.Column(db.String(80), unique=True) 122 | description = db.Column(db.String(255)) 123 | is_hidden = db.Column(db.Boolean(), default=False) 124 | 125 | permissions = db.relationship('Permission', uselist=True, lazy='dynamic', back_populates='role') 126 | users = db.relationship('User', back_populates='roles', secondary='user_role') 127 | 128 | 129 | class User(BaseMixin, db.Model, UserMixin, ReprMixin): 130 | email = db.Column(db.String(127), unique=True, nullable=False) 131 | password = db.Column(db.String(255), default='', nullable=False) 132 | name = db.Column(db.String(55), nullable=False) 133 | mobile_number = db.Column(db.String(20), unique=True, nullable=False) 134 | 135 | active = db.Column(db.Boolean()) 136 | confirmed_at = db.Column(db.DateTime()) 137 | last_login_at = db.Column(db.DateTime()) 138 | current_login_at = db.Column(db.DateTime()) 139 | 140 | last_login_ip = db.Column(db.String(45)) 141 | current_login_ip = db.Column(db.String(45)) 142 | login_count = db.Column(db.Integer) 143 | 144 | retail_brand_id = db.Column(UUID, db.ForeignKey('retail_brand.id'), index=True) 145 | 146 | retail_brand = db.relationship('RetailBrand', foreign_keys=[retail_brand_id], back_populates='users') 147 | roles = db.relationship('Role', back_populates='users', secondary='user_role') 148 | permissions = db.relationship('Permission', back_populates='users', secondary='user_permission', lazy='dynamic') 149 | retail_shops = db.relationship('RetailShop', back_populates='users', secondary='user_retail_shop', lazy='dynamic') 150 | 151 | @hybrid_property 152 | def retail_shop_ids(self): 153 | return [i[0] for i in self.retail_shops.with_entities(RetailShop.id).all()] 154 | 155 | @retail_shop_ids.expression 156 | def retail_shop_ids(self): 157 | from sqlalchemy import select 158 | return select([UserRetailShop.retail_shop_id]).where(UserRetailShop.user_id == self.id).label('retail_shop_ids').limit(1) 159 | 160 | @hybrid_method 161 | def has_shop_access(self, shop_id): 162 | return db.session.query(UserRetailShop.query.filter(UserRetailShop.retail_shop_id == shop_id, 163 | UserRetailShop.user_id == self.id).exists()).scalar() 164 | 165 | @hybrid_method 166 | def has_permission(self, permission): 167 | return db.session.query(self.permissions.filter(Permission.name == permission).exists()).scalar() 168 | 169 | @hybrid_property 170 | def is_owner(self): 171 | return self.has_role('owner') 172 | 173 | 174 | class Permission(BaseMixin, db.Model, ReprMixin): 175 | 176 | name = db.Column(db.String(80), unique=True) 177 | description = db.Column(db.String(255)) 178 | type = db.Column(db.String(15), nullable=True) 179 | is_hidden = db.Column(db.Boolean(), default=False) 180 | role_id = db.Column(UUID, db.ForeignKey('role.id'), nullable=True) 181 | 182 | role = db.relationship('Role', back_populates='permissions', uselist=False) 183 | users = db.relationship('User', back_populates='permissions', secondary='user_permission') 184 | 185 | 186 | class Customer(BaseMixin, db.Model, ReprMixin): 187 | 188 | email = db.Column(db.String(55), nullable=True) 189 | name = db.Column(db.String(55), nullable=True) 190 | active = db.Column(db.Boolean()) 191 | mobile_number = db.Column(db.String(20), nullable=True) 192 | loyalty_points = db.Column(db.Integer, default=0) 193 | retail_brand_id = db.Column(UUID, db.ForeignKey('retail_brand.id'), index=True) 194 | 195 | retail_brand = db.relationship('RetailBrand', foreign_keys=[retail_brand_id]) 196 | addresses = db.relationship('Address', secondary='customer_address') 197 | orders = db.relationship('Order', uselist=True, lazy='dynamic') 198 | transactions = db.relationship('CustomerTransaction', uselist=True, lazy='dynamic') 199 | 200 | @hybrid_property 201 | def total_orders(self): 202 | return self.orders.with_entities(func.coalesce(func.Count(Order.id), 0)).scalar() 203 | 204 | @hybrid_property 205 | def total_billing(self): 206 | return self.orders.with_entities(func.coalesce(func.Sum(Order.total), 0)).scalar() 207 | 208 | @hybrid_property 209 | def amount_due(self): 210 | return self.orders.with_entities(func.coalesce(func.Sum(Order.total), 0) - 211 | func.coalesce(func.Sum(Order.amount_paid), 0)).scalar() - \ 212 | self.transactions.with_entities(func.coalesce(func.Sum(CustomerTransaction.amount), 0)).scalar() 213 | 214 | 215 | class CustomerTransaction(BaseMixin, db.Model, ReprMixin): 216 | 217 | amount = db.Column(db.Float(precision=2), nullable=False, default=0) 218 | customer_id = db.Column(UUID, db.ForeignKey('customer.id'), nullable=False, index=True) 219 | customer = db.relationship('Customer', foreign_keys=[customer_id]) 220 | 221 | 222 | class Address(BaseMixin, db.Model, ReprMixin): 223 | 224 | name = db.Column(db.Text, nullable=False) 225 | locality_id = db.Column(UUID, db.ForeignKey('locality.id'), index=True) 226 | locality = db.relationship('Locality', uselist=False) 227 | 228 | 229 | class Locality(BaseMixin, db.Model, ReprMixin): 230 | 231 | name = db.Column(db.Text, nullable=False) 232 | city_id = db.Column(UUID, db.ForeignKey('city.id'), index=True) 233 | 234 | city = db.relationship('City', uselist=False) 235 | 236 | UniqueConstraint(city_id, name) 237 | 238 | 239 | class City(BaseMixin, db.Model, ReprMixin): 240 | 241 | name = db.Column(db.Text, nullable=False, unique=True) 242 | 243 | 244 | class PrinterConfig(BaseMixin, db.Model, ReprMixin): 245 | 246 | header = db.Column(db.Text, nullable=True) 247 | footer = db.Column(db.Text, nullable=True) 248 | 249 | bill_template = db.Column(db.Text, nullable=True) 250 | receipt_template = db.Column(db.Text, nullable=True) 251 | 252 | bill_printer_type = db.Column(db.Enum('thermal', 'dot_matrix', 'laser', name='varchar')) 253 | receipt_printer_type = db.Column(db.Enum('thermal', 'dot_matrix', 'laser', name='varchar')) 254 | label_printer_type = db.Column(db.Enum('1x1', '2x1', '3x1', '4x1', name='varchar')) 255 | 256 | have_receipt_printer = db.Column(db.Boolean(), default=False) 257 | have_bill_printer = db.Column(db.Boolean(), default=False) 258 | 259 | retail_shop_id = db.Column(UUID, db.ForeignKey('retail_shop.id')) 260 | 261 | retail_shops = db.relationship('RetailShop', back_populates='printer_config', 262 | foreign_keys=[retail_shop_id]) -------------------------------------------------------------------------------- /src/utils/resource.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import Type, List, Tuple 3 | 4 | from flask import request 5 | from sqlalchemy.exc import OperationalError, IntegrityError 6 | 7 | from .exceptions import ResourceNotFound, SQLIntegrityError, SQlOperationalError, CustomException, RequestNotAllowed 8 | from .models import db 9 | 10 | 11 | class ModelResource(ABC): 12 | model = None 13 | schema = None 14 | 15 | filters = {} 16 | 17 | max_limit: int = 100 18 | 19 | default_limit: int = 50 20 | 21 | exclude_related_resource: Tuple[str] = () 22 | 23 | order_by: List[str] = [] 24 | 25 | only: Tuple[str] = () 26 | 27 | exclude: Tuple[str] = () 28 | 29 | include: Tuple[str] = () 30 | 31 | optional: Tuple[str] = () 32 | 33 | page: int = 1 34 | 35 | auth_required: bool = False 36 | 37 | export: bool = False 38 | 39 | max_export_limit: int = 5000 40 | 41 | roles_accepted: Tuple[str] = () 42 | 43 | roles_required: Tuple[str] = () 44 | 45 | def __init__(self): 46 | 47 | if request.args.getlist('__only'): 48 | if len(request.args.getlist('__only')) == 1: 49 | self.obj_only = tuple(request.args.getlist('__only')[0].split(',')) 50 | else: 51 | self.obj_only = tuple(request.args.getlist('__only')) 52 | else: 53 | self.obj_only = self.only 54 | 55 | self.obj_exclude = [] 56 | if request.args.getlist('__exclude'): 57 | if len(request.args.getlist('__exclude')) == 1: 58 | self.obj_exclude = request.args.getlist('__exclude')[0].split(',') 59 | else: 60 | self.obj_exclude = request.args.getlist('__exclude') 61 | 62 | self.obj_exclude.extend(list(self.exclude)) 63 | self.obj_optional = list(self.optional) 64 | 65 | if request.args.getlist('__include'): 66 | if len(request.args.getlist('__include')) == 1: 67 | optionals = request.args.getlist('__include')[0].split(',') 68 | else: 69 | optionals = request.args.getlist('__include') 70 | 71 | for optional in optionals: 72 | try: 73 | self.obj_optional.remove(optional) 74 | except ValueError: 75 | pass 76 | 77 | self.obj_exclude.extend(self.obj_optional) 78 | 79 | self.page = int(request.args.get('__page')) if request.args.get('__page') else 1 80 | self.limit = int(request.args.get('__limit')) if request.args.get('__limit') \ 81 | and int( 82 | request.args.get('__limit')) <= self.max_limit else self.default_limit 83 | 84 | def apply_filters(self, queryset, **kwargs): 85 | for k, v in kwargs.items(): 86 | array_key = k.split('__') 87 | if array_key[0] == '' and array_key[1] in self.filters.keys(): 88 | for operator in self.filters.get(array_key[1]): 89 | if operator.op == array_key[2]: 90 | queryset = operator().prepare_queryset(queryset, self.model, array_key[1], v) 91 | 92 | if '__distinct_by' in request.args: 93 | queryset = queryset.distinct(getattr(self.model, request.args['__distinct_by'])) 94 | return queryset 95 | 96 | def apply_ordering(self, queryset, order_by): 97 | desc = False 98 | if order_by.startswith('-'): 99 | desc = True 100 | order_by = order_by.replace('-', '') 101 | if order_by in self.order_by: 102 | if desc: 103 | queryset = queryset.order_by(getattr(self.model, order_by).desc()) 104 | else: 105 | queryset = queryset.order_by(getattr(self.model, order_by)) 106 | return queryset 107 | 108 | def patch_resource(self, obj): 109 | if self.has_change_permission(obj) and obj: 110 | obj, errors = self.schema(exclude=self.exclude_related_resource).load(request.json, instance=obj, 111 | partial=True) 112 | if errors: 113 | db.session.rollback() 114 | return {'error': True, 'message': str(errors)}, 400 115 | 116 | try: 117 | db.session.commit() 118 | except IntegrityError: 119 | db.session.rollback() 120 | raise SQLIntegrityError(data={}, message='Integrity Error', operation='Adding Resource', 121 | status=400) 122 | except OperationalError: 123 | db.session.rollback() 124 | raise SQlOperationalError(data={}, message='Operational Error', operation='Adding Resource', 125 | status=400) 126 | return {'success': True, 'message': 'obj updated successfully', 127 | 'data': self.schema(exclude=tuple(self.obj_exclude), only=tuple(self.obj_only)) 128 | .dump(obj).data}, 200 129 | 130 | return {'error': True, 'message': 'Forbidden Permission Denied To Change Resource'}, 403 131 | 132 | def update_resource(self): 133 | data = request.json if isinstance(request.json, list) else [request.json] 134 | objects = [] 135 | for d in data: 136 | obj = self.schema().get_instance(d) 137 | obj, errors = self.schema().load(d, instance=obj) 138 | if errors: 139 | db.session.rollback() 140 | return {'error': True, 'message': str(errors)}, 400 141 | 142 | if not self.has_change_permission(obj): 143 | db.session.rollback() 144 | return {'error': True, 'message': 'Forbidden Permission Denied To Add Resource'}, 403 145 | try: 146 | db.session.commit() 147 | objects.append(obj) 148 | except IntegrityError: 149 | db.session.rollback() 150 | raise SQLIntegrityError(data=d, message='Integrity Error', operation='Updating Resource', status=400) 151 | except OperationalError: 152 | db.session.rollback() 153 | raise SQlOperationalError(data=d, message='Operational Error', operation='Updating Resource', 154 | status=400) 155 | return {'success': True, 'message': 'Resource Updated successfully', 156 | 'data': self.schema(exclude=tuple(self.obj_exclude), only=tuple(self.obj_only)) 157 | .dump(objects, many=True).data}, 201 158 | 159 | def save_resource(self): 160 | data = request.json if isinstance(request.json, list) else [request.json] 161 | objects, errors = self.schema().load(data, session=db.session, many=True) 162 | if errors: 163 | db.session.rollback() 164 | return {'error': True, 'message': str(errors)}, 400 165 | 166 | if self.has_add_permission(objects): 167 | db.session.add_all(objects) 168 | else: 169 | db.session.rollback() 170 | return {'error': True, 'message': 'Forbidden Permission Denied To Add Resource'}, 403 171 | try: 172 | db.session.commit() 173 | except IntegrityError as e: 174 | db.session.rollback() 175 | print(e) 176 | raise SQLIntegrityError(data=data, message='Integrity Error', operation='Adding Resource', status=400) 177 | except OperationalError: 178 | db.session.rollback() 179 | raise SQlOperationalError(data=data, message='Operational Error', operation='Adding Resource', status=400) 180 | return {'success': True, 'message': 'Resource added successfully', 181 | 'data': self.schema(exclude=tuple(self.obj_exclude), only=tuple(self.obj_only)) 182 | .dump(objects, many=True).data}, 201 183 | 184 | @abstractmethod 185 | def has_read_permission(self, qs) -> Type(db.Model): 186 | return qs 187 | 188 | @abstractmethod 189 | def has_change_permission(self, obj) -> bool: 190 | return True 191 | 192 | @abstractmethod 193 | def has_delete_permission(self, obj) -> bool: 194 | return True 195 | 196 | @abstractmethod 197 | def has_add_permission(self, obj) -> bool: 198 | return True 199 | 200 | 201 | class AssociationModelResource(ABC): 202 | model = None 203 | 204 | schema = None 205 | 206 | filters = {} 207 | 208 | max_limit: int = 100 209 | 210 | default_limit: int = 50 211 | 212 | exclude_related_resource: Tuple[str] = () 213 | 214 | order_by: List[str] = [] 215 | 216 | only: Tuple[str] = () 217 | 218 | exclude: Tuple[str] = () 219 | 220 | include: Tuple[str] = () 221 | 222 | optional: Tuple[str] = () 223 | 224 | page: int = 1 225 | 226 | auth_required = False 227 | 228 | roles_accepted: Tuple[str] = () 229 | 230 | roles_required: Tuple[str] = () 231 | 232 | def __init__(self): 233 | 234 | if request.args.getlist('__only'): 235 | if len(request.args.getlist('__only')) == 1: 236 | self.obj_only = tuple(request.args.getlist('__only')[0].split(',')) 237 | else: 238 | self.obj_only = tuple(request.args.getlist('__only')) 239 | else: 240 | self.obj_only = self.only 241 | 242 | self.obj_exclude = [] 243 | if request.args.getlist('__exclude'): 244 | if len(request.args.getlist('__exclude')) == 1: 245 | self.obj_exclude = request.args.getlist('__exclude')[0].split(',') 246 | else: 247 | self.obj_exclude = request.args.getlist('__exclude') 248 | 249 | self.obj_exclude.extend(list(self.exclude)) 250 | self.obj_optional = list(self.optional) 251 | 252 | if request.args.getlist('__include'): 253 | if len(request.args.getlist('__include')) == 1: 254 | optionals = request.args.getlist('__include')[0].split(',') 255 | else: 256 | optionals = request.args.getlist('__include') 257 | 258 | for optional in optionals: 259 | try: 260 | self.obj_optional.remove(optional) 261 | except ValueError: 262 | pass 263 | 264 | self.obj_exclude.extend(self.obj_optional) 265 | 266 | self.page = int(request.args.get('__page')) if request.args.get('__page') else 1 267 | self.limit = int(request.args.get('__limit')) if request.args.get('__limit') \ 268 | and int( 269 | request.args.get('__limit')) <= self.max_limit else self.default_limit 270 | 271 | def apply_filters(self, queryset, **kwargs): 272 | for k, v in kwargs.items(): 273 | array_key = k.split('__') 274 | if array_key[0] == '' and array_key[1] in self.filters.keys(): 275 | for operator in self.filters.get(array_key[1]): 276 | if operator.op == array_key[2]: 277 | queryset = operator().prepare_queryset(queryset, self.model, array_key[1], v) 278 | 279 | return queryset 280 | 281 | def apply_ordering(self, queryset, order_by): 282 | desc = False 283 | if order_by.startswith('-'): 284 | desc = True 285 | order_by = order_by.replace('-', '') 286 | if order_by in self.order_by: 287 | if desc: 288 | queryset = queryset.order_by(getattr(self.model, order_by).desc()) 289 | else: 290 | queryset = queryset.order_by(getattr(self.model, order_by)) 291 | 292 | return queryset 293 | 294 | def add_relation(self, data): 295 | obj, errors = self.schema().load(data, session=db.session) 296 | if errors: 297 | raise CustomException(data=data, message=str(errors), operation='adding relation') 298 | 299 | if self.has_add_permission(obj, data): 300 | db.session.add(obj) 301 | try: 302 | db.session.commit() 303 | except IntegrityError as e: 304 | raise SQLIntegrityError(data=data, message=str(e), operation='adding relation', status=400) 305 | except OperationalError as e: 306 | raise SQLIntegrityError(data=data, message=str(e), operation='adding relation', status=400) 307 | else: 308 | raise RequestNotAllowed(data=data, message='Object not Found', operation='adding relation', 309 | status=401) 310 | 311 | def update_relation(self, data): 312 | obj = self.model.query.get(data['id']) 313 | if obj: 314 | obj, errors = self.schema().load(data, instance=obj) 315 | if errors: 316 | raise CustomException(data=data, message=str(errors), operation='updating relation') 317 | if self.has_change_permission(obj, data): 318 | raise CustomException(data=data, message='Permission Denied', operation='adding relation') 319 | try: 320 | db.session.commit() 321 | except IntegrityError: 322 | db.session.rollback() 323 | raise SQLIntegrityError(data=data, message='Integrity Error', operation='Adding Resource', status=400) 324 | except OperationalError: 325 | db.session.rollback() 326 | raise SQlOperationalError(data=data, message='Operational Error', operation='Adding Resource', 327 | status=400) 328 | else: 329 | raise RequestNotAllowed(data=data, message='Object not Found', operation='deleting relation', 330 | status=401) 331 | else: 332 | raise ResourceNotFound(data=data, message='Object not Found', operation='Updating relation', status=404) 333 | 334 | def remove_relation(self, data): 335 | obj = self.model.query 336 | for k, v in data.items(): 337 | if hasattr(self.model, k): 338 | obj = obj.filter(getattr(self.model, k) == v) 339 | obj = obj.first() 340 | if obj: 341 | if self.has_delete_permission(obj, data): 342 | db.session.delete(obj) 343 | try: 344 | db.session.commit() 345 | except IntegrityError: 346 | raise SQLIntegrityError(data=data, message='Integrity Error', operation='deleting relation', 347 | status=400) 348 | except OperationalError: 349 | raise SQLIntegrityError(data=data, message='Operational Error', operation='deleting relation', 350 | status=400) 351 | else: 352 | raise RequestNotAllowed(data=data, message='Object not Found', operation='deleting relation', 353 | status=401) 354 | else: 355 | raise ResourceNotFound(data=data, message='Object not Found', operation='deleting relation', status=404) 356 | 357 | @abstractmethod 358 | def has_read_permission(self, qs): 359 | return qs 360 | 361 | @abstractmethod 362 | def has_change_permission(self, obj, data) -> bool: 363 | return True 364 | 365 | @abstractmethod 366 | def has_delete_permission(self, obj, data) -> bool: 367 | return True 368 | 369 | @abstractmethod 370 | def has_add_permission(self, obj, data) -> bool: 371 | return True 372 | -------------------------------------------------------------------------------- /src/user/resources.py: -------------------------------------------------------------------------------- 1 | from flask_security import current_user 2 | from sqlalchemy import or_, false 3 | from src.utils import ModelResource, operators as ops, AssociationModelResource 4 | from .schemas import User, UserSchema, Role, RoleSchema, UserRole, UserRoleSchema,\ 5 | RetailBrandSchema, RetailShopSchema, UserRetailShopSchema, CustomerSchema, AddressSchema, CitySchema,\ 6 | LocalitySchema, CustomerAddressSchema, CustomerTransactionSchema, PermissionSchema, UserPermissionSchema,\ 7 | PrinterConfigSchema, RegistrationDetailSchema 8 | from .models import RetailShop, RetailBrand, UserRetailShop, Customer, Locality, City, Address, CustomerAddress,\ 9 | CustomerTransaction, Permission, UserPermission, PrinterConfig, RegistrationDetail 10 | 11 | 12 | class UserResource(ModelResource): 13 | 14 | model = User 15 | schema = UserSchema 16 | 17 | auth_required = True 18 | 19 | roles_accepted = ('admin', 'owner', 'staff') 20 | 21 | optional = ('retail_shops', 'current_login_at', 'current_login_ip', 'created_on', 22 | 'last_login_at', 'last_login_ip', 'login_count', 'confirmed_at', 'permissions') 23 | 24 | filters = { 25 | 'username': [ops.Equal, ops.Contains], 26 | 'name': [ops.Equal, ops.Contains], 27 | 'active': [ops.Boolean], 28 | 'id': [ops.Equal], 29 | 'retail_brand_id': [ops.Equal, ops.In] 30 | } 31 | 32 | related_resource = { 33 | 34 | } 35 | 36 | order_by = ['email', 'id', 'name'] 37 | 38 | only = () 39 | 40 | exclude = () 41 | 42 | def has_read_permission(self, qs): 43 | if current_user.has_role('admin') or current_user.has_role('owner'): 44 | return qs.filter(User.retail_brand_id == current_user.retail_brand_id) 45 | else: 46 | return qs.filter(User.id == current_user.id) 47 | 48 | def has_change_permission(self, obj): 49 | if current_user.has_role('admin') or current_user.has_role('owner'): 50 | if current_user.retail_brand_id == obj.retail_brand_id: 51 | return True 52 | return False 53 | 54 | def has_delete_permission(self, obj): 55 | if current_user.has_role('admin') or current_user.has_role('owner'): 56 | if current_user.retail_brand_id == obj.retail_brand_id: 57 | return True 58 | return False 59 | 60 | def has_add_permission(self, obj): 61 | if current_user.has_role('admin') or current_user.has_role('owner'): 62 | if current_user.retail_brand_id == obj.retail_brand_id: 63 | return True 64 | return False 65 | 66 | 67 | class RetailShopResource(ModelResource): 68 | model = RetailShop 69 | schema = RetailShopSchema 70 | 71 | optional = ('localities', 'total_sales', 'retail_brand', 'printer_config', 'registration_details') 72 | 73 | filters = { 74 | 'id': [ops.Equal, ops.In] 75 | } 76 | 77 | auth_required = True 78 | roles_accepted = ('admin', 'owner', 'staff') 79 | 80 | def has_read_permission(self, qs): 81 | if current_user.has_permission('view_shop'): 82 | return qs.filter(self.model.retail_brand_id == current_user.retail_brand_id) 83 | 84 | def has_change_permission(self, obj): 85 | if obj.retail_brand_id == current_user.retail_brand_id and current_user.has_permission('change_shop'): 86 | 87 | return True 88 | return False 89 | 90 | def has_delete_permission(self, obj): 91 | if obj.retail_brand_id == current_user.retail_brand_id and current_user.has_permission('delete_shop'): 92 | 93 | return True 94 | return False 95 | 96 | def has_add_permission(self, obj): 97 | if obj.retail_brand_id == current_user.retail_brand_id and current_user.has_permission('add_shop'): 98 | 99 | return True 100 | return False 101 | 102 | 103 | class RetailBrandResource(ModelResource): 104 | model = RetailBrand 105 | schema = RetailBrandSchema 106 | 107 | auth_required = True 108 | roles_accepted = ('admin', 'owner', 'staff') 109 | 110 | def has_read_permission(self, qs): 111 | if current_user.has_permission('view_shop'): 112 | return qs.filter(self.model.retail_brand_id == current_user.retail_brand_id) 113 | 114 | def has_change_permission(self, obj): 115 | if obj.retail_brand_id == current_user.retail_brand_id and current_user.has_permission('change_shop'): 116 | return True 117 | return False 118 | 119 | def has_delete_permission(self, obj): 120 | if obj.retail_brand_id == current_user.retail_brand_id and current_user.has_permission('delete_shop'): 121 | return True 122 | return False 123 | 124 | def has_add_permission(self, obj): 125 | if obj.retail_brand_id == current_user.retail_brand_id and current_user.has_permission('add_shop'): 126 | return True 127 | return False 128 | 129 | 130 | class UserRetailShopResource(AssociationModelResource): 131 | 132 | model = UserRetailShop 133 | schema = UserRetailShopSchema 134 | 135 | auth_required = True 136 | roles_accepted = ('admin', 'owner') 137 | 138 | def has_read_permission(self, qs): 139 | if current_user.has_permission('view_user_shops'): 140 | return qs.filter(self.model.retail_shop_id.in_(current_user.retail_shop_ids)).all() 141 | 142 | def has_change_permission(self, obj, data): 143 | if current_user.has_permission('change_user_shops') and current_user.has_shop_access(obj.retail_shop_id): 144 | return True 145 | return False 146 | 147 | def has_delete_permission(self, obj, data): 148 | if current_user.has_permission('delete_user_shops') and current_user.has_shop_access(obj.retail_shop_id): 149 | return True 150 | return False 151 | 152 | def has_add_permission(self, obj, data): 153 | if current_user.has_permission('add_user_shops'): 154 | if not current_user.has_shop_access(data['retail_shop_id']): 155 | return False 156 | return True 157 | return False 158 | 159 | 160 | class CustomerResource(ModelResource): 161 | model = Customer 162 | schema = CustomerSchema 163 | 164 | optional = ('addresses', 'orders', 'retail_brand', 'transactions') 165 | 166 | filters = { 167 | 'name': [ops.Equal, ops.Contains], 168 | 'mobile_number': [ops.Equal, ops.Contains], 169 | 'email': [ops.Equal, ops.Contains], 170 | 'id': [ops.Equal], 171 | 'retail_brand_id': [ops.Equal], 172 | 'retail_shop_id': [ops.Equal, ops.In] 173 | } 174 | 175 | auth_required = True 176 | roles_accepted = ('admin', 'owner', 'staff') 177 | 178 | def has_read_permission(self, qs): 179 | if current_user.has_permission('view_customer'): 180 | return qs.filter(self.model.retail_brand_id == current_user.retail_brand_id) 181 | 182 | def has_change_permission(self, obj): 183 | if obj.retail_brand_id == current_user.retail_brand_id and current_user.has_permission('change_customer'): 184 | return True 185 | return False 186 | 187 | def has_delete_permission(self, obj): 188 | if obj.retail_brand_id == current_user.retail_brand_id and current_user.has_permission('delete_customer'): 189 | return True 190 | return False 191 | 192 | def has_add_permission(self, objects): 193 | if not current_user.has_permission('add_customer'): 194 | return False 195 | for obj in objects: 196 | if not str(obj.retail_brand_id) == current_user.retail_brand_id: 197 | return False 198 | return True 199 | 200 | 201 | class AddressResource(ModelResource): 202 | model = Address 203 | schema = AddressSchema 204 | 205 | auth_required = True 206 | roles_accepted = ('admin', 'owner', 'staff') 207 | 208 | def has_read_permission(self, qs): 209 | return qs 210 | 211 | def has_change_permission(self, obj): 212 | return True 213 | 214 | def has_delete_permission(self, obj): 215 | return True 216 | 217 | def has_add_permission(self, obj): 218 | return True 219 | 220 | 221 | class LocalityResource(ModelResource): 222 | model = Locality 223 | schema = LocalitySchema 224 | 225 | auth_required = True 226 | roles_accepted = ('admin', 'owner', 'staff') 227 | 228 | def has_read_permission(self, qs): 229 | return qs 230 | 231 | def has_change_permission(self, obj): 232 | return True 233 | 234 | def has_delete_permission(self, obj): 235 | return True 236 | 237 | def has_add_permission(self, obj): 238 | return True 239 | 240 | 241 | class CityResource(ModelResource): 242 | model = City 243 | schema = CitySchema 244 | 245 | auth_required = True 246 | roles_accepted = ('admin', 'owner', 'staff') 247 | 248 | def has_read_permission(self, qs): 249 | return qs 250 | 251 | def has_change_permission(self, obj): 252 | return True 253 | 254 | def has_delete_permission(self, obj): 255 | return True 256 | 257 | def has_add_permission(self, obj): 258 | return True 259 | 260 | 261 | class CustomerAddressResource(AssociationModelResource): 262 | 263 | model = CustomerAddress 264 | schema = CustomerAddressSchema 265 | 266 | auth_required = True 267 | roles_accepted = ('admin', 'owner', 'staff') 268 | 269 | def has_read_permission(self, qs): 270 | return qs 271 | 272 | def has_change_permission(self, obj, data): 273 | return True 274 | 275 | def has_delete_permission(self, obj, data): 276 | return True 277 | 278 | def has_add_permission(self, obj, data): 279 | return True 280 | 281 | 282 | class CustomerTransactionResource(ModelResource): 283 | 284 | model = CustomerTransaction 285 | schema = CustomerTransactionSchema 286 | 287 | auth_required = True 288 | roles_accepted = ('admin', 'owner', 'staff') 289 | 290 | def has_read_permission(self, qs): 291 | return qs 292 | 293 | def has_change_permission(self, obj): 294 | return True 295 | 296 | def has_delete_permission(self, obj): 297 | return True 298 | 299 | def has_add_permission(self, obj): 300 | return True 301 | 302 | 303 | class PermissionResource(ModelResource): 304 | 305 | model = Permission 306 | schema = PermissionSchema 307 | 308 | auth_required = True 309 | roles_accepted = ('admin', 'owner', 'staff') 310 | 311 | def has_read_permission(self, qs): 312 | if current_user.has_permission('view_permission'): 313 | return qs.filter(or_(self.model.is_hidden == False, self.model.is_hidden == None)) 314 | return False 315 | 316 | def has_change_permission(self, obj): 317 | if current_user.has_permission('change_user_shops') and current_user.has_shop_access(obj.retail_shop_id): 318 | return True 319 | return False 320 | 321 | def has_delete_permission(self, obj): 322 | if current_user.has_permission('delete_user_shops') and current_user.has_shop_access(obj.retail_shop_id): 323 | return True 324 | return False 325 | 326 | def has_add_permission(self, objects): 327 | return False 328 | 329 | 330 | class UserPermissionResource(AssociationModelResource): 331 | 332 | model = UserPermission 333 | schema = UserPermissionSchema 334 | 335 | auth_required = True 336 | roles_accepted = ('admin', 'owner') 337 | 338 | def has_read_permission(self, qs): 339 | return qs.filter(false()) 340 | 341 | def has_change_permission(self, obj, data): 342 | if current_user.has_permission('change_user_permissions') and \ 343 | current_user.retail_brand_id == User.query.with_entities(User.retail_brand_id)\ 344 | .filter(User.id == data['user_id']).scalar(): 345 | return True 346 | return False 347 | 348 | def has_delete_permission(self, obj, data): 349 | if current_user.has_permission('delete_user_permissions') and \ 350 | current_user.retail_brand_id == User.query.with_entities(User.retail_brand_id)\ 351 | .filter(User.id == data['user_id']).scalar(): 352 | return True 353 | return False 354 | 355 | def has_add_permission(self, obj, data): 356 | if current_user.has_permission('add_user_permission'): 357 | if current_user.retail_brand_id == User.query.with_entities(User.retail_brand_id)\ 358 | .filter(User.id == data['user_id']).scalar(): 359 | 360 | return True 361 | return False 362 | 363 | 364 | class RoleResource(ModelResource): 365 | model = Role 366 | schema = RoleSchema 367 | 368 | auth_required = True 369 | roles_accepted = ('admin', 'owner') 370 | 371 | optional = ('permissions',) 372 | 373 | def has_read_permission(self, qs): 374 | 375 | return qs.filter(or_(self.model.is_hidden == False, self.model.is_hidden == None)) 376 | 377 | def has_change_permission(self, obj): 378 | return False 379 | 380 | def has_delete_permission(self, obj): 381 | return False 382 | 383 | def has_add_permission(self, obj): 384 | return False 385 | 386 | 387 | class UserRoleResource(AssociationModelResource): 388 | 389 | model = UserRole 390 | schema = UserRoleSchema 391 | 392 | auth_required = True 393 | roles_accepted = ('admin', 'owner', 'staff') 394 | 395 | def has_read_permission(self, qs): 396 | return qs.filter(false()) 397 | 398 | def has_change_permission(self, obj, data): 399 | return current_user.retail_brand_id == User.query.with_entities(User.retail_brand_id) \ 400 | .filter(User.id == data['user_id']).scalar() and current_user.has_permission('change_user_role') 401 | 402 | def has_delete_permission(self, obj, data): 403 | return current_user.retail_brand_id == User.query.with_entities(User.retail_brand_id) \ 404 | .filter(User.id == data['user_id']).scalar() and current_user.has_permission('delete_user_role') 405 | 406 | def has_add_permission(self, obj, data): 407 | return current_user.retail_brand_id == User.query.with_entities(User.retail_brand_id) \ 408 | .filter(User.id == data['user_id']).scalar() and current_user.has_permission('add_user_role') 409 | 410 | 411 | class PrinterConfigResource(ModelResource): 412 | 413 | model = PrinterConfig 414 | schema = PrinterConfigSchema 415 | 416 | auth_required = True 417 | roles_accepted = ('admin', 'owner', 'staff') 418 | 419 | def has_read_permission(self, qs): 420 | if current_user.has_permission('view_product_config'): 421 | return qs.filter(self.model.retail_shop_id.in_(current_user.retail_shop_ids)) 422 | return qs.filter(false()) 423 | 424 | def has_change_permission(self, obj): 425 | return current_user.has_shop_access(obj.retail_shop_id) and current_user.has_permission('change_printer_config') 426 | 427 | def has_delete_permission(self, obj): 428 | return current_user.has_shop_access(obj.retail_shop_id) and current_user.has_permission('delete_printer_config') 429 | 430 | def has_add_permission(self, objects): 431 | if not current_user.has_permission('create_product_config'): 432 | return False 433 | for obj in objects: 434 | if not current_user.has_shop_access(obj.retail_shop_id): 435 | return False 436 | return True 437 | 438 | 439 | class RegistrationDetailResource(ModelResource): 440 | 441 | model = RegistrationDetail 442 | schema = RegistrationDetailSchema 443 | 444 | auth_required = True 445 | roles_accepted = ('admin', 'owner', 'staff') 446 | 447 | def has_read_permission(self, qs): 448 | if current_user.has_permission('view_registration_detail'): 449 | return qs.filter(self.model.retail_shop_id.in_(current_user.retail_shop_ids)) 450 | return qs.filter(false()) 451 | 452 | def has_change_permission(self, obj): 453 | return current_user.has_shop_access(obj.retail_shop_id) and \ 454 | current_user.has_permission('change_registration_detail') 455 | 456 | def has_delete_permission(self, obj): 457 | return current_user.has_shop_access(obj.retail_shop_id) and \ 458 | current_user.has_permission('delete_registration_detail') 459 | 460 | def has_add_permission(self, objects): 461 | if not current_user.has_permission('create_registration_detail'): 462 | return False 463 | for obj in objects: 464 | if not current_user.has_shop_access(obj.retail_shop_id): 465 | return False 466 | return True 467 | -------------------------------------------------------------------------------- /src/products/models.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from sqlalchemy.dialects.postgresql import UUID 3 | from sqlalchemy import desc, UniqueConstraint 4 | from sqlalchemy.ext.hybrid import hybrid_property 5 | from sqlalchemy import and_, func, select, or_ 6 | 7 | from src import db, BaseMixin, ReprMixin 8 | from src.orders.models import Item 9 | from src.user.models import RetailShop 10 | 11 | 12 | class Brand(BaseMixin, db.Model, ReprMixin): 13 | 14 | name = db.Column(db.String(55), nullable=False, index=True) 15 | retail_shop_id = db.Column(UUID, db.ForeignKey('retail_shop.id', ondelete='CASCADE'), index=True) 16 | 17 | retail_shop = db.relationship('RetailShop', foreign_keys=[retail_shop_id], uselist=False, backref='brands') 18 | products = db.relationship('Product', uselist=True, back_populates='brand') 19 | distributors = db.relationship('Distributor', back_populates='brands', secondary='brand_distributor', 20 | lazy='dynamic') 21 | 22 | 23 | class ProductTax(BaseMixin, db.Model, ReprMixin): 24 | 25 | tax_id = db.Column(UUID, db.ForeignKey('tax.id'), index=True) 26 | product_id = db.Column(UUID, db.ForeignKey('product.id'), index=True) 27 | 28 | tax = db.relationship('Tax', foreign_keys=[tax_id]) 29 | product = db.relationship('Product', foreign_keys=[product_id]) 30 | 31 | UniqueConstraint(tax_id, product_id) 32 | 33 | @hybrid_property 34 | def retail_shop_id(self): 35 | return self.product.retail_shop_id 36 | 37 | @retail_shop_id.expression 38 | def retail_shop_id(self): 39 | return select([Product.retail_shop_id]).where(Product.id == self.product_id).as_scalar() 40 | 41 | 42 | class Tax(BaseMixin, db.Model, ReprMixin): 43 | 44 | name = db.Column(db.String(25), nullable=False, index=True) 45 | value = db.Column(db.Float(precision=2), nullable=False) 46 | is_disabled = db.Column(db.Boolean(), default=False) 47 | 48 | retail_shop_id = db.Column(UUID, db.ForeignKey('retail_shop.id', ondelete='CASCADE'), index=True, nullable=False) 49 | 50 | retail_shop = db.relationship('RetailShop', foreign_keys=[retail_shop_id], uselist=False, backref='taxes') 51 | products = db.relationship('Product', back_populates='taxes', secondary='product_tax', lazy='dynamic') 52 | 53 | UniqueConstraint(name, retail_shop_id) 54 | 55 | 56 | class Distributor(BaseMixin, db.Model, ReprMixin): 57 | 58 | name = db.Column(db.String(127), nullable=False, index=True) 59 | phone_numbers = db.Column(db.JSON) 60 | emails = db.Column(db.JSON) 61 | 62 | retail_shop_id = db.Column(UUID, db.ForeignKey('retail_shop.id', ondelete='CASCADE'), index=True, nullable=False) 63 | 64 | bills = db.relationship('DistributorBill', uselist=True, back_populates='distributor', lazy='dynamic') 65 | 66 | retail_shop = db.relationship('RetailShop', foreign_keys=[retail_shop_id], uselist=False, backref='distributors') 67 | brands = db.relationship('Brand', back_populates='distributors', secondary='brand_distributor') 68 | 69 | UniqueConstraint(name, retail_shop_id) 70 | 71 | @hybrid_property 72 | def retail_shop_name(self): 73 | return self.retail_shop.name 74 | 75 | @retail_shop_name.expression 76 | def retail_shop_name(self): 77 | return select([RetailShop.name]).where(RetailShop.id == self.retail_shop_id).as_scalar() 78 | 79 | @hybrid_property 80 | def products(self): 81 | return Product.query.filter(Product.is_disabled == False, Product.brand_id.in_ 82 | ([i[0] for i in BrandDistributor.query.with_entities(BrandDistributor.brand_id) 83 | .filter(BrandDistributor.distributor_id == self.id).all()])).all() 84 | 85 | 86 | class DistributorBill(BaseMixin, db.Model, ReprMixin): 87 | 88 | __repr_fields__ = ['id', 'distributor_id'] 89 | 90 | purchase_date = db.Column(db.Date, nullable=False) 91 | reference_number = db.Column(db.String(55), nullable=True) 92 | distributor_id = db.Column(UUID, db.ForeignKey('distributor.id'), nullable=False, index=True) 93 | 94 | distributor = db.relationship('Distributor', single_parent=True, back_populates='bills') 95 | purchased_items = db.relationship('Stock', uselist=True, back_populates='distributor_bill', lazy='dynamic') 96 | 97 | @hybrid_property 98 | def bill_amount(self): 99 | return self.purchased_items.with_entities(func.Sum(Stock.purchase_amount)).scalar() 100 | 101 | @hybrid_property 102 | def total_items(self): 103 | return self.purchased_items.with_entities(func.Count(Stock.id)).scalar() 104 | 105 | @hybrid_property 106 | def retail_shop_id(self): 107 | return self.distributor.retail_shop_id 108 | 109 | @retail_shop_id.expression 110 | def retail_shop_id(self): 111 | return select([Distributor.retail_shop_id]).where(Distributor.id == self.distributor_id).as_scalar() 112 | 113 | @hybrid_property 114 | def retail_shop_name(self): 115 | return self.distributor.retail_shop.name 116 | 117 | @retail_shop_name.expression 118 | def retail_shop_name(self): 119 | return select([Distributor.retail_shop_name]).where(Distributor.id == self.distributor_id).as_scalar() 120 | 121 | @hybrid_property 122 | def distributor_name(self): 123 | return self.distributor.name 124 | 125 | @distributor_name.expression 126 | def distributor_name(self): 127 | return select([Distributor.name]).where(Distributor.id == self.distributor_id).as_scalar() 128 | 129 | 130 | class ProductType(BaseMixin, db.Model, ReprMixin): 131 | 132 | name = db.Column(db.String(80), unique=True, index=True) 133 | description = db.Column(db.TEXT()) 134 | retail_shop_id = db.Column(UUID, db.ForeignKey('retail_shop.id', ondelete='CASCADE'), index=True) 135 | 136 | 137 | class Tag(BaseMixin, db.Model, ReprMixin): 138 | name = db.Column(db.String(55), unique=False, nullable=False, index=True) 139 | retail_shop_id = db.Column(UUID, db.ForeignKey('retail_shop.id', ondelete='CASCADE'), index=True, nullable=False) 140 | 141 | products = db.relationship('Product', back_populates='tags', secondary='product_tag') 142 | retail_shop = db.relationship('RetailShop', foreign_keys=[retail_shop_id], uselist=False, backref='tags') 143 | 144 | UniqueConstraint(name, retail_shop_id) 145 | 146 | 147 | class ProductTag(BaseMixin, db.Model, ReprMixin): 148 | 149 | __repr_fields__ = ['tag_id', 'product_id'] 150 | 151 | tag_id = db.Column(UUID, db.ForeignKey('tag.id'), index=True) 152 | product_id = db.Column(UUID, db.ForeignKey('product.id'), index=True) 153 | 154 | tag = db.relationship('Tag', foreign_keys=[tag_id]) 155 | product = db.relationship('Product', foreign_keys=[product_id]) 156 | 157 | UniqueConstraint(tag_id, product_id) 158 | 159 | @hybrid_property 160 | def retail_shop_id(self): 161 | return self.product.retail_shop_id 162 | 163 | @retail_shop_id.expression 164 | def retail_shop_id(self): 165 | return select([Product.retail_shop_id]).where(Product.id == self.product_id).as_scalar() 166 | 167 | 168 | class BrandDistributor(BaseMixin, db.Model, ReprMixin): 169 | 170 | __repr_fields__ = ['brand_id', 'distributor_id'] 171 | 172 | brand_id = db.Column(UUID, db.ForeignKey('brand.id'), index=True, nullable=False) 173 | distributor_id = db.Column(UUID, db.ForeignKey('distributor.id'), index=True, nullable=False) 174 | 175 | brand = db.relationship('Brand', foreign_keys=[brand_id]) 176 | distributor = db.relationship('Distributor', foreign_keys=[distributor_id]) 177 | 178 | UniqueConstraint(brand_id, distributor_id) 179 | 180 | @hybrid_property 181 | def retail_shop_id(self): 182 | return self.brand.retail_shop_id 183 | 184 | @retail_shop_id.expression 185 | def retail_shop_id(self): 186 | return select([Brand.retail_shop_id]).where(Brand.id == self.brand_id).as_scalar() 187 | 188 | 189 | class ProductDistributor(BaseMixin, db.Model, ReprMixin): 190 | 191 | __repr_fields__ = ['distributor_id', 'product_id'] 192 | 193 | distributor_id = db.Column(UUID, db.ForeignKey('distributor.id'), index=True) 194 | product_id = db.Column(UUID, db.ForeignKey('product.id'), index=True) 195 | 196 | distributor = db.relationship('Distributor', foreign_keys=[distributor_id]) 197 | product = db.relationship('Product', foreign_keys=[product_id]) 198 | 199 | UniqueConstraint(distributor_id, product_id) 200 | 201 | @hybrid_property 202 | def retail_shop_id(self): 203 | return self.product.retail_shop_id 204 | 205 | @retail_shop_id.expression 206 | def retail_shop_id(self): 207 | return select([Product.retail_shop_id]).where(Product.id == self.product_id).as_scalar() 208 | 209 | 210 | class Product(BaseMixin, db.Model, ReprMixin): 211 | 212 | name = db.Column(db.String(127), unique=False, nullable=False, index=True) 213 | min_stock = db.Column(db.SmallInteger, nullable=False) 214 | auto_discount = db.Column(db.FLOAT(precision=2), default=0, nullable=False) 215 | description = db.Column(db.JSON(), nullable=True) 216 | sub_description = db.Column(db.Text(), nullable=True) 217 | is_disabled = db.Column(db.Boolean(), default=False) 218 | default_quantity = db.Column(db.Float(precision=2), default=1) 219 | quantity_label = db.Column(db.Enum('KG', 'GM', 'MG', 'L', 'ML', 'TAB', 'SYRUP', 'OTH', 'TAB', 'ML', 'CAP', 'INJ', 220 | 'BOTTLE', 'VAIL', 'KIT', 'STRIP', 'OTHER', 'PACK', 'SET', 'LTR', 'SACHET', 221 | 'PILLS', 'SYRINGE', 'SYRUP', 'ROLL', name='varchar'), 222 | default='OTH', nullable=True) 223 | is_loose = db.Column(db.Boolean(), default=False) 224 | barcode = db.Column(db.String(13), nullable=True) 225 | 226 | retail_shop_id = db.Column(UUID, db.ForeignKey('retail_shop.id', ondelete='CASCADE'), index=True, nullable=False) 227 | brand_id = db.Column(UUID, db.ForeignKey('brand.id'), index=True, nullable=False) 228 | 229 | retail_shop = db.relationship('RetailShop', foreign_keys=[retail_shop_id], uselist=False, back_populates='products') 230 | taxes = db.relationship('Tax', back_populates='products', secondary='product_tax') 231 | tags = db.relationship('Tag', back_populates='products', secondary='product_tag') 232 | brand = db.relationship('Brand', foreign_keys=[brand_id], uselist=False, back_populates='products') 233 | 234 | stocks = db.relationship('Stock', uselist=True, cascade="all, delete-orphan", lazy='dynamic') 235 | # distributors = db.relationship('Distributor', back_populates='products', secondary='product_distributor') 236 | combos = db.relationship('Combo', back_populates='products', secondary='combo_product') 237 | salts = db.relationship('Salt', back_populates='products', secondary='product_salt') 238 | add_ons = db.relationship('AddOn', back_populates='products', secondary='product_add_on') 239 | 240 | UniqueConstraint('barcode', 'retail_shop_id', 'bar_retail_un') 241 | 242 | @hybrid_property 243 | def available_stock(self): 244 | return self.stocks.filter(Stock.is_sold != True, Stock.expired == False)\ 245 | .with_entities(func.coalesce(func.Sum(Stock.units_purchased), 0)-func.coalesce(func.Sum(Stock.units_sold), 246 | 0)).scalar() 247 | 248 | @hybrid_property 249 | def available_stocks(self): 250 | return self.stocks.filter(and_(or_(Stock.is_sold != True), Stock.expired == False)).all() 251 | 252 | @available_stock.expression 253 | def available_stock(cls): 254 | return select([func.coalesce(func.Sum(Stock.units_purchased), 0)-func.coalesce(func.Sum(Stock.units_sold), 0)])\ 255 | .where(and_(or_(Stock.is_sold != True), Stock.product_id == cls.id)).as_scalar() 256 | 257 | @hybrid_property 258 | def mrp(self): 259 | mrp = self.stocks.filter(or_(Stock.is_sold != True))\ 260 | .with_entities(Stock.selling_amount).order_by(Stock.id).first() 261 | return mrp[0] if mrp else 0 262 | 263 | @hybrid_property 264 | def similar_products(self): 265 | if len(self.salts): 266 | return [i[0] for i in Product.query.with_entities(Product.id) 267 | .join(ProductSalt, and_(ProductSalt.product_id == Product.id)) 268 | .filter(ProductSalt.salt_id.in_([i.id for i in self.salts])).group_by(Product.id) 269 | .having(func.Count(func.Distinct(ProductSalt.salt_id)) == len(self.salts)).all()] 270 | return [] 271 | 272 | @hybrid_property 273 | def last_purchase_amount(self): 274 | return self.stocks.order_by(desc(Stock.purchase_date)).first().purchase_amount 275 | 276 | @hybrid_property 277 | def last_selling_amount(self): 278 | return self.stocks.order_by(desc(Stock.purchase_date)).first().selling_amount 279 | 280 | @hybrid_property 281 | def stock_required(self): 282 | return abs(self.min_stock - self.available_stock) 283 | 284 | @stock_required.expression 285 | def stock_required(self): 286 | return self.min_stock - self.available_stock 287 | 288 | @hybrid_property 289 | def is_short(self): 290 | return self.min_stock >= self.available_stock 291 | 292 | @hybrid_property 293 | def product_name(self): 294 | return self.name 295 | 296 | @hybrid_property 297 | def distributors(self): 298 | return self.brand.distributors.all() 299 | 300 | @hybrid_property 301 | def brand_name(self): 302 | return self.brand.name 303 | 304 | @brand_name.expression 305 | def brand_name(self): 306 | return select([Brand.name]).where(Brand.id == self.brand_id).as_scalar() 307 | 308 | 309 | class Salt(BaseMixin, db.Model, ReprMixin): 310 | 311 | name = db.Column(db.String(127), unique=True, nullable=False, index=True) 312 | retail_shop_id = db.Column(UUID, db.ForeignKey('retail_shop.id', ondelete='CASCADE'), index=True, nullable=False) 313 | 314 | products = db.relationship('Product', back_populates='salts', secondary='product_salt') 315 | retail_shop = db.relationship('RetailShop', foreign_keys=[retail_shop_id], uselist=False) 316 | 317 | UniqueConstraint(name, retail_shop_id) 318 | 319 | 320 | class ProductSalt(BaseMixin, db.Model, ReprMixin): 321 | 322 | __repr_fields__ = ['salt_id', 'product_id'] 323 | 324 | salt_id = db.Column(UUID, db.ForeignKey('salt.id'), index=True, nullable=False) 325 | product_id = db.Column(UUID, db.ForeignKey('product.id'), index=True, nullable=False) 326 | 327 | salt = db.relationship('Salt', foreign_keys=[salt_id]) 328 | product = db.relationship('Product', foreign_keys=[product_id]) 329 | 330 | UniqueConstraint(salt_id, product_id) 331 | 332 | @hybrid_property 333 | def retail_shop_id(self): 334 | return self.product.retail_shop_id 335 | 336 | @retail_shop_id.expression 337 | def retail_shop_id(self): 338 | return select([Product.retail_shop_id]).where(Product.id == self.product_id).as_scalar() 339 | 340 | 341 | class Stock(BaseMixin, db.Model, ReprMixin): 342 | 343 | __repr_fields__ = ['id', 'purchase_date'] 344 | 345 | purchase_amount = db.Column(db.Float(precision=2), nullable=False, default=0) 346 | selling_amount = db.Column(db.Float(precision=2), nullable=False, default=0) 347 | units_purchased = db.Column(db.SmallInteger, nullable=False, default=1) 348 | batch_number = db.Column(db.String(25), nullable=True) 349 | expiry_date = db.Column(db.Date, nullable=True) 350 | is_sold = db.Column(db.Boolean(), default=False, index=True) 351 | default_stock = db.Column(db.Boolean, default=False, nullable=True) 352 | 353 | distributor_bill_id = db.Column(UUID, db.ForeignKey('distributor_bill.id'), nullable=True, index=True) 354 | product_id = db.Column(UUID, db.ForeignKey('product.id'), nullable=False, index=True) 355 | 356 | distributor_bill = db.relationship('DistributorBill', single_parent=True, back_populates='purchased_items') 357 | product = db.relationship('Product', single_parent=True, foreign_keys=product_id) 358 | order_items = db.relationship('Item', uselist=True, back_populates='stock', lazy='dynamic') 359 | 360 | @hybrid_property 361 | def units_sold(self): 362 | 363 | total_sold = self.order_items.with_entities(func.Sum(Item.quantity))\ 364 | .filter(Item.stock_id == self.id).scalar() 365 | if total_sold: 366 | if total_sold >= self.units_purchased and not self.is_sold: 367 | self.is_sold = True 368 | db.session.commit() 369 | return total_sold 370 | else: 371 | return 0 372 | 373 | @units_sold.expression 374 | def units_sold(cls): 375 | return select([func.coalesce(func.Sum(Item.quantity), 0)]).where(Item.stock_id == cls.id).as_scalar() 376 | 377 | @hybrid_property 378 | def product_name(self): 379 | return self.product.name 380 | 381 | @product_name.expression 382 | def product_name(self): 383 | return select([Product.name]).where(Product.id == self.product_id).as_scalar() 384 | 385 | @hybrid_property 386 | def retail_shop_id(self): 387 | return self.product.retail_shop_id 388 | 389 | @retail_shop_id.expression 390 | def retail_shop_id(self): 391 | return select([Product.retail_shop_id]).where(Product.id == self.product_id).as_scalar() 392 | 393 | @hybrid_property 394 | def expired(self): 395 | return self.expiry_date is not None and self.expiry_date < datetime.now().date() 396 | 397 | @expired.expression 398 | def expired(self): 399 | return and_(or_(self.is_sold != True), func.coalesce(self.expiry_date, datetime.now().date()) 400 | < datetime.now().date()).label('expired') 401 | 402 | @hybrid_property 403 | def distributor_id(self): 404 | return self.distributor_bill.distributor_id 405 | 406 | @distributor_id.expression 407 | def distributor_id(self): 408 | return select([DistributorBill.distributor_id]).where(DistributorBill.id == self.distributor_bill_id).as_scalar() 409 | 410 | @hybrid_property 411 | def distributor_name(self): 412 | return self.distributor_bill.distributor.name 413 | 414 | @distributor_name.expression 415 | def distributor_name(self): 416 | return select([Distributor.name]).where(and_(DistributorBill.id == self.distributor_bill_id, 417 | Distributor.id == DistributorBill.distributor_id)).as_scalar() 418 | 419 | @hybrid_property 420 | def purchase_date(self): 421 | if self.distributor_bill_id: 422 | return self.distributor_bill.purchase_date 423 | return None 424 | 425 | @purchase_date.expression 426 | def purchase_date(cls): 427 | return select([DistributorBill.purchase_date]).where(DistributorBill.id == cls.distributor_bill_id).as_scalar() 428 | 429 | @hybrid_property 430 | def quantity_label(self): 431 | return self.product.quantity_label 432 | 433 | @quantity_label.expression 434 | def quantity_label(cls): 435 | return select([Product.quantity_label]).where(Product.id == cls.product_id).as_scalar() 436 | 437 | @hybrid_property 438 | def brand_name(self): 439 | return self.product.brand.name 440 | 441 | @brand_name.expression 442 | def brand_name(self): 443 | return select([Brand.name]).where(and_(Product.id == self.product_id, 444 | Brand.id == Product.brand_id)).as_scalar() 445 | 446 | 447 | class Combo(BaseMixin, db.Model, ReprMixin): 448 | 449 | name = db.Column(db.String(55), nullable=False, index=True) 450 | products = db.relationship('Product', back_populates='combos', secondary='combo_product') 451 | 452 | 453 | class ComboProduct(BaseMixin, db.Model, ReprMixin): 454 | 455 | __repr_fields__ = ['combo_id', 'product_id'] 456 | 457 | combo_id = db.Column(UUID, db.ForeignKey('combo.id'), index=True) 458 | product_id = db.Column(UUID, db.ForeignKey('product.id'), index=True) 459 | 460 | combo = db.relationship('Combo', foreign_keys=[combo_id]) 461 | product = db.relationship('Product', foreign_keys=[product_id]) 462 | 463 | 464 | class AddOn(BaseMixin, db.Model, ReprMixin): 465 | 466 | name = db.Column(db.String(127), unique=True, nullable=False, index=True) 467 | retail_shop_id = db.Column(UUID, db.ForeignKey('retail_shop.id', ondelete='CASCADE'), index=True) 468 | 469 | products = db.relationship('Product', back_populates='add_ons', secondary='product_add_on') 470 | 471 | 472 | class ProductAddOn(BaseMixin, db.Model, ReprMixin): 473 | 474 | __repr_fields__ = ['add_on_id', 'product_id'] 475 | 476 | add_on_id = db.Column(UUID, db.ForeignKey('add_on.id'), index=True) 477 | product_id = db.Column(UUID, db.ForeignKey('product.id'), index=True) 478 | 479 | add_on = db.relationship('AddOn', foreign_keys=[add_on_id]) 480 | product = db.relationship('Product', foreign_keys=[product_id]) 481 | -------------------------------------------------------------------------------- /src/products/resources.py: -------------------------------------------------------------------------------- 1 | from flask_security import current_user 2 | from sqlalchemy.sql import false 3 | 4 | from src.utils import ModelResource, AssociationModelResource, operators as ops 5 | from .models import Product, Tax, Stock, Brand, \ 6 | DistributorBill, Distributor, ProductTax, Tag, Combo, AddOn, Salt, ProductDistributor, ProductSalt, \ 7 | ProductTag, BrandDistributor 8 | from .schemas import ProductSchema, TaxSchema, StockSchema, BrandSchema, \ 9 | DistributorBillSchema, DistributorSchema, ProductTaxSchema, TagSchema, ComboSchema, SaltSchema, AddOnSchema, \ 10 | ProductDistributorSchema, ProductSaltSchema, ProductTagSchema, BrandDistributorSchema 11 | 12 | 13 | class ProductResource(ModelResource): 14 | model = Product 15 | schema = ProductSchema 16 | 17 | auth_required = True 18 | 19 | default_limit = 100 20 | 21 | max_limit = 500 22 | 23 | optional = ('distributors', 'brand', 'retail_shop', 'stocks', 'similar_products', 'available_stocks', 24 | 'last_purchase_amount', 'last_selling_amount', 'stock_required') 25 | 26 | filters = { 27 | 'name': [ops.Equal, ops.Contains], 28 | 'product_name': [ops.Equal, ops.Contains], 29 | 'brand_name': [ops.Equal, ops.Contains], 30 | 'stock_required': [ops.Equal, ops.Greater, ops.Greaterequal], 31 | 'available_stock': [ops.Equal, ops.Greater, ops.Greaterequal], 32 | 'id': [ops.Equal, ops.In, ops.NotEqual, ops.NotIn], 33 | 'retail_shop_id': [ops.Equal, ops.In], 34 | 'is_short': [ops.Boolean], 35 | 'is_disabled': [ops.Boolean], 36 | 'created_on': [ops.DateLesserEqual, ops.DateEqual, ops.DateGreaterEqual], 37 | 'updated_on': [ops.Greaterequal, ops.DateGreaterEqual, ops.DateEqual, ops.DateLesserEqual] 38 | } 39 | order_by = ['retail_shop_id', 'id', 'name'] 40 | 41 | def has_read_permission(self, qs): 42 | if current_user.has_permission('view_product'): 43 | return qs.filter(self.model.retail_shop_id.in_(current_user.retail_shop_ids)) 44 | return qs.filter(false()) 45 | 46 | def has_change_permission(self, obj): 47 | return current_user.has_shop_access(obj.retail_shop_id) and current_user.has_permission('change_product') 48 | 49 | def has_delete_permission(self, obj): 50 | return current_user.has_shop_access(obj.retail_shop_id) and current_user.has_permission('remove_product') 51 | 52 | def has_add_permission(self, objects): 53 | if not current_user.has_permission('create_product'): 54 | return False 55 | for obj in objects: 56 | if not current_user.has_shop_access(obj.retail_shop_id): 57 | return False 58 | return True 59 | 60 | 61 | class TagResource(ModelResource): 62 | model = Tag 63 | schema = TagSchema 64 | 65 | auth_required = True 66 | 67 | optional = ('products', 'retail_shop') 68 | 69 | order_by = ['retail_shop_id', 'id', 'name'] 70 | 71 | filters = { 72 | 'name': [ops.Equal, ops.Contains], 73 | 'retail_shop_id': [ops.Equal, ops.In] 74 | } 75 | 76 | def has_read_permission(self, qs): 77 | if current_user.has_permission('view_tag'): 78 | return qs.filter(self.model.retail_shop_id.in_(current_user.retail_shop_ids)) 79 | return qs.filter(false()) 80 | 81 | def has_change_permission(self, obj): 82 | return current_user.has_shop_access(obj.retail_shop_id) and current_user.has_permission('change_tag') 83 | 84 | def has_delete_permission(self, obj): 85 | return current_user.has_shop_access(obj.retail_shop_id) and current_user.has_permission('remove_tag') 86 | 87 | def has_add_permission(self, objects): 88 | if not current_user.has_permission('create_tag'): 89 | return False 90 | for obj in objects: 91 | if not current_user.has_shop_access(obj.retail_shop_id): 92 | return False 93 | return True 94 | 95 | 96 | class StockResource(ModelResource): 97 | model = Stock 98 | schema = StockSchema 99 | 100 | auth_required = True 101 | 102 | roles_accepted = ('admin', 'owner', 'staff') 103 | 104 | export = True 105 | 106 | max_export_limit = 500 107 | 108 | optional = ('product', 'retail_shop', 'distributor_bill', 'product_name', 'retail_shop_id', 'distributor_name') 109 | 110 | filters = { 111 | 'is_sold': [ops.Boolean], 112 | 'expired': [ops.Boolean], 113 | 'units_available': [ops.Equal, ops.Greater, ops.Greaterequal], 114 | 'units_sold': [ops.Equal, ops.Lesser, ops.LesserEqual], 115 | 'brand_name': [ops.Equal, ops.Contains], 116 | 'product_name': [ops.Contains, ops.Equal], 117 | 'retail_shop_id': [ops.Equal, ops.In], 118 | 'id': [ops.Equal, ops.In, ops.NotEqual, ops.NotIn], 119 | 'distributor_id': [ops.Equal, ops.In], 120 | 'distributor_name': [ops.Contains, ops.Equal], 121 | 'updated_on': [ops.DateGreaterEqual, ops.DateEqual, ops.DateLesserEqual], 122 | 'created_on': [ops.DateLesserEqual, ops.DateEqual, ops.DateGreaterEqual, ops.DateBetween], 123 | 'expiry_date': [ops.DateLesserEqual, ops.DateEqual, ops.DateGreaterEqual, ops.DateBetween] 124 | } 125 | 126 | order_by = ['expiry_date', 'units_sold', 'created_on'] 127 | 128 | only = () 129 | 130 | exclude = () 131 | 132 | def has_read_permission(self, qs): 133 | if current_user.has_permission('view_stock'): 134 | return qs.filter(self.model.retail_shop_id.in_(current_user.retail_shop_ids)) 135 | return qs.filter(false()) 136 | 137 | def has_change_permission(self, obj): 138 | return current_user.has_shop_access(obj.retail_shop_id) and current_user.has_permission('change_stock') 139 | 140 | def has_delete_permission(self, obj): 141 | return current_user.has_shop_access(obj.retail_shop_id) and current_user.has_permission('remove_stock') 142 | 143 | def has_add_permission(self, objects): 144 | if not current_user.has_permission('create_stock'): 145 | return False 146 | for obj in objects: 147 | if not current_user.has_shop_access(Product.query.with_entities(Product.retail_shop_id) 148 | .filter(Product.id == obj.product_id).scalar()): 149 | return False 150 | return True 151 | 152 | 153 | class DistributorResource(ModelResource): 154 | model = Distributor 155 | schema = DistributorSchema 156 | 157 | auth_required = True 158 | 159 | order_by = ['retail_shop_id', 'id', 'name'] 160 | 161 | optional = ('products', 'retail_shop', 'bills') 162 | 163 | filters = { 164 | 'id': [ops.Equal, ops.In], 165 | 'name': [ops.Equal, ops.Contains], 166 | 'retail_shop_id': [ops.Equal, ops.In] 167 | } 168 | 169 | def has_read_permission(self, qs): 170 | if current_user.has_permission('view_distributor'): 171 | return qs.filter(self.model.retail_shop_id.in_(current_user.retail_shop_ids)) 172 | return qs.filter(false()) 173 | 174 | def has_change_permission(self, obj): 175 | return current_user.has_shop_access(obj.retail_shop_id) and current_user.has_permission('change_distributor') 176 | 177 | def has_delete_permission(self, obj): 178 | return current_user.has_shop_access(obj.retail_shop_id) and current_user.has_permission('remove_distributor') 179 | 180 | def has_add_permission(self, objects): 181 | if not current_user.has_permission('create_distributor'): 182 | return False 183 | for obj in objects: 184 | if not current_user.has_shop_access(obj.retail_shop_id): 185 | return False 186 | return True 187 | 188 | 189 | class DistributorBillResource(ModelResource): 190 | model = DistributorBill 191 | schema = DistributorBillSchema 192 | 193 | auth_required = True 194 | 195 | roles_required = ('admin',) 196 | 197 | optional = ('purchased_items',) 198 | 199 | max_limit = 50 200 | 201 | filters = { 202 | 'created_on': [ops.DateLesserEqual, ops.DateEqual, ops.DateGreaterEqual, ops.DateBetween], 203 | 'purchase_date': [ops.DateLesserEqual, ops.DateEqual, ops.DateGreaterEqual, ops.DateBetween], 204 | } 205 | 206 | default_limit = 10 207 | 208 | def has_read_permission(self, qs): 209 | if current_user.has_permission('view_distributor_bill'): 210 | return qs.filter(self.model.retail_shop_id.in_(current_user.retail_shop_ids)) 211 | return qs.filter(false()) 212 | 213 | def has_change_permission(self, obj): 214 | return current_user.has_shop_access(obj.retail_shop_id) and \ 215 | current_user.has_permission('change_distributor_bill') 216 | 217 | def has_delete_permission(self, obj): 218 | return current_user.has_shop_access(obj.retail_shop_id) and \ 219 | current_user.has_permission('remove_distributor_bill') 220 | 221 | def has_add_permission(self, objects): 222 | if not current_user.has_permission('create_distributor_bill'): 223 | return False 224 | for obj in objects: 225 | if not current_user.has_shop_access(Distributor.query.with_entities(Distributor.retail_shop_id) 226 | .filter(Distributor.id == obj.distributor_id).scalar()): 227 | return False 228 | return True 229 | 230 | 231 | class BrandResource(ModelResource): 232 | model = Brand 233 | schema = BrandSchema 234 | 235 | auth_required = True 236 | 237 | order_by = ['retail_shop_id', 'id', 'name'] 238 | 239 | optional = ('products', 'retail_shop', 'distributors') 240 | 241 | filters = { 242 | 'name': [ops.Equal, ops.Contains], 243 | 'retail_shop_id': [ops.Equal, ops.In] 244 | } 245 | 246 | def has_read_permission(self, qs): 247 | if current_user.has_permission('view_brand'): 248 | return qs.filter(self.model.retail_shop_id.in_(current_user.retail_shop_ids)) 249 | return qs.filter(false()) 250 | 251 | def has_change_permission(self, obj): 252 | return current_user.has_shop_access(obj.retail_shop_id) and current_user.has_permission('change_brand') 253 | 254 | def has_delete_permission(self, obj): 255 | return current_user.has_shop_access(obj.retail_shop_id) and current_user.has_permission('remove_brand') 256 | 257 | def has_add_permission(self, objects): 258 | if not current_user.has_permission('create_brand'): 259 | return False 260 | for obj in objects: 261 | if not current_user.has_shop_access(obj.retail_shop_id): 262 | return False 263 | return True 264 | 265 | 266 | class TaxResource(ModelResource): 267 | model = Tax 268 | schema = TaxSchema 269 | 270 | auth_required = True 271 | optional = ('products', 'retail_shop') 272 | 273 | order_by = ['retail_shop_id', 'id', 'name'] 274 | 275 | filters = { 276 | 'name': [ops.Equal, ops.Contains], 277 | 'retail_shop_id': [ops.Equal, ops.In] 278 | } 279 | 280 | only = () 281 | 282 | exclude = () 283 | 284 | def has_read_permission(self, qs): 285 | if current_user.has_permission('view_tax'): 286 | return qs.filter(self.model.retail_shop_id.in_(current_user.retail_shop_ids)) 287 | return qs.filter(false()) 288 | 289 | def has_change_permission(self, obj): 290 | return current_user.has_shop_access(obj.retail_shop_id) and current_user.has_permission('change_tax') 291 | 292 | def has_delete_permission(self, obj): 293 | return current_user.has_shop_access(obj.retail_shop_id) and current_user.has_permission('remove_tax') 294 | 295 | def has_add_permission(self, objects): 296 | if not current_user.has_permission('create_tax'): 297 | return False 298 | for obj in objects: 299 | if not current_user.has_shop_access(obj.retail_shop_id): 300 | return False 301 | return True 302 | 303 | 304 | class SaltResource(ModelResource): 305 | model = Salt 306 | schema = SaltSchema 307 | 308 | auth_required = True 309 | 310 | optional = ('products', 'retail_shop') 311 | 312 | order_by = ['retail_shop_id', 'id', 'name'] 313 | 314 | filters = { 315 | 'name': [ops.Equal, ops.Contains], 316 | 'retail_shop_id': [ops.Equal, ops.In] 317 | } 318 | 319 | def has_read_permission(self, qs): 320 | if current_user.has_permission('view_salt'): 321 | return qs.filter(self.model.retail_shop_id.in_(current_user.retail_shop_ids)) 322 | return qs.filter(false()) 323 | 324 | def has_change_permission(self, obj): 325 | return current_user.has_shop_access(obj.retail_shop_id) and current_user.has_permission('change_salt') 326 | 327 | def has_delete_permission(self, obj): 328 | return current_user.has_shop_access(obj.retail_shop_id) and current_user.has_permission('remove_salt') 329 | 330 | def has_add_permission(self, objects): 331 | if not current_user.has_permission('create_salt'): 332 | return False 333 | for obj in objects: 334 | if not current_user.has_shop_access(obj.retail_shop_id): 335 | return False 336 | return True 337 | 338 | 339 | class AddOnResource(ModelResource): 340 | model = AddOn 341 | schema = AddOnSchema 342 | 343 | auth_required = True 344 | 345 | optional = ('products', 'retail_shop') 346 | 347 | filters = { 348 | 'name': [ops.Equal, ops.Contains], 349 | 'retail_shop_id': [ops.Equal, ops.In] 350 | } 351 | 352 | def has_read_permission(self, qs): 353 | if current_user.has_permission('view_add_on'): 354 | return qs.filter(self.model.retail_shop_id.in_(current_user.retail_shop_ids)) 355 | return qs.filter(false()) 356 | 357 | def has_change_permission(self, obj): 358 | return current_user.has_shop_access(obj.retail_shop_id) and current_user.has_permission('change_add_on') 359 | 360 | def has_delete_permission(self, obj): 361 | return current_user.has_shop_access(obj.retail_shop_id) and current_user.has_permission('remove_add_on') 362 | 363 | def has_add_permission(self, objects): 364 | if not current_user.has_permission('create_add_on'): 365 | return False 366 | for obj in objects: 367 | if not current_user.has_shop_access(obj.retail_shop_id): 368 | return False 369 | return True 370 | 371 | 372 | class ComboResource(ModelResource): 373 | model = Combo 374 | schema = ComboSchema 375 | 376 | auth_required = True 377 | 378 | optional = ('products', 'retail_shop') 379 | 380 | filters = { 381 | 'name': [ops.Equal, ops.Contains], 382 | 'retail_shop_id': [ops.Equal, ops.In] 383 | } 384 | 385 | def has_read_permission(self, qs): 386 | if current_user.has_permission('view_combo'): 387 | return qs.filter(self.model.retail_shop_id.in_(current_user.retail_shop_ids)) 388 | return qs.filter(false()) 389 | 390 | def has_change_permission(self, obj): 391 | return current_user.has_shop_access(obj.retail_shop_id) and current_user.has_permission('change_combo') 392 | 393 | def has_delete_permission(self, obj): 394 | return current_user.has_shop_access(obj.retail_shop_id) and current_user.has_permission('remove_combo') 395 | 396 | def has_add_permission(self, objects): 397 | if not current_user.has_permission('create_combo'): 398 | return False 399 | for obj in objects: 400 | if not current_user.has_shop_access(obj.retail_shop_id): 401 | return False 402 | return True 403 | 404 | 405 | class ProductDistributorResource(AssociationModelResource): 406 | model = ProductDistributor 407 | 408 | schema = ProductDistributorSchema 409 | 410 | auth_required = True 411 | 412 | roles_accepted = ('admin', 'owner') 413 | 414 | def has_read_permission(self, qs): 415 | if current_user.has_permission('view_product_distributor'): 416 | return qs.filter(self.model.retail_shop_id.in_(current_user.retail_shop_ids)) 417 | return qs.filter(false()) 418 | 419 | def has_change_permission(self, obj, data): 420 | return current_user.has_shop_access(obj.retail_shop_id) and \ 421 | current_user.has_permission('change_product_distributor') 422 | 423 | def has_delete_permission(self, obj, data): 424 | return current_user.has_shop_access(obj.retail_shop_id) and \ 425 | current_user.has_permission('remove_product_distributor') 426 | 427 | def has_add_permission(self, obj, data): 428 | if not current_user.has_permission('create_product_distributor') or \ 429 | not current_user.has_shop_access(Product.query.with_entities(Product.retail_shop_id) 430 | .filter(Product.id == obj.product_id).scalar()): 431 | return False 432 | return True 433 | 434 | 435 | class ProductTagResource(AssociationModelResource): 436 | model = ProductTag 437 | 438 | schema = ProductTagSchema 439 | 440 | auth_required = True 441 | 442 | roles_accepted = ('admin',) 443 | 444 | optional = ('product', 'salt') 445 | 446 | def has_read_permission(self, qs): 447 | if current_user.has_permission('view_product_tag'): 448 | return qs.filter(self.model.retail_shop_id.in_(current_user.retail_shop_ids)) 449 | return qs.filter(false()) 450 | 451 | def has_change_permission(self, obj, data): 452 | return current_user.has_shop_access(obj.retail_shop_id) and \ 453 | current_user.has_permission('change_product_tag') 454 | 455 | def has_delete_permission(self, obj, data): 456 | return current_user.has_shop_access(obj.retail_shop_id) and \ 457 | current_user.has_permission('remove_product_tag') 458 | 459 | def has_add_permission(self, obj, data): 460 | 461 | if not current_user.has_permission('create_product_tag') or \ 462 | not current_user.has_shop_access(Product.query.with_entities(Product.retail_shop_id) 463 | .filter(Product.id == obj.product_id).scalar()): 464 | return False 465 | return True 466 | 467 | 468 | class ProductSaltResource(AssociationModelResource): 469 | model = ProductSalt 470 | 471 | schema = ProductSaltSchema 472 | 473 | auth_required = True 474 | 475 | default_limit = 100 476 | 477 | max_limit = 500 478 | 479 | roles_accepted = ('admin',) 480 | 481 | optional = ('product', 'salt') 482 | 483 | filters = { 484 | 'updated_on': [ops.Greaterequal, ops.DateGreaterEqual, ops.DateEqual, ops.DateLesserEqual], 485 | 'created_on': [ops.Greaterequal, ops.DateGreaterEqual, ops.DateEqual, ops.DateLesserEqual] 486 | } 487 | 488 | def has_read_permission(self, qs): 489 | if current_user.has_permission('view_product_salt'): 490 | return qs.filter(self.model.retail_shop_id.in_(current_user.retail_shop_ids)) 491 | return qs.filter(false()) 492 | 493 | def has_change_permission(self, obj, data): 494 | return current_user.has_shop_access(obj.retail_shop_id) and \ 495 | current_user.has_permission('change_product_salt') 496 | 497 | def has_delete_permission(self, obj, data): 498 | return current_user.has_shop_access(obj.retail_shop_id) and \ 499 | current_user.has_permission('remove_product_salt') 500 | 501 | def has_add_permission(self, obj, data): 502 | if not current_user.has_permission('create_product_salt') or \ 503 | not current_user.has_shop_access(Product.query.with_entities(Product.retail_shop_id) 504 | .filter(Product.id == obj.product_id).scalar()): 505 | return False 506 | return True 507 | 508 | 509 | class ProductTaxResource(AssociationModelResource): 510 | model = ProductTax 511 | schema = ProductTaxSchema 512 | 513 | auth_required = True 514 | 515 | def has_read_permission(self, qs): 516 | if current_user.has_permission('view_product_tax'): 517 | return qs.filter(self.model.retail_shop_id.in_(current_user.retail_shop_ids)) 518 | return qs.filter(false()) 519 | 520 | def has_change_permission(self, obj, data): 521 | return current_user.has_shop_access(obj.retail_shop_id) and current_user.has_permission('change_product_tax') 522 | 523 | def has_delete_permission(self, obj, data): 524 | return current_user.has_shop_access(obj.retail_shop_id) and current_user.has_permission('remove_product_tax') 525 | 526 | def has_add_permission(self, obj, data): 527 | if not current_user.has_permission('create_product_tax') or \ 528 | not current_user.has_shop_access(Product.query.with_entities(Product.retail_shop_id) 529 | .filter(Product.id == obj.product_id).scalar()): 530 | return False 531 | return True 532 | 533 | 534 | class BrandDistributorResource(AssociationModelResource): 535 | model = BrandDistributor 536 | schema = BrandDistributorSchema 537 | 538 | optional = ('brand', 'distributor') 539 | 540 | filters = { 541 | 'brand_id': [ops.In, ops.Equal], 542 | 'distributor_id': [ops.In, ops.Equal] 543 | } 544 | 545 | auth_required = True 546 | 547 | def has_read_permission(self, qs): 548 | if current_user.has_permission('view_brand_distributor'): 549 | return qs.filter(self.model.retail_shop_id.in_(current_user.retail_shop_ids)) 550 | return qs.filter(false()) 551 | 552 | def has_change_permission(self, obj, data): 553 | return current_user.has_shop_access(obj.retail_shop_id) and current_user.has_permission( 554 | 'change_brand_distributor') 555 | 556 | def has_delete_permission(self, obj, data): 557 | return current_user.has_shop_access(obj.retail_shop_id) and current_user.has_permission( 558 | 'remove_brand_distributor') 559 | 560 | def has_add_permission(self, obj, data): 561 | if not current_user.has_permission('create_brand_distributor') or \ 562 | not current_user.has_shop_access(Product.query.with_entities(Brand.retail_shop_id) 563 | .filter(Brand.id == obj.brand_id).scalar()): 564 | return False 565 | return True 566 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | {one line to give the program's name and a brief idea of what it does.} 635 | Copyright (C) {year} {name of author} 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | {project} Copyright (C) {year} {fullname} 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | --------------------------------------------------------------------------------