├── .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 |  55 |  56 |  57 |  58 |  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 |