├── .gitignore ├── .pre-commit-config.yaml ├── MANIFEST.in ├── README.md ├── erpnext_quota ├── __init__.py ├── config │ ├── __init__.py │ ├── desktop.py │ ├── docs.py │ └── settings.py ├── erpnext_quota │ ├── __init__.py │ ├── doctype │ │ ├── __init__.py │ │ ├── quota_document_limit_detail │ │ │ ├── __init__.py │ │ │ ├── quota_document_limit_detail.json │ │ │ └── quota_document_limit_detail.py │ │ └── usage_info │ │ │ ├── __init__.py │ │ │ ├── test_usage_info.py │ │ │ ├── usage_info.js │ │ │ ├── usage_info.json │ │ │ └── usage_info.py │ └── quota.py ├── events │ ├── __init__.py │ └── auth.py ├── hooks.py ├── install.py ├── modules.txt ├── patches.txt ├── tasks.py └── templates │ ├── __init__.py │ └── pages │ └── __init__.py ├── images ├── database_limit.png ├── files_space_limit.png ├── login_validity.gif ├── usage_info.png ├── usage_info_doc.png ├── usage_info_error.png └── user_limit.png ├── license.txt ├── requirements.txt └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.pyc 3 | *.egg-info 4 | *.swp 5 | tags 6 | erpnext_quota/docs/current -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | exclude: 'node_modules|.git' 2 | default_stages: [commit] 3 | fail_fast: false 4 | 5 | 6 | repos: 7 | - repo: https://github.com/pre-commit/pre-commit-hooks 8 | rev: v4.0.1 9 | hooks: 10 | - id: trailing-whitespace 11 | files: "erpnext_quota.*" 12 | exclude: ".*json$|.*txt$|.*csv|.*md" 13 | - id: check-yaml 14 | - id: check-merge-conflict 15 | - id: check-ast 16 | 17 | - repo: https://gitlab.com/pycqa/flake8 18 | rev: 3.9.2 19 | hooks: 20 | - id: flake8 21 | args: ['--config', '.github/helper/.flake8_strict', "--ignore=F401,E501,W191,E302"] 22 | exclude: ".*setup.py$" 23 | 24 | - repo: https://github.com/timothycrosley/isort 25 | rev: 5.9.1 26 | hooks: 27 | - id: isort 28 | exclude: ".*setup.py$" 29 | 30 | ci: 31 | autoupdate_schedule: weekly 32 | skip: [] 33 | submodules: false 34 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include MANIFEST.in 2 | include requirements.txt 3 | include *.json 4 | include *.md 5 | include *.py 6 | include *.txt 7 | recursive-include erpnext_quota *.css 8 | recursive-include erpnext_quota *.csv 9 | recursive-include erpnext_quota *.html 10 | recursive-include erpnext_quota *.ico 11 | recursive-include erpnext_quota *.js 12 | recursive-include erpnext_quota *.json 13 | recursive-include erpnext_quota *.md 14 | recursive-include erpnext_quota *.png 15 | recursive-include erpnext_quota *.py 16 | recursive-include erpnext_quota *.svg 17 | recursive-include erpnext_quota *.txt 18 | recursive-exclude erpnext_quota *.pyc -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Erpnext Quota 2 | 3 | App to manage ERPNext User, Company and Space limitations 4 | 5 | #### How to Install 6 | ``` 7 | bench get-app https://github.com/ahmadpak/erpnext_quota 8 | bench --site *site_name* install-app erpnext_quota 9 | ``` 10 | ### Usage 11 | Install the app. It will add quota config in the site_config.json file 12 | Contents will look similar: 13 | 14 | ```json 15 | { 16 | "db_name": "_153e0b60307d7518", 17 | "db_password": "LrhxSwya9SlAfjAa", 18 | "db_type": "mariadb", 19 | "encryption_key": "IcfnBCemM-aDs6Xe9RErXLMlXsDdM1nfC4q3jg7_PFE=", 20 | "quota": { 21 | "active_users": 6, 22 | "backup_files_size": 29, 23 | "company": 2, 24 | "count_administrator_user": 0, 25 | "count_website_users": 0, 26 | "db_space": 0, 27 | "document_limit": { 28 | "Sales Invoice": {"limit": 10, "period": "Daily"}, 29 | "Purchase Invoice": {"limit": 10, "period": "Weekly"}, 30 | "Journal Entry": {"limit": 10, "period": "Monthly"}, 31 | "Payment Entry": {"limit": 10, "period": "Monthly"} 32 | }, 33 | "private_files_size": 0, 34 | "public_files_size": 3, 35 | "space": 0, 36 | "used_company": 1, 37 | "used_space": 31, 38 | "users": 5, 39 | "valid_till": "2023-03-19" 40 | }, 41 | "user_type_doctype_limit": { 42 | "employee_self_service": 20 43 | } 44 | ``` 45 | 46 | Manually change the default values to change the limits. 47 | Default is: 48 | - 5 active users not including website users 49 | - 2 companies 50 | 51 | quota.json file will automatically get updated for any 52 | 53 | To view the Usage info, find it in Settings Module or search 'Usage Info' in the awesome bar 54 | ![Database Limit Screenshot](images/database_limit.png) 55 | ![Files Limit Screenshot](images/files_space_limit.png) 56 | ![User Limit Screenshot](images/user_limit.png) 57 | ![Login Limit Screenshot](images/login_validity.gif) 58 | ![Usage Info Screenshot](images/usage_info_doc.png) 59 | 60 | #### License 61 | MIT 62 | -------------------------------------------------------------------------------- /erpnext_quota/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | __version__ = '0.1.0' 5 | -------------------------------------------------------------------------------- /erpnext_quota/config/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahmadpak/erpnext_quota/2feb8e99faf404869013bf44570d342f40e8fea9/erpnext_quota/config/__init__.py -------------------------------------------------------------------------------- /erpnext_quota/config/desktop.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | from frappe import _ 4 | 5 | def get_data(): 6 | return [ 7 | { 8 | "module_name": "Erpnext Quota", 9 | "color": "grey", 10 | "icon": "octicon octicon-file-directory", 11 | "type": "module", 12 | "label": _("Erpnext Quota") 13 | } 14 | ] 15 | -------------------------------------------------------------------------------- /erpnext_quota/config/docs.py: -------------------------------------------------------------------------------- 1 | """ 2 | Configuration for docs 3 | """ 4 | 5 | # source_link = "https://github.com/[org_name]/erpnext_quota" 6 | # docs_base_url = "https://[org_name].github.io/erpnext_quota" 7 | # headline = "App that does everything" 8 | # sub_heading = "Yes, you got that right the first time, everything" 9 | 10 | def get_context(context): 11 | context.brand_html = "Erpnext Quota" 12 | -------------------------------------------------------------------------------- /erpnext_quota/config/settings.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | from frappe import _ 3 | from frappe.desk.moduleview import add_setup_section 4 | 5 | def get_data(): 6 | data = [ 7 | { 8 | "label": _("Settings"), 9 | "icon": "fa fa-wrench", 10 | "items": [ 11 | { 12 | "type": "doctype", 13 | "name": "Usage Info", 14 | "label": _("Usage Info"), 15 | "hide_count": True, 16 | "settings": 1, 17 | } 18 | ] 19 | } 20 | ] 21 | 22 | return data -------------------------------------------------------------------------------- /erpnext_quota/erpnext_quota/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahmadpak/erpnext_quota/2feb8e99faf404869013bf44570d342f40e8fea9/erpnext_quota/erpnext_quota/__init__.py -------------------------------------------------------------------------------- /erpnext_quota/erpnext_quota/doctype/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahmadpak/erpnext_quota/2feb8e99faf404869013bf44570d342f40e8fea9/erpnext_quota/erpnext_quota/doctype/__init__.py -------------------------------------------------------------------------------- /erpnext_quota/erpnext_quota/doctype/quota_document_limit_detail/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahmadpak/erpnext_quota/2feb8e99faf404869013bf44570d342f40e8fea9/erpnext_quota/erpnext_quota/doctype/quota_document_limit_detail/__init__.py -------------------------------------------------------------------------------- /erpnext_quota/erpnext_quota/doctype/quota_document_limit_detail/quota_document_limit_detail.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions": [], 3 | "allow_rename": 1, 4 | "creation": "2023-03-01 21:01:37.958915", 5 | "doctype": "DocType", 6 | "editable_grid": 1, 7 | "engine": "InnoDB", 8 | "field_order": [ 9 | "document_type", 10 | "period", 11 | "limit", 12 | "column_break_vjt7h", 13 | "usage", 14 | "from_date", 15 | "to_date" 16 | ], 17 | "fields": [ 18 | { 19 | "fieldname": "document_type", 20 | "fieldtype": "Link", 21 | "in_list_view": 1, 22 | "label": "Document Type", 23 | "options": "DocType", 24 | "reqd": 1 25 | }, 26 | { 27 | "fieldname": "limit", 28 | "fieldtype": "Int", 29 | "in_list_view": 1, 30 | "label": "Limit", 31 | "reqd": 1 32 | }, 33 | { 34 | "default": "Daily", 35 | "fieldname": "period", 36 | "fieldtype": "Select", 37 | "in_list_view": 1, 38 | "label": "Period", 39 | "options": "Daily\nWeekly\nMonthly", 40 | "reqd": 1 41 | }, 42 | { 43 | "default": "0", 44 | "fieldname": "usage", 45 | "fieldtype": "Int", 46 | "in_list_view": 1, 47 | "label": "Usage" 48 | }, 49 | { 50 | "fieldname": "from_date", 51 | "fieldtype": "Date", 52 | "in_list_view": 1, 53 | "label": "From Date" 54 | }, 55 | { 56 | "fieldname": "to_date", 57 | "fieldtype": "Date", 58 | "in_list_view": 1, 59 | "label": "To Date" 60 | }, 61 | { 62 | "fieldname": "column_break_vjt7h", 63 | "fieldtype": "Column Break" 64 | } 65 | ], 66 | "istable": 1, 67 | "links": [], 68 | "modified": "2023-03-05 14:07:37.765300", 69 | "modified_by": "Administrator", 70 | "module": "Erpnext Quota", 71 | "name": "Quota Document Limit Detail", 72 | "owner": "Administrator", 73 | "permissions": [], 74 | "sort_field": "modified", 75 | "sort_order": "DESC" 76 | } -------------------------------------------------------------------------------- /erpnext_quota/erpnext_quota/doctype/quota_document_limit_detail/quota_document_limit_detail.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, Havenir Solutions Private Limited and contributors 2 | # For license information, please see license.txt 3 | 4 | # import frappe 5 | from frappe.model.document import Document 6 | 7 | class QuotaDocumentLimitDetail(Document): 8 | pass 9 | -------------------------------------------------------------------------------- /erpnext_quota/erpnext_quota/doctype/usage_info/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahmadpak/erpnext_quota/2feb8e99faf404869013bf44570d342f40e8fea9/erpnext_quota/erpnext_quota/doctype/usage_info/__init__.py -------------------------------------------------------------------------------- /erpnext_quota/erpnext_quota/doctype/usage_info/test_usage_info.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (c) 2020, Havenir Solutions Private Limited and Contributors 3 | # See license.txt 4 | from __future__ import unicode_literals 5 | 6 | # import frappe 7 | import unittest 8 | 9 | class TestUsageInfo(unittest.TestCase): 10 | pass 11 | -------------------------------------------------------------------------------- /erpnext_quota/erpnext_quota/doctype/usage_info/usage_info.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020, Havenir Solutions Private Limited and contributors 2 | // For license information, please see license.txt 3 | 4 | frappe.ui.form.on('Usage Info', { 5 | setup: function (frm){ 6 | frm.disable_save(); 7 | }, 8 | onload_post_render: function (frm){ 9 | frm.disable_save(); 10 | frm.call('get_usage_info').then( r => { 11 | frm.refresh(); 12 | }) 13 | }, 14 | refresh: function(frm) { 15 | frm.disable_save(); 16 | } 17 | }); 18 | 19 | 20 | frappe.ui.form.on('Quota Document Limit Detail', { 21 | document_limit_add: function (frm, cdt, cdn){ 22 | frm.set_query('document_type', 'document_limit', () => { 23 | return { 24 | filters: { 25 | issingle: 0, 26 | istable: 0, 27 | module: ["!=", "Core"], 28 | name: ["NOT IN", "Email Queue", "Notification Log"] 29 | } 30 | } 31 | }) 32 | } 33 | }); -------------------------------------------------------------------------------- /erpnext_quota/erpnext_quota/doctype/usage_info/usage_info.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions": [], 3 | "creation": "2020-02-21 16:16:00.879982", 4 | "doctype": "DocType", 5 | "document_type": "System", 6 | "editable_grid": 1, 7 | "engine": "InnoDB", 8 | "field_order": [ 9 | "users", 10 | "space", 11 | "company", 12 | "column_break_3", 13 | "active_users", 14 | "used_space", 15 | "used_company", 16 | "space_usage_details_section", 17 | "private_files_size", 18 | "public_files_size", 19 | "column_break_11", 20 | "backup_files_size", 21 | "database_details_section", 22 | "db_space", 23 | "column_break_15", 24 | "used_db_space", 25 | "document_limit_section", 26 | "document_limit" 27 | ], 28 | "fields": [ 29 | { 30 | "fieldname": "column_break_3", 31 | "fieldtype": "Column Break" 32 | }, 33 | { 34 | "fieldname": "active_users", 35 | "fieldtype": "Int", 36 | "label": "Active Users", 37 | "read_only": 1 38 | }, 39 | { 40 | "fieldname": "used_space", 41 | "fieldtype": "Int", 42 | "label": "Used Space (MB)", 43 | "read_only": 1 44 | }, 45 | { 46 | "fieldname": "space_usage_details_section", 47 | "fieldtype": "Tab Break", 48 | "label": "Space Usage Details" 49 | }, 50 | { 51 | "fieldname": "column_break_11", 52 | "fieldtype": "Column Break" 53 | }, 54 | { 55 | "fieldname": "private_files_size", 56 | "fieldtype": "Int", 57 | "label": "Private Files (MB)", 58 | "read_only": 1 59 | }, 60 | { 61 | "fieldname": "public_files_size", 62 | "fieldtype": "Int", 63 | "label": "Public Files (MB)", 64 | "read_only": 1 65 | }, 66 | { 67 | "fieldname": "backup_files_size", 68 | "fieldtype": "Int", 69 | "label": "Backup Files (MB)", 70 | "read_only": 1 71 | }, 72 | { 73 | "fieldname": "database_details_section", 74 | "fieldtype": "Tab Break", 75 | "label": "Database Details" 76 | }, 77 | { 78 | "default": "0", 79 | "fieldname": "db_space", 80 | "fieldtype": "Data", 81 | "label": "Database Space Allowed (MB)", 82 | "read_only": 1 83 | }, 84 | { 85 | "default": "0", 86 | "fieldname": "used_db_space", 87 | "fieldtype": "Int", 88 | "label": "Database Size (MB)", 89 | "read_only": 1 90 | }, 91 | { 92 | "fieldname": "column_break_15", 93 | "fieldtype": "Column Break" 94 | }, 95 | { 96 | "fieldname": "users", 97 | "fieldtype": "Data", 98 | "label": "Users Allowed", 99 | "read_only": 1 100 | }, 101 | { 102 | "fieldname": "space", 103 | "fieldtype": "Data", 104 | "label": "Files Space Allowed (MB)", 105 | "read_only": 1 106 | }, 107 | { 108 | "fieldname": "company", 109 | "fieldtype": "Data", 110 | "label": "Company Allowed", 111 | "read_only": 1 112 | }, 113 | { 114 | "fieldname": "used_company", 115 | "fieldtype": "Int", 116 | "label": "Active Company", 117 | "read_only": 1 118 | }, 119 | { 120 | "fieldname": "document_limit_section", 121 | "fieldtype": "Tab Break", 122 | "label": "Document Limit" 123 | }, 124 | { 125 | "fieldname": "document_limit", 126 | "fieldtype": "Table", 127 | "label": "Document Limit", 128 | "options": "Quota Document Limit Detail", 129 | "read_only": 1 130 | } 131 | ], 132 | "hide_toolbar": 1, 133 | "issingle": 1, 134 | "links": [], 135 | "modified": "2023-03-19 01:58:17.445013", 136 | "modified_by": "Administrator", 137 | "module": "Erpnext Quota", 138 | "name": "Usage Info", 139 | "owner": "Administrator", 140 | "permissions": [ 141 | { 142 | "email": 1, 143 | "print": 1, 144 | "read": 1, 145 | "role": "System Manager", 146 | "share": 1 147 | } 148 | ], 149 | "quick_entry": 1, 150 | "sort_field": "modified", 151 | "sort_order": "DESC", 152 | "states": [] 153 | } -------------------------------------------------------------------------------- /erpnext_quota/erpnext_quota/doctype/usage_info/usage_info.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (c) 2020, Havenir Solutions Private Limited and contributors 3 | # For license information, please see license.txt 4 | 5 | from __future__ import unicode_literals 6 | 7 | import frappe 8 | from frappe.model.document import Document 9 | 10 | from erpnext_quota.erpnext_quota.quota import get_limit_period 11 | 12 | 13 | class UsageInfo(Document): 14 | @frappe.whitelist() 15 | def get_usage_info(self): 16 | quota = frappe.get_site_config()['quota'] 17 | usage = {} 18 | 19 | for key, value in quota.items(): 20 | usage[key] = value 21 | 22 | # copy out document_limit and remove from dict 23 | document_limit = usage['document_limit'] 24 | del usage['document_limit'] 25 | 26 | for key, value in usage.items(): 27 | self.db_set(key, value) 28 | 29 | # update document list table 30 | frappe.db.truncate("Quota Document Limit Detail") 31 | self.reload() 32 | for key, value in document_limit.items(): 33 | period = get_limit_period(value['period']) 34 | value['usage'] = len(frappe.db.get_all( 35 | key, 36 | {'creation': ["BETWEEN", [f"{period.start} 00:00:00.000000", f"{period.end} 23:23:59.999999"]]} 37 | )) 38 | value['document_type'] = key 39 | value['from_date'] = period.start 40 | value['to_date'] = period.end 41 | self.append('document_limit', value) 42 | self.save() 43 | self.reload() 44 | -------------------------------------------------------------------------------- /erpnext_quota/erpnext_quota/quota.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | 3 | import frappe 4 | from frappe import _ 5 | from frappe.installer import update_site_config 6 | from frappe.utils import get_first_day, get_first_day_of_week, getdate 7 | 8 | 9 | # User 10 | def user_limit(self, method): 11 | quota = frappe.get_site_config()['quota'] 12 | count_website_users = quota["count_website_users"] 13 | count_administrator_user = quota["count_administrator_user"] 14 | allowed_users = quota["users"] 15 | active_users = validate_users(self, count_administrator_user, count_website_users, allowed_users) 16 | quota['active_users'] = active_users 17 | 18 | # updating site config 19 | update_site_config('quota', quota) 20 | 21 | 22 | def validate_users(self, count_administrator_user, count_website_users, allowed_users): 23 | ''' 24 | Validates and returns active users 25 | Params: 26 | 1. self 27 | 2. count_administrator_user => (bool) => either count administrator or not 28 | 3. count_website_users => (bool) => either count website users or not 29 | 4. allowed_users => (int) => maximum users allowed 30 | ''' 31 | # allowed user value type check 32 | if type(allowed_users) is not int: 33 | frappe.throw(_("Invalid value for maximum User Allowed limit. it can be a whole number only."), frappe.ValidationError) 34 | 35 | # Fetching all active users list 36 | filters = { 37 | 'enabled': 1, 38 | "name": ['!=', 'Guest'] 39 | } 40 | 41 | # if we don't have to count administrator also 42 | if count_administrator_user == 0: 43 | filters['name'] = ['not in', ['Guest', 'Administrator']] 44 | 45 | user_list = frappe.get_all('User', filters, ["name"]) 46 | active_users = len(user_list) 47 | is_desk = "" 48 | 49 | # if don't have to count website users 50 | if not count_website_users: 51 | active_users = 0 52 | is_desk = "Desk" 53 | 54 | for user in user_list: 55 | roles = frappe.get_all("Has Role", {'parent': user.name}, ['role']) 56 | for row in roles: 57 | if frappe.get_value("Role", row.role, "desk_access") == 1: 58 | active_users += 1 59 | break 60 | 61 | # Users limit validation 62 | if allowed_users != 0 and active_users >= allowed_users: 63 | if not frappe.get_all('User', filters={'name': self.name}): 64 | frappe.throw('Only {} active {} users allowed and you have {} active users. Please disable users or to increase the limit please contact sales'. format(allowed_users, is_desk, active_users)) 65 | 66 | return active_users 67 | 68 | 69 | # Files 70 | def files_space_limit(self, method): 71 | validate_files_space_limit() 72 | 73 | 74 | def validate_files_space_limit(): 75 | ''' 76 | Validates files space limit 77 | ''' 78 | 79 | # Site config 80 | quota = frappe.get_site_config()['quota'] 81 | 82 | allowed_space = quota["space"] 83 | 84 | # allowed space value type check 85 | if type(allowed_space) is not int: 86 | frappe.throw(_("Invalid value for maximum Space limit. It can be a whole number only."), frappe.ValidationError) 87 | 88 | # all possible file locations 89 | site_path = frappe.get_site_path() 90 | private_files_path = site_path + '/private/files' 91 | public_files_path = site_path + '/public/files' 92 | backup_files_path = site_path + '/private/backups' 93 | 94 | # Calculating Sizes 95 | total_size = get_directory_size(site_path) 96 | private_files_size = get_directory_size(private_files_path) 97 | public_files_size = get_directory_size(public_files_path) 98 | backup_files_size = get_directory_size(backup_files_path) 99 | 100 | # Writing File 101 | quota['used_space'] = total_size 102 | quota['private_files_size'] = private_files_size 103 | quota['public_files_size'] = public_files_size 104 | quota['backup_files_size'] = backup_files_size 105 | 106 | update_site_config('quota', quota) 107 | 108 | if allowed_space != 0 and total_size > allowed_space: 109 | msg = ''' 110 |
You have exceeded your files space limit. Delete some files from file manager or to increase the limit please contact sales
111 |
112 | '''.format(private_files_size, public_files_size, backup_files_size) 113 | 114 | frappe.throw(_(msg)) 115 | 116 | 117 | # DB 118 | def db_space_limit(self, method): 119 | validate_db_space_limit() 120 | 121 | 122 | def validate_db_space_limit(): 123 | ''' 124 | Validates DB space limit 125 | ''' 126 | # Site config 127 | quota = frappe.get_site_config()['quota'] 128 | allowed_db_space = quota["db_space"] 129 | 130 | # allowed DB space value type check 131 | if type(allowed_db_space) is not int: 132 | frappe.throw(_("Invalid value for maximum Database Space limit. it can be a whole number only."), frappe.ValidationError) 133 | 134 | # Getting DB Space 135 | used_db_space = frappe.db.sql('''SELECT `table_schema` as `database_name`, SUM(`data_length` + `index_length`) / 1024 / 1024 AS `database_size` FROM information_schema.tables GROUP BY `table_schema`''')[1][1] 136 | used_db_space = int(used_db_space) 137 | 138 | # Updating quota config 139 | quota['used_db_space'] = used_db_space 140 | 141 | update_site_config('quota', quota) 142 | 143 | if allowed_db_space != 0 and used_db_space > allowed_db_space: 144 | msg = ''' 145 |
You have exceeded your Database Size limit. Please contact sales to upgrade your package
146 | 147 | '''.format(allowed_db_space, used_db_space) 148 | frappe.throw(_(msg)) 149 | 150 | 151 | # Company 152 | def company_limit(self, method): 153 | ''' 154 | Validates Company limit 155 | ''' 156 | 157 | quota = frappe.get_site_config()['quota'] 158 | allowed_companies = quota.get('company') 159 | 160 | # allowed Companies value type check 161 | if type(allowed_companies) is not int: 162 | frappe.throw(_("Invalid value for maximum allowed Companies limit. it can be a whole number only."), frappe.ValidationError) 163 | 164 | # Calculating total companies 165 | total_company = len(frappe.db.get_all('Company', filters={})) 166 | quota['used_company'] = total_company 167 | 168 | # Updating site config 169 | update_site_config('quota', quota) 170 | 171 | # Validation 172 | if allowed_companies != 0 and total_company >= allowed_companies: 173 | if not frappe.get_all('Company', {'name': self.name}): 174 | frappe.throw(_("Only {} company(s) allowed and you have {} company(s).Please remove other company or to increase the limit please contact sales").format(quota.get('company'), total_company)) 175 | 176 | 177 | # Directory Size 178 | def get_directory_size(path): 179 | ''' 180 | returns total size of directory in MBss 181 | ''' 182 | output_string = subprocess.check_output(["du", "-mcs", "{}".format(path)]) 183 | total_size = '' 184 | for char in output_string: 185 | if chr(char) == "\t": 186 | break 187 | else: 188 | total_size += chr(char) 189 | 190 | return int(total_size) 191 | 192 | 193 | def document_limit(doc, event): 194 | """ 195 | We check for the doctype in document_limit and compute accordingly. 196 | """ 197 | limit_dict = frappe.get_site_config()['quota']['document_limit'] 198 | if (limit_dict.get(doc.doctype)): 199 | limit = frappe._dict(limit_dict.get(doc.doctype)) 200 | limit_period = get_limit_period(limit.period) 201 | usage = len(frappe.db.get_all( 202 | doc.doctype, 203 | filters={ 204 | 'creation': ['BETWEEN', [str(limit_period.start) + ' 00:00:00.000000', str(limit_period.end) + ' 23:59:59.999999']] 205 | })) 206 | if usage >= limit.limit: 207 | msg = _(f"Your have reached your {doc.doctype} {limit.period} limit of {limit.limit} and hench cannot create new document. Please contact administrator.") 208 | frappe.throw(msg, title="Quota Limit") 209 | 210 | 211 | def get_limit_period(period): 212 | """ 213 | Get date mappinf for document limit period 214 | """ 215 | today = getdate() 216 | week_start = get_first_day_of_week(today) 217 | periods = { 218 | 'Daily': {'start': str(today), 'end': str(today)}, 219 | 'Weekly': {'start': str(week_start), 'end': str(today)}, 220 | 'Monthly': {'start': str(get_first_day(today)), 'end': str(today)}, 221 | } 222 | return frappe._dict(periods.get(period)) 223 | -------------------------------------------------------------------------------- /erpnext_quota/events/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahmadpak/erpnext_quota/2feb8e99faf404869013bf44570d342f40e8fea9/erpnext_quota/events/__init__.py -------------------------------------------------------------------------------- /erpnext_quota/events/auth.py: -------------------------------------------------------------------------------- 1 | import frappe 2 | from frappe import _ 3 | from frappe.utils.data import date_diff, today 4 | 5 | 6 | def successful_login(login_manager): 7 | """ 8 | on_login verify if site is not expired 9 | """ 10 | quota = frappe.get_site_config()['quota'] 11 | valid_till = quota['valid_till'] 12 | diff = date_diff(valid_till, today()) 13 | if diff < 0: 14 | frappe.throw(_("You site is suspended. Please contact Sales"), frappe.AuthenticationError) 15 | -------------------------------------------------------------------------------- /erpnext_quota/hooks.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | app_name = "erpnext_quota" 5 | app_title = "Erpnext Quota" 6 | app_publisher = "Havenir Solutions Private Limited" 7 | app_description = "App to manage ERPNext User and Space limitations" 8 | app_icon = "octicon octicon-file-directory" 9 | app_color = "grey" 10 | app_email = "info@havenir.com" 11 | app_license = "MIT" 12 | 13 | # Includes in 14 | # ------------------ 15 | 16 | # include js, css files in header of desk.html 17 | # app_include_css = "/assets/erpnext_quota/css/erpnext_quota.css" 18 | # app_include_js = "/assets/erpnext_quota/js/erpnext_quota.js" 19 | 20 | # include js, css files in header of web template 21 | # web_include_css = "/assets/erpnext_quota/css/erpnext_quota.css" 22 | # web_include_js = "/assets/erpnext_quota/js/erpnext_quota.js" 23 | 24 | # include js in page 25 | # page_js = {"page" : "public/js/file.js"} 26 | 27 | # include js in doctype views 28 | # doctype_js = {"doctype" : "public/js/doctype.js"} 29 | # doctype_list_js = {"doctype" : "public/js/doctype_list.js"} 30 | # doctype_tree_js = {"doctype" : "public/js/doctype_tree.js"} 31 | # doctype_calendar_js = {"doctype" : "public/js/doctype_calendar.js"} 32 | 33 | # Home Pages 34 | # ---------- 35 | 36 | # application home page (will override Website Settings) 37 | # home_page = "login" 38 | 39 | # website user home page (by Role) 40 | # role_home_page = { 41 | # "Role": "home_page" 42 | # } 43 | 44 | # Website user home page (by function) 45 | # get_website_user_home_page = "erpnext_quota.utils.get_home_page" 46 | 47 | # Generators 48 | # ---------- 49 | 50 | # automatically create page for each record of this doctype 51 | # website_generators = ["Web Page"] 52 | 53 | # Installation 54 | # ------------ 55 | 56 | before_install = "erpnext_quota.install.before_install" 57 | # after_install = "erpnext_quota.install.after_install" 58 | 59 | # Desk Notifications 60 | # ------------------ 61 | # See frappe.core.notifications.get_notification_config 62 | 63 | # notification_config = "erpnext_quota.notifications.get_notification_config" 64 | 65 | # Permissions 66 | # ----------- 67 | # Permissions evaluated in scripted ways 68 | 69 | # permission_query_conditions = { 70 | # "Event": "frappe.desk.doctype.event.event.get_permission_query_conditions", 71 | # } 72 | # 73 | # has_permission = { 74 | # "Event": "frappe.desk.doctype.event.event.has_permission", 75 | # } 76 | 77 | # Document Events 78 | # --------------- 79 | # Hook on document methods and events 80 | 81 | # doc_events = { 82 | # "*": { 83 | # "on_update": "method", 84 | # "on_cancel": "method", 85 | # "on_trash": "method" 86 | # } 87 | # } 88 | 89 | on_login = 'erpnext_quota.events.auth.successful_login' 90 | 91 | doc_events = { 92 | 'User': { 93 | 'validate': 'erpnext_quota.erpnext_quota.quota.user_limit', 94 | 'on_update': 'erpnext_quota.erpnext_quota.quota.user_limit' 95 | }, 96 | 'Company': { 97 | 'validate': 'erpnext_quota.erpnext_quota.quota.company_limit', 98 | 'on_update': 'erpnext_quota.erpnext_quota.quota.company_limit' 99 | }, 100 | '*': { 101 | 'on_submit': 'erpnext_quota.erpnext_quota.quota.db_space_limit', 102 | 'before_insert': 'erpnext_quota.erpnext_quota.quota.document_limit' 103 | }, 104 | 'File': { 105 | 'validate': 'erpnext_quota.erpnext_quota.quota.files_space_limit' 106 | } 107 | } 108 | # Scheduled Tasks 109 | # --------------- 110 | 111 | scheduler_events = { 112 | "daily": [ 113 | "erpnext_quota.tasks.daily" 114 | ] 115 | } 116 | 117 | # Testing 118 | # ------- 119 | 120 | # before_tests = "erpnext_quota.install.before_tests" 121 | 122 | # Overriding Methods 123 | # ------------------------------ 124 | # 125 | # override_whitelisted_methods = { 126 | # "frappe.desk.doctype.event.event.get_events": "erpnext_quota.event.get_events" 127 | # } 128 | # 129 | # each overriding function accepts a `data` argument; 130 | # generated from the base implementation of the doctype dashboard, 131 | # along with any modifications made in other Frappe apps 132 | # override_doctype_dashboards = { 133 | # "Task": "erpnext_quota.task.get_dashboard_data" 134 | # } 135 | -------------------------------------------------------------------------------- /erpnext_quota/install.py: -------------------------------------------------------------------------------- 1 | import frappe 2 | from frappe.installer import update_site_config 3 | from frappe.utils.data import add_days, today 4 | 5 | 6 | def before_install(): 7 | filters = { 8 | 'enabled': 1, 9 | 'name': ['not in', ['Guest', 'Administrator']] 10 | } 11 | 12 | user_list = frappe.get_all('User', filters=filters, fields=["name"]) 13 | 14 | active_users = 0 15 | 16 | for user in user_list: 17 | roles = frappe.get_all( 18 | "Has Role", 19 | filters={ 20 | 'parent': user.name 21 | }, 22 | fields=['role'] 23 | ) 24 | 25 | for row in roles: 26 | if frappe.get_value("Role", row.role, "desk_access") == 1: 27 | active_users += 1 28 | break 29 | 30 | data = { 31 | 'users': 5, 32 | 'active_users': active_users, 33 | 'space': 0, 34 | 'db_space': 0, 35 | 'company': 2, 36 | 'used_company': 1, 37 | 'count_website_users': 0, 38 | 'count_administrator_user': 0, 39 | 'valid_till': add_days(today(), 14), 40 | 'document_limit': { 41 | 'Sales Invoice': {'limit': 10, 'period': 'Daily'}, 42 | 'Purchase Invoice': {'limit': 10, 'period': 'Weekly'}, 43 | 'Journal Entry': {'limit': 10, 'period': 'Monthly'}, 44 | 'Payment Entry': {'limit': 10, 'period': 'Monthly'} 45 | } 46 | } 47 | 48 | # Updating site config 49 | update_site_config('quota', data) 50 | -------------------------------------------------------------------------------- /erpnext_quota/modules.txt: -------------------------------------------------------------------------------- 1 | Erpnext Quota -------------------------------------------------------------------------------- /erpnext_quota/patches.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahmadpak/erpnext_quota/2feb8e99faf404869013bf44570d342f40e8fea9/erpnext_quota/patches.txt -------------------------------------------------------------------------------- /erpnext_quota/tasks.py: -------------------------------------------------------------------------------- 1 | from erpnext_quota.erpnext_quota.quota import (validate_db_space_limit, 2 | validate_files_space_limit) 3 | 4 | 5 | def daily(): 6 | validate_files_space_limit() 7 | validate_db_space_limit() 8 | -------------------------------------------------------------------------------- /erpnext_quota/templates/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahmadpak/erpnext_quota/2feb8e99faf404869013bf44570d342f40e8fea9/erpnext_quota/templates/__init__.py -------------------------------------------------------------------------------- /erpnext_quota/templates/pages/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahmadpak/erpnext_quota/2feb8e99faf404869013bf44570d342f40e8fea9/erpnext_quota/templates/pages/__init__.py -------------------------------------------------------------------------------- /images/database_limit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahmadpak/erpnext_quota/2feb8e99faf404869013bf44570d342f40e8fea9/images/database_limit.png -------------------------------------------------------------------------------- /images/files_space_limit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahmadpak/erpnext_quota/2feb8e99faf404869013bf44570d342f40e8fea9/images/files_space_limit.png -------------------------------------------------------------------------------- /images/login_validity.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahmadpak/erpnext_quota/2feb8e99faf404869013bf44570d342f40e8fea9/images/login_validity.gif -------------------------------------------------------------------------------- /images/usage_info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahmadpak/erpnext_quota/2feb8e99faf404869013bf44570d342f40e8fea9/images/usage_info.png -------------------------------------------------------------------------------- /images/usage_info_doc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahmadpak/erpnext_quota/2feb8e99faf404869013bf44570d342f40e8fea9/images/usage_info_doc.png -------------------------------------------------------------------------------- /images/usage_info_error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahmadpak/erpnext_quota/2feb8e99faf404869013bf44570d342f40e8fea9/images/usage_info_error.png -------------------------------------------------------------------------------- /images/user_limit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahmadpak/erpnext_quota/2feb8e99faf404869013bf44570d342f40e8fea9/images/user_limit.png -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | License: MIT -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | frappe -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from setuptools import setup, find_packages 3 | 4 | with open('requirements.txt') as f: 5 | install_requires = f.read().strip().split('\n') 6 | 7 | # get version from __version__ variable in erpnext_quota/__init__.py 8 | from erpnext_quota import __version__ as version 9 | 10 | setup( 11 | name='erpnext_quota', 12 | version=version, 13 | description='App to manage ERPNext User and Space limitations', 14 | author='Havenir Solutions Private Limited', 15 | author_email='info@havenir.com', 16 | packages=find_packages(), 17 | zip_safe=False, 18 | include_package_data=True, 19 | install_requires=install_requires 20 | ) 21 | --------------------------------------------------------------------------------