├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── LICENSE ├── MANIFEST.in ├── README.md ├── expenses ├── __init__.py ├── config │ ├── __init__.py │ ├── desktop.py │ └── docs.py ├── expenses │ ├── __init__.py │ ├── doctype │ │ ├── __init__.py │ │ ├── expense │ │ │ ├── __init__.py │ │ │ ├── expense.js │ │ │ ├── expense.json │ │ │ ├── expense.py │ │ │ └── expense_list.js │ │ ├── expense_attachment │ │ │ ├── __init__.py │ │ │ ├── expense_attachment.json │ │ │ └── expense_attachment.py │ │ ├── expense_item │ │ │ ├── __init__.py │ │ │ ├── expense_item.js │ │ │ ├── expense_item.json │ │ │ ├── expense_item.py │ │ │ └── expense_item_list.js │ │ ├── expense_item_account │ │ │ ├── __init__.py │ │ │ ├── expense_item_account.json │ │ │ └── expense_item_account.py │ │ ├── expense_type │ │ │ ├── __init__.py │ │ │ ├── expense_type.js │ │ │ ├── expense_type.json │ │ │ ├── expense_type.py │ │ │ ├── expense_type_list.js │ │ │ └── expense_type_tree.js │ │ ├── expense_type_account │ │ │ ├── __init__.py │ │ │ ├── expense_type_account.json │ │ │ └── expense_type_account.py │ │ ├── expenses_entry │ │ │ ├── __init__.py │ │ │ ├── expenses_entry.js │ │ │ ├── expenses_entry.json │ │ │ ├── expenses_entry.py │ │ │ └── expenses_entry_list.js │ │ ├── expenses_entry_details │ │ │ ├── __init__.py │ │ │ ├── expenses_entry_details.json │ │ │ └── expenses_entry_details.py │ │ ├── expenses_request │ │ │ ├── __init__.py │ │ │ ├── expenses_request.js │ │ │ ├── expenses_request.json │ │ │ ├── expenses_request.py │ │ │ └── expenses_request_list.js │ │ ├── expenses_request_details │ │ │ ├── __init__.py │ │ │ ├── expenses_request_details.json │ │ │ └── expenses_request_details.py │ │ ├── expenses_settings │ │ │ ├── __init__.py │ │ │ ├── expenses_settings.js │ │ │ ├── expenses_settings.json │ │ │ └── expenses_settings.py │ │ └── expenses_update_receiver │ │ │ ├── __init__.py │ │ │ ├── expenses_update_receiver.json │ │ │ └── expenses_update_receiver.py │ ├── print_format │ │ ├── __init__.py │ │ ├── expense │ │ │ ├── __init__.py │ │ │ ├── expense.html │ │ │ └── expense.json │ │ ├── expenses_entry │ │ │ ├── __init__.py │ │ │ ├── expenses_entry.html │ │ │ └── expenses_entry.json │ │ └── expenses_request │ │ │ ├── __init__.py │ │ │ ├── expenses_request.html │ │ │ └── expenses_request.json │ └── report │ │ ├── __init__.py │ │ └── expenses_entry_report │ │ ├── __init__.py │ │ ├── expenses_entry_report.html │ │ ├── expenses_entry_report.js │ │ ├── expenses_entry_report.json │ │ └── expenses_entry_report.py ├── fixtures │ ├── role.json │ ├── workflow.json │ ├── workflow_action_master.json │ └── workflow_state.json ├── hooks.py ├── libs │ ├── __init__.py │ ├── account.py │ ├── attachment.py │ ├── background.py │ ├── cache.py │ ├── check.py │ ├── common.py │ ├── company.py │ ├── entry.py │ ├── entry_details.py │ ├── exchange.py │ ├── expense.py │ ├── filter.py │ ├── item.py │ ├── journal.py │ ├── logger.py │ ├── realtime.py │ ├── request.py │ ├── request_details.py │ ├── search.py │ ├── system.py │ ├── type.py │ └── update.py ├── modules.txt ├── patches.txt ├── public │ └── js │ │ └── expenses.bundle.js ├── setup │ ├── __init__.py │ ├── install.py │ ├── migrate.py │ └── uninstall.py └── version.py ├── pyproject.toml ├── requirements.txt └── setup.py /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]: " 5 | labels: bug 6 | assignees: kid1194 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[REQ]: " 5 | labels: enhancement 6 | assignees: kid1194 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright 2022 Level Up Marketing & Development Services 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 6 | software and associated documentation files (the "Software"), to deal in the Software 7 | without restriction, including without limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons 9 | to whom the Software is furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all copies 12 | or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 15 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR 16 | PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 17 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 18 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE 19 | USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /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 expenses *.css 8 | recursive-include expenses *.csv 9 | recursive-include expenses *.html 10 | recursive-include expenses *.ico 11 | recursive-include expenses *.js 12 | recursive-include expenses *.json 13 | recursive-include expenses *.md 14 | recursive-include expenses *.png 15 | recursive-include expenses *.py 16 | recursive-include expenses *.svg 17 | recursive-include expenses *.txt 18 | recursive-exclude expenses *.pyc -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ERPNext Expenses 2 | 3 | ![v1.0.0-Alpha5](https://img.shields.io/badge/v1.0.0_Alpha5-2024/07/21-blue?style=plastic) 4 | 5 | An expenses management module for ERPNext. 6 | 7 | ⚠️ **This plugin is in ALPHA stage so it is not PRODUCTION ready.** ⚠️ 8 | 9 | --- 10 | 11 | ### Contributors 12 | **The list of people who deserves more than a simple "Thank You".** 13 | - [![Monolith Online](https://img.shields.io/badge/Monolith_Online-Debug_%7C_Test-red?style=plastic)](https://github.com/monolithon) 14 | - [![Andrew Rogers](https://img.shields.io/badge/Andrew_Rogers-Debug_%7C_Test-blue?style=plastic)](https://github.com/agrogers) 15 | - [![Washaqq](https://img.shields.io/badge/Washaqq-Debug_%7C_Test-orange?style=plastic)](https://github.com/washaqq) 16 | - [![Codi](https://img.shields.io/badge/Codi-Debug_%7C_Test-green?style=plastic)](https://github.com/hassan-youssef) 17 | - [![Ian Kahare](https://img.shields.io/badge/Ian_Kahare-Debug_%7C_Test-yellow?style=plastic)](https://github.com/iakah) 18 | 19 | --- 20 | 21 | ### Table of Contents 22 | - [Requirements](#requirements) 23 | - [Setup](#setup) 24 | - [Install](#install) 25 | - [Update](#update) 26 | - [Uninstall](#uninstall) 27 | - [Usage](#usage) 28 | - [Issues](#issues) 29 | - [License](#license) 30 | 31 | --- 32 | 33 | ### Requirements 34 | - [Frappe](https://github.com/frappe/frappe) >= v13.0.0 35 | - [ERPNext](https://github.com/frappe/erpnext) >= v13.0.0 36 | 37 | --- 38 | 39 | ### Setup 40 | 41 | ⚠️ **Important** ⚠️ 42 | 43 | *Do not forget to replace "[sitename]" with the name of your site in all commands.* 44 | 45 | #### Install 46 | 1. Go to bench directory 47 | 48 | ``` 49 | cd ~/frappe-bench 50 | ``` 51 | 52 | 2. Get plugin from Github 53 | 54 | ``` 55 | bench get-app https://github.com/kid1194/erpnext_expenses 56 | ``` 57 | 58 | 3. Build plugin 59 | 60 | ``` 61 | bench build --app expenses 62 | ``` 63 | 64 | 4. Install plugin on your site 65 | 66 | ``` 67 | bench --site [sitename] install-app expenses 68 | ``` 69 | 70 | 5. Restart bench to clear cache 71 | 72 | ``` 73 | bench restart 74 | ``` 75 | 76 | 6. Read the [Usage](#usage) section below 77 | 78 | #### Update 79 | 1. Go to app directory 80 | 81 | ``` 82 | cd ~/frappe-bench/apps/expenses 83 | ``` 84 | 85 | 2. Get updates from Github 86 | 87 | ``` 88 | git pull 89 | ``` 90 | 91 | 3. Go to bench directory (Optional) 92 | 93 | ``` 94 | cd ~/frappe-bench 95 | ``` 96 | 97 | 4. Build plugin 98 | 99 | ``` 100 | bench build --app expenses 101 | ``` 102 | 103 | 5. Update your site 104 | 105 | ``` 106 | bench --site [sitename] migrate 107 | ``` 108 | 109 | 5. Restart bench to clear cache 110 | 111 | ``` 112 | bench restart 113 | ``` 114 | 115 | #### Uninstall 116 | 1. Go to bench directory 117 | 118 | ``` 119 | cd ~/frappe-bench 120 | ``` 121 | 122 | 2. Uninstall plugin from your site 123 | 124 | ``` 125 | bench --site [sitename] uninstall-app expenses 126 | ``` 127 | 128 | 3. Remove plugin from bench cache 129 | 130 | ``` 131 | bench remove-app expenses 132 | ``` 133 | 134 | 4. Restart bench to clear cache 135 | 136 | ``` 137 | bench restart 138 | ``` 139 | 140 | --- 141 | 142 | ### Usage 143 | 1. **Expense Type** 144 | - Create the hierarchy of expense types based on your needs 145 | - Add an expense account for each company so it gets inherited by all new expense items 146 | 147 | ℹ️ *Note: Expense accounts are inherited from parents.* 148 | 149 | 2. **Expense Item** 150 | - Create the expense items that reflect your expenses 151 | - Add each expense item to the expense type that it belongs to 152 | - Add an expense account for each company and/or set the expense defaults (cost, quantity, etc..) 153 | - Modify the expense defaults (cost, quantity, etc..) of the inherited expense accounts, if exist 154 | 155 | ℹ️ *Note: Expense accounts will be inherited from linked expense type and they are not modifiable except for cost and quantity related fields.* 156 | 157 | 3. **Expense** 158 | - Create a company expense and select the expense item 159 | - Fill the cost, quantity, etc.. if not fixed for the expense item 160 | - Attachments can be added or removed even after submit, but before adding the expense to an expenses request 161 | 162 | 4. **Expenses Request** 163 | - Create a request for a company list of expenses so that it can be approved or rejected 164 | - When requests are rejected, the linked expenses will be automatically rejected & cancelled 165 | - Rejected requests can be appealed and after appealing, the status of linked expenses will be automatically restored and set as Requested 166 | 167 | 5. **Expenses Entry** 168 | - Create entries based on a request or manually add company related expenses 169 | - After submit, all the expenses will be posted to the journal 170 | 171 | 6. **Expenses Settings** 172 | - Enable the module (Enabled by default) 173 | - Modify the expense settings 174 | - Modify the update notification settings 175 | - Check for update manually 176 | 177 | ℹ️ *Note: Module update functionality will only be enabled in the PRODUCTION stage* 178 | 179 | --- 180 | 181 | ### Issues 182 | If you find bug in the plugin, please create a [bug report](https://github.com/kid1194/erpnext_expenses/issues/new?assignees=&labels=&template=bug_report.md&title=) and let us know about it. 183 | 184 | --- 185 | 186 | ### License 187 | This repository has been released under the [MIT License](https://github.com/kid1194/erpnext_expenses/blob/main/LICENSE). -------------------------------------------------------------------------------- /expenses/__init__.py: -------------------------------------------------------------------------------- 1 | # Expenses © 2024 2 | # Author: Ameen Ahmed 3 | # Company: Level Up Marketing & Software Development Services 4 | # Licence: Please refer to LICENSE file 5 | 6 | 7 | __module__ = "Expenses" 8 | __abbr__ = "EXP" 9 | __version__ = "1.0.0" 10 | __api__ = "https://api.github.com/repos/kid1194/erpnext_expenses/releases/latest" 11 | __production__ = False -------------------------------------------------------------------------------- /expenses/config/__init__.py: -------------------------------------------------------------------------------- 1 | # Expenses © 2024 2 | # Author: Ameen Ahmed 3 | # Company: Level Up Marketing & Software Development Services 4 | # Licence: Please refer to LICENSE file -------------------------------------------------------------------------------- /expenses/config/desktop.py: -------------------------------------------------------------------------------- 1 | # Expenses © 2024 2 | # Author: Ameen Ahmed 3 | # Company: Level Up Marketing & Software Development Services 4 | # Licence: Please refer to LICENSE file 5 | 6 | 7 | from frappe import _ 8 | 9 | 10 | def get_data(): 11 | return [ 12 | { 13 | "module_name": "Expenses", 14 | "category": "Modules", 15 | "color": "blue", 16 | "icon": "octicon octicon-note", 17 | "type": "module", 18 | "label": _("Expenses"), 19 | "description": _("An expenses management module for ERPNext") 20 | } 21 | ] -------------------------------------------------------------------------------- /expenses/config/docs.py: -------------------------------------------------------------------------------- 1 | # Expenses © 2024 2 | # Author: Ameen Ahmed 3 | # Company: Level Up Marketing & Software Development Services 4 | # Licence: Please refer to LICENSE file 5 | 6 | 7 | """ 8 | Configuration for docs 9 | """ 10 | 11 | 12 | def get_context(context): 13 | context.brand_html = "Expenses" 14 | -------------------------------------------------------------------------------- /expenses/expenses/__init__.py: -------------------------------------------------------------------------------- 1 | # Expenses © 2024 2 | # Author: Ameen Ahmed 3 | # Company: Level Up Marketing & Software Development Services 4 | # Licence: Please refer to LICENSE file -------------------------------------------------------------------------------- /expenses/expenses/doctype/__init__.py: -------------------------------------------------------------------------------- 1 | # Expenses © 2024 2 | # Author: Ameen Ahmed 3 | # Company: Level Up Marketing & Software Development Services 4 | # Licence: Please refer to LICENSE file -------------------------------------------------------------------------------- /expenses/expenses/doctype/expense/__init__.py: -------------------------------------------------------------------------------- 1 | # Expenses © 2024 2 | # Author: Ameen Ahmed 3 | # Company: Level Up Marketing & Software Development Services 4 | # Licence: Please refer to LICENSE file -------------------------------------------------------------------------------- /expenses/expenses/doctype/expense/expense_list.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Expenses © 2024 3 | * Author: Ameen Ahmed 4 | * Company: Level Up Marketing & Software Development Services 5 | * Licence: Please refer to LICENSE file 6 | */ 7 | 8 | 9 | frappe.provide('frappe.listview_settings'); 10 | 11 | 12 | frappe.listview_settings.Expense = { 13 | add_fields: ['party_type', 'party', 'is_paid', 'paid_by'], 14 | onload: function(list) { 15 | frappe.exp() 16 | .on('ready change', function() { this.setup_list(list); }) 17 | .on('on_alert', function(d, t) { 18 | ['fatal', 'error'].includes(t) && (d.title = __(list.doctype)); 19 | }); 20 | list.page.add_actions_menu_item( 21 | __('Create Request'), frappe.exp().$fn(function() { 22 | if (!this.is_enabled) return this.error(this.app_disabled_note); 23 | 24 | let data = list.get_checked_items(); 25 | if (!this.$isArrVal(data)) 26 | return this.error(__('Select at least one pending expense.')); 27 | 28 | let company = null, 29 | expenses = []; 30 | for (let i = 0, l = data.length, v, c; i < l; i++) { 31 | v = data[i]; 32 | if (cint(v.docstatus) !== 1 || cstr(v.status) !== 'pending') continue; 33 | c = cstr(v.company); 34 | if (!company) company = c; 35 | if (company === c) expenses.push(cstr(v.name)); 36 | } 37 | if (!company || !expenses.length) 38 | return this.error(__('Selected expenses must be pending and linked to one single company only.')); 39 | 40 | list.clear_checked_items(); 41 | this.cache().set('expenses-request', expenses, 60); 42 | frappe.new_doc('Expenses Request', {company: company}); 43 | }), 44 | true 45 | ); 46 | }, 47 | get_indicator: function(doc) { 48 | let opts = { 49 | Draft: ['gray', 0], 50 | Pending: ['orange', 1], 51 | Requested: ['blue', 1], 52 | Approved: ['green', 1], 53 | Rejected: ['red', 2], 54 | Cancelled: ['red', 2], 55 | }, 56 | status = cstr(doc.status); 57 | return [ 58 | __(status), opts[status][0], 59 | 'status,=,\'' + status + '\'|docstatus,=,' + opts[status][1] 60 | ]; 61 | }, 62 | formatters: { 63 | name: function(v, df, doc) { 64 | let html = []; 65 | if (frappe.exp().$isStrVal(doc.party_type) && frappe.exp().$isStrVal(doc.party)) 66 | html.push('' + __(doc.party_type) + ': ' + doc.party); 67 | if (frappe.exp().$isStrVal(doc.paid_by)) 68 | html.push('' + __('Paid By') + ': ' + doc.paid_by); 69 | if (html.length) v = cstr(v) + '
' + html.join(' | ') + ''; 70 | return v; 71 | }, 72 | is_advance: function(v) { return cint(v) ? __('Yes') : __('No'); }, 73 | }, 74 | }; -------------------------------------------------------------------------------- /expenses/expenses/doctype/expense_attachment/__init__.py: -------------------------------------------------------------------------------- 1 | # Expenses © 2024 2 | # Author: Ameen Ahmed 3 | # Company: Level Up Marketing & Software Development Services 4 | # Licence: Please refer to LICENSE file -------------------------------------------------------------------------------- /expenses/expenses/doctype/expense_attachment/expense_attachment.json: -------------------------------------------------------------------------------- 1 | { 2 | "allow_copy": 0, 3 | "allow_import": 1, 4 | "editable_grid": 1, 5 | "autoname": "hash", 6 | "creation": "2023-04-04 04:04:04", 7 | "description": "Expense attachment for Expenses module", 8 | "doctype": "DocType", 9 | "engine": "InnoDB", 10 | "field_order": [ 11 | "file", 12 | "main_column", 13 | "description", 14 | "expenses_entry_row_ref" 15 | ], 16 | "fields": [ 17 | { 18 | "fieldname": "file", 19 | "fieldtype": "Attach", 20 | "label": "File", 21 | "reqd": 1, 22 | "bold": 1, 23 | "in_list_view": 1, 24 | "columns": 4 25 | }, 26 | { 27 | "fieldname": "main_column", 28 | "fieldtype": "Column Break" 29 | }, 30 | { 31 | "fieldname": "description", 32 | "fieldtype": "Small Text", 33 | "label": "Description", 34 | "in_list_view": 1, 35 | "columns": 6 36 | }, 37 | { 38 | "fieldname": "expenses_entry_row_ref", 39 | "fieldtype": "Data", 40 | "label": "Expenses Entry Row Reference", 41 | "hidden": 1, 42 | "read_only": 1, 43 | "print_hide": 1, 44 | "report_hide": 1 45 | } 46 | ], 47 | "istable": 1, 48 | "modified": "2024-05-12 04:04:04", 49 | "modified_by": "Administrator", 50 | "module": "Expenses", 51 | "name": "Expense Attachment", 52 | "owner": "Administrator" 53 | } -------------------------------------------------------------------------------- /expenses/expenses/doctype/expense_attachment/expense_attachment.py: -------------------------------------------------------------------------------- 1 | # Expenses © 2024 2 | # Author: Ameen Ahmed 3 | # Company: Level Up Marketing & Software Development Services 4 | # Licence: Please refer to LICENSE file 5 | 6 | 7 | from frappe.model.document import Document 8 | 9 | 10 | class ExpenseAttachment(Document): 11 | pass -------------------------------------------------------------------------------- /expenses/expenses/doctype/expense_item/__init__.py: -------------------------------------------------------------------------------- 1 | # Expenses © 2024 2 | # Author: Ameen Ahmed 3 | # Company: Level Up Marketing & Software Development Services 4 | # Licence: Please refer to LICENSE file -------------------------------------------------------------------------------- /expenses/expenses/doctype/expense_item/expense_item.json: -------------------------------------------------------------------------------- 1 | { 2 | "allow_copy": 0, 3 | "allow_import": 1, 4 | "autoname": "Prompt", 5 | "creation": "2023-04-04 04:04:04", 6 | "description": "Expense item for Expenses module", 7 | "doctype": "DocType", 8 | "engine": "InnoDB", 9 | "field_order": [ 10 | "main_section", 11 | "disabled", 12 | "expense_type", 13 | "main_column", 14 | "uom", 15 | "accounts_section", 16 | "expense_accounts" 17 | ], 18 | "fields": [ 19 | { 20 | "fieldname": "main_section", 21 | "fieldtype": "Section Break" 22 | }, 23 | { 24 | "fieldname": "disabled", 25 | "fieldtype": "Check", 26 | "label": "Is Disabled", 27 | "default": "0", 28 | "depends_on": "eval:!doc.__islocal", 29 | "search_index": 1 30 | }, 31 | { 32 | "fieldname": "expense_type", 33 | "fieldtype": "Link", 34 | "label": "Expense Type", 35 | "options": "Expense Type", 36 | "reqd": 1, 37 | "bold": 1, 38 | "in_list_view": 1, 39 | "in_filter": 1, 40 | "in_standard_filter": 1, 41 | "in_preview": 1, 42 | "search_index": 1, 43 | "ignore_user_permissions": 1 44 | }, 45 | { 46 | "fieldname": "main_column", 47 | "fieldtype": "Column Break" 48 | }, 49 | { 50 | "fieldname": "uom", 51 | "fieldtype": "Link", 52 | "label": "Unit Of Measure", 53 | "options": "UOM", 54 | "reqd": 1, 55 | "bold": 1, 56 | "in_preview": 1, 57 | "ignore_user_permissions": 1 58 | }, 59 | { 60 | "fieldname": "accounts_section", 61 | "fieldtype": "Section Break" 62 | }, 63 | { 64 | "fieldname": "expense_accounts", 65 | "fieldtype": "Table", 66 | "label": "Expense Accounts & Defaults", 67 | "options": "Expense Item Account", 68 | "reqd": 1, 69 | "bold": 1 70 | } 71 | ], 72 | "icon": "fa fa-file-o", 73 | "modified": "2024-05-11 04:04:04", 74 | "modified_by": "Administrator", 75 | "module": "Expenses", 76 | "name": "Expense Item", 77 | "naming_rule": "Set by user", 78 | "owner": "Administrator", 79 | "permissions": [ 80 | { 81 | "amend": 1, 82 | "cancel": 1, 83 | "create": 1, 84 | "delete": 1, 85 | "email": 1, 86 | "export": 1, 87 | "if_owner": 0, 88 | "import": 1, 89 | "permlevel": 0, 90 | "print": 1, 91 | "read": 1, 92 | "report": 1, 93 | "role": "Expense Manager", 94 | "set_user_permissions": 1, 95 | "share": 1, 96 | "submit": 1, 97 | "write": 1 98 | }, 99 | { 100 | "amend": 1, 101 | "cancel": 1, 102 | "create": 1, 103 | "delete": 1, 104 | "email": 1, 105 | "export": 1, 106 | "if_owner": 0, 107 | "import": 1, 108 | "permlevel": 0, 109 | "print": 1, 110 | "read": 1, 111 | "report": 1, 112 | "role": "Accounts Manager", 113 | "set_user_permissions": 1, 114 | "share": 1, 115 | "submit": 1, 116 | "write": 1 117 | }, 118 | { 119 | "amend": 1, 120 | "cancel": 1, 121 | "create": 1, 122 | "delete": 1, 123 | "email": 1, 124 | "export": 1, 125 | "if_owner": 0, 126 | "import": 1, 127 | "permlevel": 0, 128 | "print": 1, 129 | "read": 1, 130 | "report": 1, 131 | "role": "System Manager", 132 | "set_user_permissions": 1, 133 | "share": 1, 134 | "submit": 1, 135 | "write": 1 136 | }, 137 | { 138 | "amend": 1, 139 | "cancel": 1, 140 | "create": 1, 141 | "delete": 1, 142 | "email": 1, 143 | "export": 1, 144 | "if_owner": 0, 145 | "import": 1, 146 | "permlevel": 0, 147 | "print": 1, 148 | "read": 1, 149 | "report": 1, 150 | "role": "Administrator", 151 | "set_user_permissions": 1, 152 | "share": 1, 153 | "submit": 1, 154 | "write": 1 155 | } 156 | ], 157 | "sort_field": "modified", 158 | "sort_order": "DESC", 159 | "show_name_in_global_search": 1, 160 | "translated_doctype": 1, 161 | "translate_link_fields": 1, 162 | "track_changes": 1 163 | } -------------------------------------------------------------------------------- /expenses/expenses/doctype/expense_item/expense_item_list.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Expenses © 2024 3 | * Author: Ameen Ahmed 4 | * Company: Level Up Marketing & Software Development Services 5 | * Licence: Please refer to LICENSE file 6 | */ 7 | 8 | 9 | frappe.provide('frappe.listview_settings'); 10 | 11 | 12 | frappe.listview_settings['Expense Item'] = { 13 | onload: function(list) { 14 | frappe.exp().on('ready change', function() { this.setup_list(list); }); 15 | }, 16 | get_indicator: function(doc) { 17 | return cint(doc.disabled) 18 | ? [__('Disabled'), 'red', 'disabled,=,1'] 19 | : [__('Enabled'), 'green', 'disabled,=,0']; 20 | }, 21 | }; -------------------------------------------------------------------------------- /expenses/expenses/doctype/expense_item_account/__init__.py: -------------------------------------------------------------------------------- 1 | # Expenses © 2024 2 | # Author: Ameen Ahmed 3 | # Company: Level Up Marketing & Software Development Services 4 | # Licence: Please refer to LICENSE file -------------------------------------------------------------------------------- /expenses/expenses/doctype/expense_item_account/expense_item_account.json: -------------------------------------------------------------------------------- 1 | { 2 | "allow_copy": 0, 3 | "allow_import": 1, 4 | "editable_grid": 1, 5 | "autoname": "hash", 6 | "creation": "2023-04-04 04:04:04", 7 | "description": "Expense item company and expense account for Expenses module", 8 | "doctype": "DocType", 9 | "engine": "InnoDB", 10 | "field_order": [ 11 | "main_section", 12 | "company", 13 | "main_column", 14 | "account", 15 | "currency", 16 | "expense_section", 17 | "cost", 18 | "min_cost", 19 | "max_cost", 20 | "expense_column", 21 | "qty", 22 | "min_qty", 23 | "max_qty", 24 | "inherited" 25 | ], 26 | "fields": [ 27 | { 28 | "fieldname": "main_section", 29 | "fieldtype": "Section Break" 30 | }, 31 | { 32 | "fieldname": "company", 33 | "fieldtype": "Link", 34 | "label": "Company", 35 | "options": "Company", 36 | "reqd": 1, 37 | "bold": 1, 38 | "read_only_depends_on": "eval:doc.inherited", 39 | "in_list_view": 1, 40 | "ignore_user_permissions": 1 41 | }, 42 | { 43 | "fieldname": "main_column", 44 | "fieldtype": "Column Break" 45 | }, 46 | { 47 | "fieldname": "account", 48 | "fieldtype": "Link", 49 | "label": "Expense Account", 50 | "options": "Account", 51 | "reqd": 1, 52 | "bold": 1, 53 | "read_only_depends_on": "eval:!doc.company || doc.inherited", 54 | "in_list_view": 1, 55 | "ignore_user_permissions": 1 56 | }, 57 | { 58 | "fieldname": "currency", 59 | "fieldtype": "Read Only", 60 | "label": "Currency", 61 | "fetch_from": "account.account_currency", 62 | "hidden": 1, 63 | "read_only": 1, 64 | "print_hide": 1, 65 | "report_hide": 1, 66 | "ignore_user_permissions": 1 67 | }, 68 | { 69 | "fieldname": "expense_section", 70 | "fieldtype": "Section Break" 71 | }, 72 | { 73 | "fieldname": "cost", 74 | "fieldtype": "Currency", 75 | "label": "Cost", 76 | "description": "Use to set a fixed expense cost.", 77 | "options": "currency", 78 | "default": "0", 79 | "non_negative": 1, 80 | "read_only_depends_on": "eval:!doc.account || doc.min_cost > 0 || doc.max_cost > 0", 81 | "in_list_view": 1 82 | }, 83 | { 84 | "fieldname": "min_cost", 85 | "fieldtype": "Currency", 86 | "label": "Minimum Cost", 87 | "description": "Use to set a minimum expense cost but will be ignored if a fixed expense cost is set.", 88 | "options": "currency", 89 | "default": "0", 90 | "non_negative": 1, 91 | "read_only_depends_on": "eval:!doc.account || doc.cost > 0" 92 | }, 93 | { 94 | "fieldname": "max_cost", 95 | "fieldtype": "Currency", 96 | "label": "Maximum Cost", 97 | "description": "Use to set a maximum expense cost but will be ignored if a fixed expense cost is set.", 98 | "options": "currency", 99 | "default": "0", 100 | "non_negative": 1, 101 | "read_only_depends_on": "eval:!doc.account || doc.cost > 0" 102 | }, 103 | { 104 | "fieldname": "expense_column", 105 | "fieldtype": "Column Break" 106 | }, 107 | { 108 | "fieldname": "qty", 109 | "fieldtype": "Float", 110 | "label": "Quantity", 111 | "description": "Use to set a fixed expense quantity.", 112 | "default": "0", 113 | "precision": "4", 114 | "non_negative": 1, 115 | "read_only_depends_on": "eval:doc.min_qty > 0 || doc.max_qty > 0", 116 | "in_list_view": 1 117 | }, 118 | { 119 | "fieldname": "min_qty", 120 | "fieldtype": "Float", 121 | "label": "Minimum Quantity", 122 | "description": "Use to set a minimum expense quantity but will be ignored if a fixed expense quantity is set.", 123 | "default": "0", 124 | "precision": "4", 125 | "non_negative": 1, 126 | "read_only_depends_on": "eval:doc.qty > 0" 127 | }, 128 | { 129 | "fieldname": "max_qty", 130 | "fieldtype": "Float", 131 | "label": "Maximum Quantity", 132 | "description": "Use to set a maximum expense quantity but will be ignored if a fixed expense quantity is set.", 133 | "default": "0", 134 | "precision": "4", 135 | "non_negative": 1, 136 | "read_only_depends_on": "eval:doc.qty > 0" 137 | }, 138 | { 139 | "fieldname": "inherited", 140 | "fieldtype": "Check", 141 | "label": "Inherited", 142 | "default": "0", 143 | "hidden": 1, 144 | "read_only": 1, 145 | "print_hide": 1, 146 | "report_hide": 1 147 | } 148 | ], 149 | "istable": 1, 150 | "modified": "2024-05-12 04:04:04", 151 | "modified_by": "Administrator", 152 | "module": "Expenses", 153 | "name": "Expense Item Account", 154 | "owner": "Administrator" 155 | } -------------------------------------------------------------------------------- /expenses/expenses/doctype/expense_item_account/expense_item_account.py: -------------------------------------------------------------------------------- 1 | # Expenses © 2024 2 | # Author: Ameen Ahmed 3 | # Company: Level Up Marketing & Software Development Services 4 | # Licence: Please refer to LICENSE file 5 | 6 | 7 | from frappe.model.document import Document 8 | 9 | 10 | class ExpenseItemAccount(Document): 11 | pass -------------------------------------------------------------------------------- /expenses/expenses/doctype/expense_type/__init__.py: -------------------------------------------------------------------------------- 1 | # Expenses © 2024 2 | # Author: Ameen Ahmed 3 | # Company: Level Up Marketing & Software Development Services 4 | # Licence: Please refer to LICENSE file -------------------------------------------------------------------------------- /expenses/expenses/doctype/expense_type/expense_type.json: -------------------------------------------------------------------------------- 1 | { 2 | "allow_copy": 0, 3 | "allow_import": 1, 4 | "autoname": "Prompt", 5 | "creation": "2023-04-04 04:04:04", 6 | "description": "Expense type for Expenses module", 7 | "doctype": "DocType", 8 | "engine": "InnoDB", 9 | "field_order": [ 10 | "main_section", 11 | "disabled", 12 | "is_group", 13 | "main_column", 14 | "parent_type", 15 | "accounts_section", 16 | "expense_accounts", 17 | "lft", 18 | "rgt" 19 | ], 20 | "fields": [ 21 | { 22 | "fieldname": "main_section", 23 | "fieldtype": "Section Break" 24 | }, 25 | { 26 | "fieldname": "disabled", 27 | "fieldtype": "Check", 28 | "label": "Is Disabled", 29 | "depends_on": "eval:!doc.__islocal", 30 | "search_index": 1 31 | }, 32 | { 33 | "fieldname": "is_group", 34 | "fieldtype": "Check", 35 | "label": "Is Group", 36 | "description": "Type items can only be created under Groups and Expense Items can only belong to type items.", 37 | "set_once": 1, 38 | "in_list_view": 1, 39 | "search_index": 1 40 | }, 41 | { 42 | "fieldname": "main_column", 43 | "fieldtype": "Column Break" 44 | }, 45 | { 46 | "fieldname": "parent_type", 47 | "fieldtype": "Link", 48 | "label": "Parent Type", 49 | "options": "Expense Type", 50 | "mandatory_depends_on": "eval:!doc.is_group", 51 | "in_list_view": 1, 52 | "in_filter": 1, 53 | "in_standard_filter": 1, 54 | "in_preview": 1, 55 | "ignore_user_permissions": 1, 56 | "search_index": 1 57 | }, 58 | { 59 | "fieldname": "accounts_section", 60 | "fieldtype": "Section Break" 61 | }, 62 | { 63 | "fieldname": "expense_accounts", 64 | "fieldtype": "Table", 65 | "label": "Expense Accounts", 66 | "options": "Expense Type Account" 67 | }, 68 | { 69 | "fieldname": "old_parent_type", 70 | "fieldtype": "Link", 71 | "label": "Old Parent Type", 72 | "options": "Expense Type", 73 | "hidden": 1, 74 | "read_only": 1, 75 | "print_hide": 1, 76 | "report_hide": 1, 77 | "search_index": 1 78 | }, 79 | { 80 | "fieldname": "lft", 81 | "fieldtype": "Int", 82 | "label": "Lft", 83 | "hidden": 1, 84 | "read_only": 1, 85 | "print_hide": 1, 86 | "report_hide": 1, 87 | "search_index": 1 88 | }, 89 | { 90 | "fieldname": "rgt", 91 | "fieldtype": "Int", 92 | "label": "Rgt", 93 | "hidden": 1, 94 | "read_only": 1, 95 | "print_hide": 1, 96 | "report_hide": 1, 97 | "search_index": 1 98 | } 99 | ], 100 | "icon": "fa fa-folder-o", 101 | "is_tree": 1, 102 | "modified": "2024-05-23 04:04:04", 103 | "modified_by": "Administrator", 104 | "module": "Expenses", 105 | "name": "Expense Type", 106 | "nsm_parent_field": "parent_type", 107 | "naming_rule": "Set by user", 108 | "owner": "Administrator", 109 | "permissions": [ 110 | { 111 | "amend": 1, 112 | "cancel": 1, 113 | "create": 1, 114 | "delete": 1, 115 | "email": 1, 116 | "export": 1, 117 | "if_owner": 0, 118 | "import": 1, 119 | "permlevel": 0, 120 | "print": 1, 121 | "read": 1, 122 | "report": 1, 123 | "role": "Expense Manager", 124 | "set_user_permissions": 1, 125 | "share": 1, 126 | "submit": 1, 127 | "write": 1 128 | }, 129 | { 130 | "amend": 1, 131 | "cancel": 1, 132 | "create": 1, 133 | "delete": 1, 134 | "email": 1, 135 | "export": 1, 136 | "if_owner": 0, 137 | "import": 1, 138 | "permlevel": 0, 139 | "print": 1, 140 | "read": 1, 141 | "report": 1, 142 | "role": "Accounts Manager", 143 | "set_user_permissions": 1, 144 | "share": 1, 145 | "submit": 1, 146 | "write": 1 147 | }, 148 | { 149 | "amend": 1, 150 | "cancel": 1, 151 | "create": 1, 152 | "delete": 1, 153 | "email": 1, 154 | "export": 1, 155 | "if_owner": 0, 156 | "import": 1, 157 | "permlevel": 0, 158 | "print": 1, 159 | "read": 1, 160 | "report": 1, 161 | "role": "System Manager", 162 | "set_user_permissions": 1, 163 | "share": 1, 164 | "submit": 1, 165 | "write": 1 166 | }, 167 | { 168 | "amend": 1, 169 | "cancel": 1, 170 | "create": 1, 171 | "delete": 1, 172 | "email": 1, 173 | "export": 1, 174 | "if_owner": 0, 175 | "import": 1, 176 | "permlevel": 0, 177 | "print": 1, 178 | "read": 1, 179 | "report": 1, 180 | "role": "Administrator", 181 | "set_user_permissions": 1, 182 | "share": 1, 183 | "submit": 1, 184 | "write": 1 185 | } 186 | ], 187 | "sort_field": "modified", 188 | "sort_order": "DESC", 189 | "translated_doctype": 1, 190 | "translate_link_fields": 1, 191 | "track_changes": 1 192 | } -------------------------------------------------------------------------------- /expenses/expenses/doctype/expense_type/expense_type.py: -------------------------------------------------------------------------------- 1 | # Expenses © 2024 2 | # Author: Ameen Ahmed 3 | # Company: Level Up Marketing & Software Development Services 4 | # Licence: Please refer to LICENSE file 5 | 6 | 7 | import frappe 8 | from frappe import _ 9 | from frappe.utils import cint 10 | from frappe.utils.nestedset import NestedSet 11 | 12 | from expenses.libs import clear_doc_cache 13 | 14 | 15 | class ExpenseType(NestedSet): 16 | nsm_parent_field = "parent_type" 17 | nsm_oldparent_field = "old_parent_type" 18 | 19 | 20 | def validate(self): 21 | self._check_app_status() 22 | self.flags.error_list = [] 23 | self._validate_name() 24 | self._validate_type() 25 | self._validate_parent() 26 | self._validate_accounts() 27 | if self.flags.error_list: 28 | self._error(self.flags.error_list) 29 | 30 | 31 | def before_rename(self, olddn, newdn, merge=False): 32 | self._check_app_status() 33 | 34 | super(ExpenseType, self).before_rename(olddn, newdn, merge) 35 | 36 | clear_doc_cache(self.doctype, olddn) 37 | self._clean_flags() 38 | 39 | 40 | def before_save(self): 41 | clear_doc_cache(self.doctype, self.name) 42 | if ( 43 | not self.is_new() and 44 | self.has_value_changed("disabled") and 45 | self._is_disabled and self._is_group 46 | ): 47 | self.flags.disable_descendants = 1 48 | 49 | 50 | def on_update(self): 51 | if self.flags.pop("disable_descendants", 0): 52 | from expenses.libs import disable_type_descendants 53 | 54 | disable_type_descendants(self.lft, self.rgt) 55 | 56 | if self.flags.pop("reload_linked_items", 0): 57 | from expenses.libs import reload_type_linked_items 58 | 59 | reload_type_linked_items(self.lft, self.rgt) 60 | 61 | self._clean_flags() 62 | if not frappe.local.flags.ignore_update_nsm: 63 | super(ExpenseType, self).on_update() 64 | 65 | 66 | def on_trash(self): 67 | self._check_app_status() 68 | if self._is_group: 69 | from expenses.libs import type_children_exists 70 | 71 | if type_children_exists(self.name): 72 | self._error(_("Expense type group with existing child items can't be removed.")) 73 | else: 74 | from expenses.libs import type_items_exists 75 | 76 | if type_items_exists(self.name): 77 | self._error(_("Expense type with linked expense items can't be removed.")) 78 | 79 | super(ExpenseType, self).on_trash(True) 80 | 81 | 82 | def after_delete(self): 83 | self._clean_flags() 84 | clear_doc_cache(self.doctype, self.name) 85 | 86 | 87 | def convert_to_item(self, parent_type=None): 88 | if not self._is_group: 89 | return {"success": 1} 90 | 91 | if not parent_type: 92 | if not self.parent_type: 93 | return {"error": _("A valid parent expense type is required.")} 94 | 95 | from expenses.libs import type_children_exists 96 | 97 | if type_children_exists(self.name): 98 | return {"error": _("Expense type group with existing child items can't be converted to an item.")} 99 | 100 | if parent_type and self.parent_type != parent_type: 101 | error = self._check_parent(parent_type, False) 102 | if error: 103 | return error 104 | 105 | self.parent_type = parent_type 106 | 107 | self.is_group = 0 108 | self.save(ignore_permissions=True) 109 | return {"success": 1} 110 | 111 | 112 | def convert_to_group(self): 113 | if self._is_group: 114 | return {"success": 1} 115 | 116 | from expenses.libs import type_items_exists 117 | 118 | if type_items_exists(self.name): 119 | return {"error": _("Expense type with linked expense items can't be converted to a group.")} 120 | 121 | self.is_group = 1 122 | self.save(ignore_permissions=True) 123 | return {"success": 1} 124 | 125 | 126 | @property 127 | def _is_disabled(self): 128 | return cint(self.disabled) > 0 129 | 130 | 131 | @property 132 | def _is_group(self): 133 | return cint(self.is_group) > 0 134 | 135 | 136 | def _validate_name(self): 137 | if not self.name: 138 | self._error(_("A valid name is required.")) 139 | 140 | 141 | def _validate_type(self): 142 | if not self.is_new() and self.has_value_changed("is_group"): 143 | self._error(_("Set once fields can't be changed.")) 144 | 145 | 146 | def _validate_parent(self): 147 | if not self._is_group and not self.parent_type: 148 | self._add_error(_("A valid parent expense type is required.")) 149 | elif self.parent_type and (self.is_new() or self.has_value_changed("parent_type")): 150 | self._check_parent(self.parent_type, True) 151 | 152 | 153 | def _validate_accounts(self): 154 | if not self.expense_accounts or (not self.is_new() and not self.has_value_changed("expense_accounts")): 155 | return 0 156 | 157 | table = _("Expense Accounts") 158 | ext = {"company": [], "account": []} 159 | fil = {"companies": [], "accounts": []} 160 | for v in self.expense_accounts: 161 | if v.company: 162 | fil["companies"].append(v.company) 163 | if v.account: 164 | fil["accounts"].append(v.account) 165 | 166 | if fil["companies"]: 167 | from expenses.libs import companies_filter 168 | 169 | fil["companies"] = companies_filter(fil["companies"], {"is_group": 0}) 170 | 171 | if fil["accounts"]: 172 | from expenses.libs import company_accounts_filter 173 | 174 | fil["accounts"] = company_accounts_filter(fil["accounts"]) 175 | 176 | for i, v in enumerate(self.expense_accounts): 177 | if not v.company: 178 | self._add_error(_("{0} - #{1}: A valid company is required.").format(table, i)) 179 | continue 180 | if v.company in ext["company"]: 181 | self._add_error(_("{0} - #{1}: Company \"{2}\" already exist.").format(table, i, v.company)) 182 | continue 183 | if v.company not in fil["companies"]: 184 | self._add_error(_("{0} - #{1}: Company \"{2}\" is a group or doesn't exist.").format(table, i, v.company)) 185 | continue 186 | ext["company"].append(v.company) 187 | if not v.account: 188 | self._add_error(_("{0} - #{1}: A valid expense account is required.").format(table, i)) 189 | continue 190 | if v.account in ext["account"]: 191 | self._add_error(_("{0} - #{1}: Expense account \"{2}\" already exist.").format(table, i, v.account)) 192 | continue 193 | if v.account not in fil["accounts"] or v.company != fil["accounts"][v.account]: 194 | self._add_error( 195 | _("{0} - #{1}: Expense account \"{2}\" isn't linked to company \"{3}\" or doesn't exist.") 196 | .format(table, i, v.account, v.company) 197 | ) 198 | continue 199 | ext["account"].append(v.account) 200 | 201 | ext.clear() 202 | fil.clear() 203 | if not self.is_new() and not self.flags.error_list: 204 | self.flags.reload_linked_items = 1 205 | 206 | 207 | def _check_parent(self, parent_type, _throw): 208 | if parent_type == self.name: 209 | err = _("Expense type can't be its own parent.") 210 | if _throw: 211 | self._add_error(err) 212 | return {"error": err} if not _throw else 0 213 | 214 | from expenses.libs import type_exists 215 | 216 | if not type_exists(parent_type, {"is_group": 1}): 217 | err = _("Parent expense type \"{0}\" isn't a group or doesn't exist.").format(parent_type) 218 | if _throw: 219 | self._add_error(err) 220 | return {"error": err} if not _throw else 0 221 | 222 | 223 | def _check_app_status(self): 224 | if not self.flags.get("status_checked", 0): 225 | from expenses.libs import check_app_status 226 | 227 | check_app_status() 228 | self.flags.status_checked = 1 229 | 230 | 231 | def _clean_flags(self): 232 | keys = [ 233 | "error_list", 234 | "disable_descendants", 235 | "reload_linked_items", 236 | "status_checked" 237 | ] 238 | for i in range(len(keys)): 239 | self.flags.pop(keys.pop(0), 0) 240 | 241 | 242 | def _add_error(self, msg): 243 | self.flags.error_list.append(msg) 244 | 245 | 246 | def _error(self, msg): 247 | from expenses.libs import error 248 | 249 | if isinstance(msg, list): 250 | if len(msg) == 1: 251 | msg = msg.pop(0) 252 | else: 253 | msg = msg.copy() 254 | 255 | self._clean_flags() 256 | error(msg, _(self.doctype)) -------------------------------------------------------------------------------- /expenses/expenses/doctype/expense_type/expense_type_list.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Expenses © 2024 3 | * Author: Ameen Ahmed 4 | * Company: Level Up Marketing & Software Development Services 5 | * Licence: Please refer to LICENSE file 6 | */ 7 | 8 | 9 | frappe.provide('frappe.listview_settings'); 10 | 11 | 12 | frappe.listview_settings['Expense Type'] = { 13 | onload: function(list) { 14 | frappe.exp().on('ready change', function() { this.setup_list(list); }); 15 | }, 16 | get_indicator: function(doc) { 17 | return cint(doc.disabled) 18 | ? [__('Disabled'), 'red', 'disabled,=,1'] 19 | : [__('Enabled'), 'green', 'disabled,=,0']; 20 | }, 21 | formatters: { 22 | parent_type: function(v) { return frappe.exp().$isStrVal(v) ? v : __('Root'); }, 23 | is_group: function(v) { return __(cint(v) ? 'Yes' : 'No'); }, 24 | }, 25 | }; -------------------------------------------------------------------------------- /expenses/expenses/doctype/expense_type/expense_type_tree.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Expenses © 2024 3 | * Author: Ameen Ahmed 4 | * Company: Level Up Marketing & Software Development Services 5 | * Licence: Please refer to LICENSE file 6 | */ 7 | 8 | 9 | frappe.provide('frappe.treeview_settings'); 10 | 11 | 12 | frappe.treeview_settings['Expense Type'] = { 13 | breadcrumb: __('Expense Types'), 14 | title: __('Chart of Expense Types'), 15 | get_tree_root: true, 16 | disable_add_node: true, 17 | root_label: __('Expense Types'), 18 | show_expand_all: true, 19 | get_tree_nodes: frappe.exp().get_method('get_type_children'), 20 | onload: function(treeview) { 21 | frappe.exp().on('ready change', function() { this.setup_tree(treeview); }); 22 | }, 23 | post_render: function(treeview) { 24 | treeview.page.clear_primary_action(); 25 | if (!frappe.exp().is_enabled) return; 26 | treeview.page.set_primary_action(__('New'), function() { 27 | frappe.new_doc(frappe.exp().tree.doctype, {from_tree: 1}); 28 | }, 'add'); 29 | }, 30 | toolbar: [ 31 | { 32 | label: __('Add Child'), 33 | condition: function(node) { 34 | return frappe.exp().is_enabled 35 | && frappe.boot.user.can_create.includes('Expense Type') 36 | && !node.hide_add 37 | && node.expandable; 38 | }, 39 | click: function() { 40 | if (!frappe.exp().is_enabled) return; 41 | frappe.new_doc(frappe.exp().tree.doctype, { 42 | from_tree: 1, 43 | is_group: cint(frappe.exp().tree.args.expandable), 44 | parent_type: cstr(frappe.exp().tree.args.parent), 45 | }); 46 | }, 47 | btnClass: 'hidden-xs' 48 | }, 49 | { 50 | label: __('Convert To Group'), 51 | condition: function(node) { 52 | return frappe.exp().is_enabled 53 | && frappe.boot.user.can_write.includes('Expense Type') 54 | && !node.expandable; 55 | }, 56 | click: function() { 57 | if (!frappe.exp().is_enabled) return; 58 | frappe.exp().request( 59 | 'convert_item_to_group', 60 | {name: cstr(frappe.exp().tree.args.value)}, 61 | function(ret) { 62 | if (!ret) this.error_(__('Unable to convert expense type item to a group.')); 63 | else if (ret.error) this.error_(ret.error); 64 | else { 65 | this.success_(__('Expense type converted successfully.')); 66 | this.tree.make_tree(); 67 | } 68 | }, 69 | function(e) { 70 | this.error_(e.self ? e.message : __('Unable to convert expense type item to a group.')); 71 | } 72 | ); 73 | }, 74 | btnClass: 'hidden-xs' 75 | }, 76 | { 77 | label: __('Convert To Item'), 78 | condition: function(node) { 79 | return frappe.exp().is_enabled 80 | && frappe.boot.user.can_write.includes('Expense Type') 81 | && node.expandable; 82 | }, 83 | click: function() { 84 | if (!frappe.exp().is_enabled) return; 85 | frappe.exp().request( 86 | 'convert_group_to_item', 87 | {name: cstr(frappe.exp().tree.args.value)}, 88 | function(ret) { 89 | if (!ret) this.error_(__('Unable to convert expense type group to an item.')); 90 | else if (ret.error) this.error_(ret.error); 91 | else { 92 | this.success_(__('Expense type converted successfully.')); 93 | this.tree.make_tree(); 94 | } 95 | }, 96 | function(e) { 97 | this.error_(e.self ? e.message : __('Unable to convert expense type item to a group.')); 98 | } 99 | ); 100 | }, 101 | btnClass: 'hidden-xs' 102 | }, 103 | ], 104 | extend_toolbar: true 105 | }; -------------------------------------------------------------------------------- /expenses/expenses/doctype/expense_type_account/__init__.py: -------------------------------------------------------------------------------- 1 | # Expenses © 2024 2 | # Author: Ameen Ahmed 3 | # Company: Level Up Marketing & Software Development Services 4 | # Licence: Please refer to LICENSE file -------------------------------------------------------------------------------- /expenses/expenses/doctype/expense_type_account/expense_type_account.json: -------------------------------------------------------------------------------- 1 | { 2 | "allow_copy": 0, 3 | "allow_import": 1, 4 | "editable_grid": 1, 5 | "autoname": "hash", 6 | "creation": "2023-04-04 04:04:04", 7 | "description": "Expense type company and expense account for Expenses module", 8 | "doctype": "DocType", 9 | "engine": "InnoDB", 10 | "field_order": [ 11 | "company", 12 | "main_column", 13 | "account" 14 | ], 15 | "fields": [ 16 | { 17 | "fieldname": "company", 18 | "fieldtype": "Link", 19 | "label": "Company", 20 | "options": "Company", 21 | "reqd": 1, 22 | "bold": 1, 23 | "in_list_view": 1, 24 | "ignore_user_permissions": 1 25 | }, 26 | { 27 | "fieldname": "main_column", 28 | "fieldtype": "Column Break" 29 | }, 30 | { 31 | "fieldname": "account", 32 | "fieldtype": "Link", 33 | "label": "Expense Account", 34 | "options": "Account", 35 | "reqd": 1, 36 | "bold": 1, 37 | "read_only_depends_on": "eval:!doc.company", 38 | "in_list_view": 1, 39 | "ignore_user_permissions": 1 40 | } 41 | ], 42 | "istable": 1, 43 | "modified": "2024-05-12 04:04:04", 44 | "modified_by": "Administrator", 45 | "module": "Expenses", 46 | "name": "Expense Type Account", 47 | "owner": "Administrator" 48 | } -------------------------------------------------------------------------------- /expenses/expenses/doctype/expense_type_account/expense_type_account.py: -------------------------------------------------------------------------------- 1 | # Expenses © 2024 2 | # Author: Ameen Ahmed 3 | # Company: Level Up Marketing & Software Development Services 4 | # Licence: Please refer to LICENSE file 5 | 6 | 7 | from frappe.model.document import Document 8 | 9 | 10 | class ExpenseTypeAccount(Document): 11 | pass -------------------------------------------------------------------------------- /expenses/expenses/doctype/expenses_entry/__init__.py: -------------------------------------------------------------------------------- 1 | # Expenses © 2024 2 | # Author: Ameen Ahmed 3 | # Company: Level Up Marketing & Software Development Services 4 | # Licence: Please refer to LICENSE file -------------------------------------------------------------------------------- /expenses/expenses/doctype/expenses_entry/expenses_entry.json: -------------------------------------------------------------------------------- 1 | { 2 | "allow_copy": 1, 3 | "allow_import": 1, 4 | "autoname": "format:EXP-E-{YYYY}-{#####}", 5 | "creation": "2023-04-04 04:04:04", 6 | "description": "Expenses Entry for Expenses module", 7 | "doctype": "DocType", 8 | "engine": "InnoDB", 9 | "field_order": [ 10 | "main_section", 11 | "company", 12 | "mode_of_payment", 13 | "main_column", 14 | "posting_date", 15 | "custom_exchange_rate", 16 | "dimensions_section", 17 | "default_project", 18 | "dimensions_column", 19 | "default_cost_center", 20 | "expenses_section", 21 | "expenses", 22 | "remarks", 23 | "total_section", 24 | "payment_account", 25 | "payment_target", 26 | "payment_currency", 27 | "total_in_payment_currency", 28 | "total_section", 29 | "exchange_rate", 30 | "total", 31 | "info_section", 32 | "payment_reference", 33 | "info_column", 34 | "clearance_date", 35 | "attachments_section", 36 | "attachments", 37 | "amended_from", 38 | "amendment_date", 39 | "expenses_request_ref" 40 | ], 41 | "fields": [ 42 | { 43 | "fieldname": "main_section", 44 | "fieldtype": "Section Break" 45 | }, 46 | { 47 | "fieldname": "company", 48 | "fieldtype": "Link", 49 | "label": "Company", 50 | "options": "Company", 51 | "default": ":Company", 52 | "reqd": 1, 53 | "bold": 1, 54 | "remember_last_selected_value": 1, 55 | "in_list_view": 1, 56 | "in_filter": 1, 57 | "in_standard_filter": 1, 58 | "in_preview": 1, 59 | "ignore_user_permissions": 1 60 | }, 61 | { 62 | "fieldname": "mode_of_payment", 63 | "fieldtype": "Link", 64 | "label": "Mode of Payment", 65 | "options": "Mode of Payment", 66 | "reqd": 1, 67 | "bold": 1, 68 | "read_only_depends_on": "eval:!doc.company", 69 | "remember_last_selected_value": 1, 70 | "in_list_view": 1, 71 | "in_filter": 1, 72 | "in_standard_filter": 1, 73 | "in_preview": 1, 74 | "ignore_user_permissions": 1 75 | }, 76 | { 77 | "fieldname": "main_column", 78 | "fieldtype": "Column Break" 79 | }, 80 | { 81 | "fieldname": "posting_date", 82 | "fieldtype": "Date", 83 | "label": "Posting Date", 84 | "default": "Today", 85 | "read_only": 1, 86 | "in_list_view": 1, 87 | "in_filter": 1, 88 | "in_standard_filter": 1, 89 | "in_preview": 1, 90 | "search_index": 1 91 | }, 92 | { 93 | "fieldname": "custom_exchange_rate", 94 | "fieldtype": "Check", 95 | "label": "Custom Exchange Rate", 96 | "default": "0" 97 | }, 98 | { 99 | "fieldname": "dimensions_section", 100 | "fieldtype": "Section Break", 101 | "label": "Accounting Dimensions", 102 | "depends_on": "eval:doc.company", 103 | "collapsible": 1 104 | }, 105 | { 106 | "fieldname": "default_project", 107 | "fieldtype": "Link", 108 | "label": "Default Project", 109 | "description": "Use only if expenses are project related. Applies to all listed expenses, unless specified differently.", 110 | "options": "Project", 111 | "in_preview": 1, 112 | "remember_last_selected_value": 1 113 | }, 114 | { 115 | "fieldname": "dimensions_column", 116 | "fieldtype": "Column Break" 117 | }, 118 | { 119 | "fieldname": "default_cost_center", 120 | "fieldtype": "Link", 121 | "label": "Default Cost Center", 122 | "description": "Applies to all listed expenses, unless specified differently.", 123 | "options": "Cost Center", 124 | "default": ":Company", 125 | "in_preview": 1, 126 | "remember_last_selected_value": 1 127 | }, 128 | { 129 | "fieldname": "expenses_section", 130 | "fieldtype": "Section Break" 131 | }, 132 | { 133 | "fieldname": "expenses", 134 | "fieldtype": "Table", 135 | "label": "Expenses", 136 | "options": "Expenses Entry Details", 137 | "read_only_depends_on": "eval:doc.expenses_request_ref", 138 | "mandatory_depends_on": "eval:!doc.expenses_request_ref", 139 | "bold": 1, 140 | "in_preview": 1 141 | }, 142 | { 143 | "fieldname": "remarks", 144 | "fieldtype": "Text", 145 | "label": "Remarks", 146 | "in_preview": 1 147 | }, 148 | { 149 | "fieldname": "total_section", 150 | "fieldtype": "Section Break" 151 | }, 152 | { 153 | "fieldname": "payment_account", 154 | "fieldtype": "Link", 155 | "label": "Payment Account", 156 | "options": "Account", 157 | "hidden": 1, 158 | "print_hide": 1, 159 | "report_hide": 1 160 | }, 161 | { 162 | "fieldname": "payment_target", 163 | "fieldtype": "Data", 164 | "label": "Payment Target", 165 | "fetch_from": "mode_of_payment.type", 166 | "hidden": 1, 167 | "print_hide": 1, 168 | "report_hide": 1 169 | }, 170 | { 171 | "fieldname": "payment_currency", 172 | "fieldtype": "Link", 173 | "label": "Currency", 174 | "options": "Currency", 175 | "fetch_from": "payment_account.account_currency", 176 | "read_only": 1, 177 | "print_hide": 1, 178 | "report_hide": 1 179 | }, 180 | { 181 | "fieldname": "total_in_payment_currency", 182 | "fieldtype": "Currency", 183 | "label": "Total", 184 | "options": "payment_currency", 185 | "bold": 1, 186 | "non_negative": 1, 187 | "read_only": 1, 188 | "in_list_view": 1, 189 | "in_preview": 1 190 | }, 191 | { 192 | "fieldname": "total_column", 193 | "fieldtype": "Column Break" 194 | }, 195 | { 196 | "fieldname": "exchange_rate", 197 | "fieldtype": "Float", 198 | "label": "Exchange Rate", 199 | "default": "1", 200 | "precision": "9", 201 | "non_negative": 1, 202 | "read_only": 1, 203 | "in_preview": 1 204 | }, 205 | { 206 | "fieldname": "total", 207 | "fieldtype": "Currency", 208 | "label": "Total (Company Currency)", 209 | "options": "Company:company:default_currency", 210 | "bold": 1, 211 | "non_negative": 1, 212 | "read_only": 1, 213 | "print_hide": 1 214 | }, 215 | { 216 | "fieldname": "info_section", 217 | "fieldtype": "Section Break", 218 | "depends_on": "eval:doc.payment_target === 'Bank'" 219 | }, 220 | { 221 | "fieldname": "payment_reference", 222 | "fieldtype": "Data", 223 | "label": "Payment Reference", 224 | "bold": 1, 225 | "mandatory_depends_on": "eval:doc.payment_target == 'Bank'", 226 | "in_list_view": 1, 227 | "in_preview": 1 228 | }, 229 | { 230 | "fieldname": "info_column", 231 | "fieldtype": "Column Break" 232 | }, 233 | { 234 | "fieldname": "clearance_date", 235 | "fieldtype": "Date", 236 | "label": "Reference / Clearance Date", 237 | "bold": 1, 238 | "mandatory_depends_on": "eval:doc.payment_target == 'Bank'", 239 | "in_list_view": 1, 240 | "in_preview": 1 241 | }, 242 | { 243 | "fieldname": "attachments_section", 244 | "fieldtype": "Section Break", 245 | "label": "Expenses Attachments", 246 | "collapsible": 1 247 | }, 248 | { 249 | "fieldname": "attachments", 250 | "fieldtype": "Table", 251 | "label": "Attachments", 252 | "options": "Expense Attachment" 253 | }, 254 | { 255 | "fieldname": "amended_from", 256 | "fieldtype": "Link", 257 | "label": "Amended From", 258 | "options": "Expenses Entry", 259 | "read_only": 1, 260 | "hidden": 1, 261 | "allow_on_submit": 1, 262 | "ignore_user_permissions": 1 263 | }, 264 | { 265 | "fieldname": "amendment_date", 266 | "fieldtype": "Date", 267 | "label": "Amendment Date", 268 | "read_only": 1, 269 | "hidden": 1, 270 | "allow_on_submit": 1 271 | }, 272 | { 273 | "fieldname": "expenses_request_ref", 274 | "fieldtype": "Link", 275 | "label": "Expenses Request Reference", 276 | "options": "Expenses Request", 277 | "read_only": 1, 278 | "hidden": 1 279 | } 280 | ], 281 | "icon": "fa fa-file-text", 282 | "is_submittable": 1, 283 | "modified": "2024-05-19 04:04:04", 284 | "modified_by": "Administrator", 285 | "module": "Expenses", 286 | "name": "Expenses Entry", 287 | "naming_rule": "Expression", 288 | "owner": "Administrator", 289 | "permissions": [ 290 | { 291 | "amend": 0, 292 | "cancel": 0, 293 | "create": 1, 294 | "delete": 0, 295 | "email": 1, 296 | "export": 0, 297 | "if_owner": 1, 298 | "import": 0, 299 | "print": 1, 300 | "read": 1, 301 | "report": 1, 302 | "role": "Expenses Request Reviewer", 303 | "set_user_permissions": 0, 304 | "share": 1, 305 | "submit": 1, 306 | "write": 1 307 | }, 308 | { 309 | "amend": 1, 310 | "cancel": 1, 311 | "create": 1, 312 | "delete": 1, 313 | "email": 1, 314 | "export": 1, 315 | "if_owner": 0, 316 | "import": 1, 317 | "print": 1, 318 | "read": 1, 319 | "report": 1, 320 | "role": "Expense Manager", 321 | "set_user_permissions": 1, 322 | "share": 1, 323 | "submit": 1, 324 | "write": 1 325 | }, 326 | { 327 | "amend": 1, 328 | "cancel": 1, 329 | "create": 1, 330 | "delete": 1, 331 | "email": 1, 332 | "export": 1, 333 | "if_owner": 0, 334 | "import": 1, 335 | "print": 1, 336 | "read": 1, 337 | "report": 1, 338 | "role": "Accounts Manager", 339 | "set_user_permissions": 1, 340 | "share": 1, 341 | "submit": 1, 342 | "write": 1 343 | }, 344 | { 345 | "amend": 1, 346 | "cancel": 1, 347 | "create": 1, 348 | "delete": 1, 349 | "email": 1, 350 | "export": 1, 351 | "if_owner": 0, 352 | "import": 1, 353 | "print": 1, 354 | "read": 1, 355 | "report": 1, 356 | "role": "System Manager", 357 | "set_user_permissions": 1, 358 | "share": 1, 359 | "submit": 1, 360 | "write": 1 361 | }, 362 | { 363 | "amend": 1, 364 | "cancel": 1, 365 | "create": 1, 366 | "delete": 1, 367 | "email": 1, 368 | "export": 1, 369 | "if_owner": 0, 370 | "import": 1, 371 | "print": 1, 372 | "read": 1, 373 | "report": 1, 374 | "role": "Administrator", 375 | "set_user_permissions": 1, 376 | "share": 1, 377 | "submit": 1, 378 | "write": 1 379 | } 380 | ], 381 | "sort_field": "modified", 382 | "sort_order": "DESC", 383 | "default_print_format": "Expenses Entry", 384 | "track_changes": 1 385 | } -------------------------------------------------------------------------------- /expenses/expenses/doctype/expenses_entry/expenses_entry_list.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Expenses © 2024 3 | * Author: Ameen Ahmed 4 | * Company: Level Up Marketing & Software Development Services 5 | * Licence: Please refer to LICENSE file 6 | */ 7 | 8 | 9 | frappe.provide('frappe.listview_settings'); 10 | 11 | 12 | frappe.listview_settings['Expenses Entry'] = { 13 | onload: function(list) { 14 | frappe.exp().on('ready change', function() { this.setup_list(list); }); 15 | }, 16 | formatters: { 17 | payment_reference: function(v) { return cstr(v); }, 18 | clearance_date: function(v) { return cstr(v); }, 19 | }, 20 | }; -------------------------------------------------------------------------------- /expenses/expenses/doctype/expenses_entry_details/__init__.py: -------------------------------------------------------------------------------- 1 | # Expenses © 2024 2 | # Author: Ameen Ahmed 3 | # Company: Level Up Marketing & Software Development Services 4 | # Licence: Please refer to LICENSE file -------------------------------------------------------------------------------- /expenses/expenses/doctype/expenses_entry_details/expenses_entry_details.json: -------------------------------------------------------------------------------- 1 | { 2 | "allow_copy": 0, 3 | "allow_import": 1, 4 | "autoname": "hash", 5 | "creation": "2023-04-04 04:04:04", 6 | "description": "Expenses entry details for Expenses module", 7 | "doctype": "DocType", 8 | "engine": "InnoDB", 9 | "field_order": [ 10 | "main_section", 11 | "account", 12 | "main_column", 13 | "description", 14 | "dimensions_section", 15 | "project", 16 | "dimensions_column", 17 | "cost_center", 18 | "expense_section", 19 | "account_currency", 20 | "cost_in_account_currency", 21 | "expense_column", 22 | "exchange_rate", 23 | "cost", 24 | "type_section", 25 | "is_advance", 26 | "is_paid", 27 | "type_column", 28 | "paid_by", 29 | "expense_claim", 30 | "party_section", 31 | "party_type", 32 | "party_column", 33 | "party", 34 | "expense_ref" 35 | ], 36 | "fields": [ 37 | { 38 | "fieldname": "main_section", 39 | "fieldtype": "Section Break" 40 | }, 41 | { 42 | "fieldname": "account", 43 | "fieldtype": "Link", 44 | "label": "Expense Account", 45 | "options": "Account", 46 | "reqd": 1, 47 | "bold": 1, 48 | "in_list_view": 1, 49 | "search_index": 1, 50 | "ignore_user_permissions": 1 51 | }, 52 | { 53 | "fieldname": "main_column", 54 | "fieldtype": "Column Break", 55 | "width": "50%" 56 | }, 57 | { 58 | "fieldname": "description", 59 | "fieldtype": "Small Text", 60 | "label": "Description", 61 | "in_list_view": 1 62 | }, 63 | { 64 | "fieldname": "dimensions_section", 65 | "fieldtype": "Section Break", 66 | "label": "Accounting Dimensions", 67 | "collapsible": 1 68 | }, 69 | { 70 | "fieldname": "project", 71 | "fieldtype": "Link", 72 | "label": "Project", 73 | "description": "Use only if expense is project related", 74 | "options": "Project", 75 | "remember_last_selected_value": 1, 76 | "ignore_user_permissions": 1, 77 | "search_index": 1 78 | }, 79 | { 80 | "fieldname": "dimensions_column", 81 | "fieldtype": "Column Break", 82 | "width": "50%" 83 | }, 84 | { 85 | "fieldname": "cost_center", 86 | "fieldtype": "Link", 87 | "label": "Cost Center", 88 | "options": "Cost Center", 89 | "remember_last_selected_value": 1, 90 | "ignore_user_permissions": 1, 91 | "search_index": 1 92 | }, 93 | { 94 | "fieldname": "expense_section", 95 | "fieldtype": "Section Break" 96 | }, 97 | { 98 | "fieldname": "account_currency", 99 | "fieldtype": "Link", 100 | "label": "Currency", 101 | "options": "Currency", 102 | "fetch_from": "account.account_currency", 103 | "read_only": 1, 104 | "print_hide": 1, 105 | "report_hide": 1, 106 | "ignore_user_permissions": 1 107 | }, 108 | { 109 | "fieldname": "cost_in_account_currency", 110 | "fieldtype": "Currency", 111 | "label": "Cost", 112 | "options": "account_currency", 113 | "reqd": 1, 114 | "bold": 1, 115 | "non_negative": 1, 116 | "in_list_view": 1 117 | }, 118 | { 119 | "fieldname": "expense_column", 120 | "fieldtype": "Column Break", 121 | "width": "50%" 122 | }, 123 | { 124 | "fieldname": "exchange_rate", 125 | "fieldtype": "Float", 126 | "label": "Exchange Rate", 127 | "description": "From expense currency to company currency", 128 | "default": "1", 129 | "precision": "9", 130 | "non_negative": 1, 131 | "read_only": 1, 132 | "print_hide": 1 133 | }, 134 | { 135 | "fieldname": "cost", 136 | "fieldtype": "Currency", 137 | "label": "Cost (Company Currency)", 138 | "options": "Company:company:default_currency", 139 | "non_negative": 1, 140 | "bold": 1, 141 | "read_only": 1, 142 | "print_hide": 1 143 | }, 144 | { 145 | "fieldname": "type_section", 146 | "fieldtype": "Section Break" 147 | }, 148 | { 149 | "fieldname": "is_advance", 150 | "fieldtype": "Check", 151 | "label": "Is Advance", 152 | "default": "0", 153 | "in_list_view": 1, 154 | "search_index": 1 155 | }, 156 | { 157 | "fieldname": "is_paid", 158 | "fieldtype": "Check", 159 | "label": "Is Paid", 160 | "default": "0", 161 | "search_index": 1 162 | }, 163 | { 164 | "fieldname": "type_column", 165 | "fieldtype": "Column Break", 166 | "width": "50%" 167 | }, 168 | { 169 | "fieldname": "paid_by", 170 | "fieldtype": "Link", 171 | "label": "Paid By", 172 | "options": "Employee", 173 | "read_only_depends_on": "eval:!doc.is_paid", 174 | "mandatory_depends_on": "eval:doc.is_paid", 175 | "ignore_user_permissions": 1, 176 | "search_index": 1 177 | }, 178 | { 179 | "fieldname": "expense_claim", 180 | "fieldtype": "Link", 181 | "label": "Expense Claim", 182 | "options": "Expense", 183 | "hidden": 1, 184 | "read_only_depends_on": "eval:!doc.is_paid", 185 | "mandatory_depends_on": "eval:doc.is_paid", 186 | "ignore_user_permissions": 1, 187 | "search_index": 1 188 | }, 189 | { 190 | "fieldname": "party_section", 191 | "fieldtype": "Section Break", 192 | "label": "Expense Party", 193 | "collapsible": 1 194 | }, 195 | { 196 | "fieldname": "party_type", 197 | "fieldtype": "Link", 198 | "label": "Party Type", 199 | "description": "Use only if expense is party related", 200 | "options": "DocType", 201 | "in_list_view": 1, 202 | "search_index": 1 203 | }, 204 | { 205 | "fieldname": "party_column", 206 | "fieldtype": "Column Break", 207 | "width": "50%" 208 | }, 209 | { 210 | "fieldname": "party", 211 | "fieldtype": "Dynamic Link", 212 | "label": "Party", 213 | "description": "Use only if expense is party related", 214 | "options": "party_type", 215 | "read_only_depends_on": "eval:!doc.party_type", 216 | "mandatory_depends_on": "eval:doc.party_type", 217 | "in_list_view": 1, 218 | "search_index": 1 219 | }, 220 | { 221 | "fieldname": "expense_ref", 222 | "fieldtype": "Link", 223 | "label": "Expense Reference", 224 | "options": "Expense", 225 | "hidden": 1, 226 | "read_only": 1, 227 | "print_hide": 1, 228 | "report_hide": 1, 229 | "search_index": 1, 230 | "ignore_user_permissions": 1 231 | } 232 | ], 233 | "istable": 1, 234 | "modified": "2024-05-19 04:04:04", 235 | "modified_by": "Administrator", 236 | "module": "Expenses", 237 | "name": "Expenses Entry Details", 238 | "owner": "Administrator" 239 | } -------------------------------------------------------------------------------- /expenses/expenses/doctype/expenses_entry_details/expenses_entry_details.py: -------------------------------------------------------------------------------- 1 | # Expenses © 2024 2 | # Author: Ameen Ahmed 3 | # Company: Level Up Marketing & Software Development Services 4 | # Licence: Please refer to LICENSE file 5 | 6 | 7 | from frappe.model.document import Document 8 | 9 | 10 | class ExpensesEntryDetails(Document): 11 | pass -------------------------------------------------------------------------------- /expenses/expenses/doctype/expenses_request/__init__.py: -------------------------------------------------------------------------------- 1 | # Expenses © 2024 2 | # Author: Ameen Ahmed 3 | # Company: Level Up Marketing & Software Development Services 4 | # Licence: Please refer to LICENSE file -------------------------------------------------------------------------------- /expenses/expenses/doctype/expenses_request/expenses_request.json: -------------------------------------------------------------------------------- 1 | { 2 | "allow_copy": 1, 3 | "allow_import": 1, 4 | "autoname": "format:EXP-R-{YY}{MM}{DD}-{###}", 5 | "creation": "2023-04-04 04:04:04", 6 | "description": "Expenses Request for Expenses module", 7 | "doctype": "DocType", 8 | "engine": "InnoDB", 9 | "field_order": [ 10 | "main_section", 11 | "company", 12 | "main_column", 13 | "posting_date", 14 | "expenses_section", 15 | "expenses", 16 | "remarks", 17 | "control_section", 18 | "reviewer", 19 | "status", 20 | "workflow_state", 21 | "amended_from", 22 | "amendment_date" 23 | ], 24 | "fields": [ 25 | { 26 | "fieldname": "main_section", 27 | "fieldtype": "Section Break" 28 | }, 29 | { 30 | "fieldname": "company", 31 | "fieldtype": "Link", 32 | "label": "Company", 33 | "options": "Company", 34 | "default": ":Company", 35 | "reqd": 1, 36 | "bold": 1, 37 | "remember_last_selected_value": 1, 38 | "in_list_view": 1, 39 | "in_filter": 1, 40 | "in_standard_filter": 1, 41 | "in_preview": 1, 42 | "search_index": 1, 43 | "ignore_user_permissions": 1 44 | }, 45 | { 46 | "fieldname": "main_column", 47 | "fieldtype": "Column Break" 48 | }, 49 | { 50 | "fieldname": "posting_date", 51 | "fieldtype": "Date", 52 | "label": "Posting Date", 53 | "default": "Today", 54 | "read_only": 1, 55 | "in_list_view": 1, 56 | "in_filter": 1, 57 | "in_standard_filter": 1, 58 | "in_preview": 1, 59 | "search_index": 1 60 | }, 61 | { 62 | "fieldname": "expenses_section", 63 | "fieldtype": "Section Break" 64 | }, 65 | { 66 | "fieldname": "expenses", 67 | "fieldtype": "Table", 68 | "label": "Expenses", 69 | "options": "Expenses Request Details", 70 | "bold": 1, 71 | "reqd": 1 72 | }, 73 | { 74 | "fieldname": "remarks", 75 | "fieldtype": "Text", 76 | "label": "Remarks", 77 | "in_preview": 1 78 | }, 79 | { 80 | "fieldname": "control_section", 81 | "fieldtype": "Section Break", 82 | "hidden": 1 83 | }, 84 | { 85 | "fieldname": "reviewer", 86 | "fieldtype": "Link", 87 | "label": "Reviewer", 88 | "options": "User", 89 | "read_only": 1, 90 | "hidden": 1, 91 | "allow_on_submit": 1, 92 | "in_list_view": 1, 93 | "in_filter": 1, 94 | "in_standard_filter": 1, 95 | "ignore_user_permissions": 1, 96 | "search_index": 1 97 | }, 98 | { 99 | "fieldname": "status", 100 | "fieldtype": "Select", 101 | "label": "Status", 102 | "options": "Draft\nPending\nCancelled\nApproved\nRejected\nProcessed", 103 | "default": "Draft", 104 | "read_only": 1, 105 | "hidden": 1, 106 | "allow_on_submit": 1, 107 | "in_filter": 1, 108 | "in_standard_filter": 1, 109 | "in_preview": 1, 110 | "search_index": 1 111 | }, 112 | { 113 | "fieldname": "workflow_state", 114 | "fieldtype": "Link", 115 | "label": "Workflow State", 116 | "options": "Workflow State", 117 | "default": "Draft", 118 | "no_copy": 1, 119 | "read_only": 1, 120 | "hidden": 1, 121 | "print_hide": 1, 122 | "report_hide": 1, 123 | "ignore_user_permissions": 1, 124 | "allow_on_submit": 1 125 | }, 126 | { 127 | "fieldname": "amended_from", 128 | "fieldtype": "Link", 129 | "label": "Amended From", 130 | "options": "Expenses Request", 131 | "read_only": 1, 132 | "hidden": 1, 133 | "allow_on_submit": 1, 134 | "ignore_user_permissions": 1 135 | }, 136 | { 137 | "fieldname": "amendment_date", 138 | "fieldtype": "Date", 139 | "label": "Amendment Date", 140 | "read_only": 1, 141 | "hidden": 1, 142 | "allow_on_submit": 1 143 | } 144 | ], 145 | "icon": "fa fa-file-text", 146 | "is_submittable": 1, 147 | "modified": "2023-04-04 04:04:04", 148 | "modified_by": "Administrator", 149 | "module": "Expenses", 150 | "name": "Expenses Request", 151 | "naming_rule": "Expression", 152 | "owner": "Administrator", 153 | "permissions": [ 154 | { 155 | "amend": 0, 156 | "cancel": 1, 157 | "create":1, 158 | "delete": 0, 159 | "email": 0, 160 | "export": 0, 161 | "if_owner": 1, 162 | "import": 0, 163 | "permlevel": 0, 164 | "print": 1, 165 | "read": 1, 166 | "report": 0, 167 | "role": "Accounts User", 168 | "share": 0, 169 | "submit": 1, 170 | "write": 1 171 | }, 172 | { 173 | "amend": 0, 174 | "cancel": 0, 175 | "create": 0, 176 | "delete": 0, 177 | "email": 0, 178 | "export": 0, 179 | "if_owner": 0, 180 | "import": 0, 181 | "permlevel": 0, 182 | "print": 1, 183 | "read": 1, 184 | "report": 1, 185 | "role": "Expenses Request Reviewer", 186 | "set_user_permissions": 0, 187 | "share": 1, 188 | "submit": 0, 189 | "write": 0 190 | }, 191 | { 192 | "amend": 1, 193 | "cancel": 1, 194 | "create": 1, 195 | "delete": 1, 196 | "email": 1, 197 | "export": 1, 198 | "if_owner": 0, 199 | "import": 1, 200 | "permlevel": 0, 201 | "print": 1, 202 | "read": 1, 203 | "report": 1, 204 | "role": "Accounts Manager", 205 | "set_user_permissions": 1, 206 | "share": 1, 207 | "submit": 1, 208 | "write": 1 209 | }, 210 | { 211 | "amend": 1, 212 | "cancel": 1, 213 | "create": 1, 214 | "delete": 1, 215 | "email": 1, 216 | "export": 1, 217 | "if_owner": 0, 218 | "import": 1, 219 | "permlevel": 0, 220 | "print": 1, 221 | "read": 1, 222 | "report": 1, 223 | "role": "System Manager", 224 | "set_user_permissions": 1, 225 | "share": 1, 226 | "submit": 1, 227 | "write": 1 228 | }, 229 | { 230 | "amend": 1, 231 | "cancel": 1, 232 | "create": 1, 233 | "delete": 1, 234 | "email": 1, 235 | "export": 1, 236 | "if_owner": 0, 237 | "import": 1, 238 | "permlevel": 0, 239 | "print": 1, 240 | "read": 1, 241 | "report": 1, 242 | "role": "Administrator", 243 | "set_user_permissions": 1, 244 | "share": 1, 245 | "submit": 1, 246 | "write": 1 247 | } 248 | ], 249 | "sort_field": "modified", 250 | "sort_order": "DESC", 251 | "default_print_format": "Expenses Request", 252 | "track_changes": 1 253 | } -------------------------------------------------------------------------------- /expenses/expenses/doctype/expenses_request/expenses_request_list.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Expenses © 2024 3 | * Author: Ameen Ahmed 4 | * Company: Level Up Marketing & Software Development Services 5 | * Licence: Please refer to LICENSE file 6 | */ 7 | 8 | 9 | frappe.provide('frappe.listview_settings'); 10 | 11 | 12 | frappe.listview_settings['Expenses Request'] = { 13 | onload: function(list) { 14 | frappe.exp().on('ready change', function() { this.setup_list(list); }); 15 | }, 16 | button: { 17 | show: function(doc) { 18 | return frappe.user_roles.includes('Expenses Reviewer') 19 | && cint(doc.docstatus) === 1 && cstr(doc.status) === 'Approved'; 20 | }, 21 | get_label: function(doc) { 22 | return __('Create Entry'); 23 | }, 24 | get_description: function(doc) { 25 | return __('Create expenses entry for "{0}".', [cstr(doc.name)]); 26 | }, 27 | action: function(doc) { 28 | frappe.new_doc('Expenses Entry', {expenses_request_ref: cstr(doc.name)}); 29 | }, 30 | }, 31 | formatters: { 32 | reviewer: function(v) { return cstr(v); }, 33 | }, 34 | }; -------------------------------------------------------------------------------- /expenses/expenses/doctype/expenses_request_details/__init__.py: -------------------------------------------------------------------------------- 1 | # Expenses © 2024 2 | # Author: Ameen Ahmed 3 | # Company: Level Up Marketing & Software Development Services 4 | # Licence: Please refer to LICENSE file -------------------------------------------------------------------------------- /expenses/expenses/doctype/expenses_request_details/expenses_request_details.json: -------------------------------------------------------------------------------- 1 | { 2 | "allow_copy": 0, 3 | "allow_import": 1, 4 | "autoname": "hash", 5 | "creation": "2023-04-04 04:04:04", 6 | "description": "Expenses request details for Expenses module", 7 | "doctype": "DocType", 8 | "engine": "InnoDB", 9 | "field_order": [ 10 | "main_section", 11 | "expense", 12 | "expense_item", 13 | "uom", 14 | "main_column", 15 | "total", 16 | "is_advance", 17 | "required_by", 18 | "attachments_section", 19 | "attachments" 20 | ], 21 | "fields": [ 22 | { 23 | "fieldname": "main_section", 24 | "fieldtype": "Section Break" 25 | }, 26 | { 27 | "fieldname": "expense", 28 | "fieldtype": "Link", 29 | "label": "Expense", 30 | "options": "Expense", 31 | "read_only": 1, 32 | "bold": 1, 33 | "in_list_view": 1, 34 | "search_index": 1, 35 | "ignore_user_permissions": 1 36 | }, 37 | { 38 | "fieldname": "expense_item", 39 | "fieldtype": "Data", 40 | "label": "Expense Item", 41 | "fetch_from": "expense.expense_item", 42 | "read_only": 1, 43 | "in_list_view": 1 44 | }, 45 | { 46 | "fieldname": "uom", 47 | "fieldtype": "Data", 48 | "label": "Unit Of Measure", 49 | "fetch_from": "expense.uom", 50 | "read_only": 1, 51 | "in_list_view": 1 52 | }, 53 | { 54 | "fieldname": "main_column", 55 | "fieldtype": "Column Break" 56 | }, 57 | { 58 | "fieldname": "total", 59 | "fieldtype": "Data", 60 | "label": "Total", 61 | "fetch_from": "expense.total", 62 | "read_only": 1, 63 | "in_list_view": 1 64 | }, 65 | { 66 | "fieldname": "is_advance", 67 | "fieldtype": "Data", 68 | "label": "Is Advance", 69 | "fetch_from": "expense.is_advance", 70 | "read_only": 1, 71 | "in_list_view": 1 72 | }, 73 | { 74 | "fieldname": "required_by", 75 | "fieldtype": "Data", 76 | "label": "Required By", 77 | "fetch_from": "expense.required_by", 78 | "read_only": 1, 79 | "in_list_view": 1 80 | }, 81 | { 82 | "fieldname": "attachments_section", 83 | "fieldtype": "Section Break" 84 | }, 85 | { 86 | "fieldname": "attachments", 87 | "fieldtype": "HTML", 88 | "label": "Attachments", 89 | "read_only": 1 90 | } 91 | ], 92 | "istable": 1, 93 | "modified": "2024-05-12 04:04:04", 94 | "modified_by": "Administrator", 95 | "module": "Expenses", 96 | "name": "Expenses Request Details", 97 | "owner": "Administrator" 98 | } -------------------------------------------------------------------------------- /expenses/expenses/doctype/expenses_request_details/expenses_request_details.py: -------------------------------------------------------------------------------- 1 | # Expenses © 2024 2 | # Author: Ameen Ahmed 3 | # Company: Level Up Marketing & Software Development Services 4 | # Licence: Please refer to LICENSE file 5 | 6 | 7 | from frappe.model.document import Document 8 | 9 | 10 | class ExpensesRequestDetails(Document): 11 | pass -------------------------------------------------------------------------------- /expenses/expenses/doctype/expenses_settings/__init__.py: -------------------------------------------------------------------------------- 1 | # Expenses © 2024 2 | # Author: Ameen Ahmed 3 | # Company: Level Up Marketing & Software Development Services 4 | # Licence: Please refer to LICENSE file -------------------------------------------------------------------------------- /expenses/expenses/doctype/expenses_settings/expenses_settings.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Expenses © 2024 3 | * Author: Ameen Ahmed 4 | * Company: Level Up Marketing & Software Development Services 5 | * Licence: Please refer to LICENSE file 6 | */ 7 | 8 | 9 | frappe.ui.form.on('Expenses Settings', { 10 | onload: function(frm) { 11 | frappe.exp().on('on_alert', function(d, t) { 12 | frm._sets.errs.includes(t) && (d.title = __(frm.doctype)); 13 | }); 14 | frm._sets = { 15 | errs: ['fatal', 'error'], 16 | ready: 0, 17 | }; 18 | }, 19 | refresh: function(frm) { 20 | if (frm._sets.ready) return; 21 | frm.events.setup_general_note(frm); 22 | frm.events.setup_update_note(frm); 23 | frm._sets.ready++; 24 | }, 25 | check_for_update: function(frm) { 26 | frappe.exp().request('check_for_update', null, function(v) { v && frm.reload_doc(); }); 27 | }, 28 | validate: function(frm) { 29 | if (!cint(frm.doc.send_update_notification)) return; 30 | if (!frappe.exp().$isStrVal(frm.doc.update_notification_sender)) { 31 | frappe.exp().fatal(__('A valid update notification sender is required.')); 32 | return false; 33 | } 34 | if (!frappe.exp().$isArrVal(frm.doc.update_notification_receivers)) { 35 | frappe.exp().fatal(__('At least one valid update notification receiver is required.')); 36 | return false; 37 | } 38 | }, 39 | setup_general_note: function(frm) { 40 | frm.get_field('general_note').$wrapper.empty().append('\ 41 | ' + __('Important') + ':\ 42 |

' + __('Disabling the module will prevent the creation and modification of entries in all the module doctypes.') + '

\ 43 | '); 44 | }, 45 | setup_update_note: function(frm) { 46 | frm.get_field('update_note').$wrapper.empty().append('\ 47 | \ 74 | '); 75 | }, 76 | }); -------------------------------------------------------------------------------- /expenses/expenses/doctype/expenses_settings/expenses_settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "creation": "2023-04-04 04:04:04", 3 | "description": "Settings for Expenses module", 4 | "doctype": "DocType", 5 | "engine": "InnoDB", 6 | "field_order": [ 7 | "general_section", 8 | "is_enabled", 9 | "general_column", 10 | "general_note", 11 | "expense_section", 12 | "reqd_expense_claim_if_paid", 13 | "expense_column", 14 | "update_section", 15 | "auto_check_for_update", 16 | "send_update_notification", 17 | "update_notification_sender", 18 | "update_notification_receivers", 19 | "update_column", 20 | "check_for_update", 21 | "update_note", 22 | "current_version", 23 | "latest_version", 24 | "latest_check", 25 | "has_update" 26 | ], 27 | "fields": [ 28 | { 29 | "fieldname": "general_section", 30 | "fieldtype": "Section Break", 31 | "label": "General Settings" 32 | }, 33 | { 34 | "fieldname": "is_enabled", 35 | "fieldtype": "Check", 36 | "label": "Is Enabled", 37 | "default": "1" 38 | }, 39 | { 40 | "fieldname": "general_column", 41 | "fieldtype": "Column Break" 42 | }, 43 | { 44 | "fieldname": "general_note", 45 | "fieldtype": "HTML", 46 | "label": "", 47 | "read_only": 1 48 | }, 49 | { 50 | "fieldname": "expense_section", 51 | "fieldtype": "Section Break", 52 | "label": "Expense Settings" 53 | }, 54 | { 55 | "fieldname": "reqd_expense_claim_if_paid", 56 | "fieldtype": "Check", 57 | "label": "Require Expense Claim If Paid", 58 | "description": "Require Expense Claim when Is Paid field is checked." 59 | }, 60 | { 61 | "fieldname": "expense_column", 62 | "fieldtype": "Column Break" 63 | }, 64 | { 65 | "fieldname": "update_section", 66 | "fieldtype": "Section Break", 67 | "label": "Update Settings" 68 | }, 69 | { 70 | "fieldname": "auto_check_for_update", 71 | "fieldtype": "Check", 72 | "label": "Auto Check For Update", 73 | "default": "1" 74 | }, 75 | { 76 | "fieldname": "send_update_notification", 77 | "fieldtype": "Check", 78 | "label": "Send Update Notification", 79 | "default": "1" 80 | }, 81 | { 82 | "fieldname": "update_notification_sender", 83 | "fieldtype": "Link", 84 | "label": "Update Notification Sender", 85 | "options": "User", 86 | "bold": 1, 87 | "read_only_depends_on": "eval:!doc.send_update_notification", 88 | "mandatory_depends_on": "eval:doc.send_update_notification", 89 | "ignore_user_permissions": 1 90 | }, 91 | { 92 | "fieldname": "update_notification_receivers", 93 | "fieldtype": "Table MultiSelect", 94 | "label": "Update Notification Receivers", 95 | "options": "Expenses Update Receiver", 96 | "bold": 1, 97 | "read_only_depends_on": "eval:!doc.send_update_notification", 98 | "mandatory_depends_on": "eval:doc.send_update_notification" 99 | }, 100 | { 101 | "fieldname": "update_column", 102 | "fieldtype": "Column Break" 103 | }, 104 | { 105 | "fieldname": "check_for_update", 106 | "fieldtype": "Button", 107 | "label": "Check For Update", 108 | "read_only_depends_on": "eval:!doc.is_enabled" 109 | }, 110 | { 111 | "fieldname": "update_note", 112 | "fieldtype": "HTML", 113 | "label": "", 114 | "read_only": 1 115 | }, 116 | { 117 | "fieldname": "current_version", 118 | "fieldtype": "Data", 119 | "label": "Current Version", 120 | "read_only": 1, 121 | "hidden": 1 122 | }, 123 | { 124 | "fieldname": "latest_version", 125 | "fieldtype": "Data", 126 | "label": "Latest Version", 127 | "read_only": 1, 128 | "hidden": 1 129 | }, 130 | { 131 | "fieldname": "latest_check", 132 | "fieldtype": "Data", 133 | "label": "Latest Check", 134 | "read_only": 1, 135 | "hidden": 1 136 | }, 137 | { 138 | "fieldname": "has_update", 139 | "fieldtype": "Check", 140 | "label": "Has Update", 141 | "read_only": 1, 142 | "hidden": 1 143 | } 144 | ], 145 | "icon": "fa fa-cog", 146 | "issingle": 1, 147 | "modified": "2024-05-22 04:04:04", 148 | "modified_by": "Administrator", 149 | "module": "Expenses", 150 | "name": "Expenses Settings", 151 | "owner": "Administrator", 152 | "permissions": [ 153 | { 154 | "amend": 1, 155 | "cancel": 1, 156 | "create": 1, 157 | "delete": 1, 158 | "email": 1, 159 | "export": 1, 160 | "import": 1, 161 | "print": 1, 162 | "read": 1, 163 | "report": 1, 164 | "role": "Expense Manager", 165 | "set_user_permissions": 1, 166 | "share": 1, 167 | "submit": 1, 168 | "write": 1 169 | }, 170 | { 171 | "amend": 1, 172 | "cancel": 1, 173 | "create": 1, 174 | "delete": 1, 175 | "email": 1, 176 | "export": 1, 177 | "import": 1, 178 | "print": 1, 179 | "read": 1, 180 | "report": 1, 181 | "role": "Accounts Manager", 182 | "set_user_permissions": 1, 183 | "share": 1, 184 | "submit": 1, 185 | "write": 1 186 | }, 187 | { 188 | "amend": 1, 189 | "cancel": 1, 190 | "create": 1, 191 | "delete": 1, 192 | "email": 1, 193 | "export": 1, 194 | "import": 1, 195 | "print": 1, 196 | "read": 1, 197 | "report": 1, 198 | "role": "System Manager", 199 | "set_user_permissions": 1, 200 | "share": 1, 201 | "submit": 1, 202 | "write": 1 203 | }, 204 | { 205 | "amend": 1, 206 | "cancel": 1, 207 | "create": 1, 208 | "delete": 1, 209 | "email": 1, 210 | "export": 1, 211 | "import": 1, 212 | "print": 1, 213 | "read": 1, 214 | "report": 1, 215 | "role": "Administrator", 216 | "set_user_permissions": 1, 217 | "share": 1, 218 | "submit": 1, 219 | "write": 1 220 | } 221 | ] 222 | } -------------------------------------------------------------------------------- /expenses/expenses/doctype/expenses_settings/expenses_settings.py: -------------------------------------------------------------------------------- 1 | # Expenses © 2024 2 | # Author: Ameen Ahmed 3 | # Company: Level Up Marketing & Software Development Services 4 | # Licence: Please refer to LICENSE file 5 | 6 | 7 | from frappe import _ 8 | from frappe.utils import cint 9 | from frappe.model.document import Document 10 | 11 | 12 | class ExpensesSettings(Document): 13 | def validate(self): 14 | if self._send_update_notification: 15 | self._check_sender() 16 | self._check_receivers() 17 | 18 | 19 | def before_save(self): 20 | from expenses.libs import clear_doc_cache 21 | 22 | clear_doc_cache(self.doctype) 23 | 24 | 25 | def after_save(self): 26 | if self.has_value_changed("is_enabled"): 27 | from expenses.libs import emit_settings_changed 28 | 29 | emit_settings_changed({ 30 | "is_enabled": 1 if self._is_enabled else 0 31 | }) 32 | 33 | 34 | @property 35 | def _is_enabled(self): 36 | return cint(self.is_enabled) > 0 37 | 38 | 39 | @property 40 | def _reqd_expense_claim_if_paid(self): 41 | return cint(self.reqd_expense_claim_if_paid) > 0 42 | 43 | 44 | @property 45 | def _auto_check_for_update(self): 46 | return cint(self.auto_check_for_update) > 0 47 | 48 | 49 | @property 50 | def _send_update_notification(self): 51 | return cint(self.send_update_notification) > 0 52 | 53 | 54 | def _check_sender(self): 55 | if not self.update_notification_sender: 56 | self._error(_("A valid update notification sender is required.")) 57 | 58 | from expenses.libs import user_exists 59 | 60 | if not user_exists(self.update_notification_sender): 61 | self._error(_("Update notification sender doesn't exist.")) 62 | 63 | 64 | def _check_receivers(self): 65 | if ( 66 | self.update_notification_receivers and 67 | self.has_value_changed("update_notification_receivers") 68 | ): 69 | from expenses.libs import users_filter 70 | 71 | users = users_filter([v.user for v in self.update_notification_receivers]) 72 | exist = [] 73 | notfound = [] 74 | for v in self.update_notification_receivers: 75 | if v.user not in users or v.user in exist: 76 | notfound.append(v) 77 | else: 78 | exist.append(v.user) 79 | 80 | users.clear() 81 | exist.clear() 82 | if notfound: 83 | for i in range(len(notfound)): 84 | self.update_notification_receivers.remove(notfound.pop(0)) 85 | 86 | if not self.update_notification_receivers: 87 | self._error(_("At least one valid update notification receiver is required.")) 88 | 89 | 90 | def _error(self, msg): 91 | from expenses.libs import error 92 | 93 | error(msg, _(self.doctype)) -------------------------------------------------------------------------------- /expenses/expenses/doctype/expenses_update_receiver/__init__.py: -------------------------------------------------------------------------------- 1 | # Expenses © 2024 2 | # Author: Ameen Ahmed 3 | # Company: Level Up Marketing & Software Development Services 4 | # Licence: Please refer to LICENSE file -------------------------------------------------------------------------------- /expenses/expenses/doctype/expenses_update_receiver/expenses_update_receiver.json: -------------------------------------------------------------------------------- 1 | { 2 | "allow_copy": 0, 3 | "allow_import": 1, 4 | "autoname": "hash", 5 | "creation": "2023-04-04 04:04:04", 6 | "description": "Update receiver for Expenses module", 7 | "doctype": "DocType", 8 | "engine": "InnoDB", 9 | "field_order": [ 10 | "user" 11 | ], 12 | "fields": [ 13 | { 14 | "fieldname": "user", 15 | "fieldtype": "Link", 16 | "label": "User", 17 | "options": "User", 18 | "reqd": 1, 19 | "bold": 1, 20 | "in_list_view": 1, 21 | "ignore_user_permissions": 1 22 | } 23 | ], 24 | "istable": 1, 25 | "modified": "2024-05-12 04:04:04", 26 | "modified_by": "Administrator", 27 | "module": "Expenses", 28 | "name": "Expenses Update Receiver", 29 | "owner": "Administrator" 30 | } -------------------------------------------------------------------------------- /expenses/expenses/doctype/expenses_update_receiver/expenses_update_receiver.py: -------------------------------------------------------------------------------- 1 | # Expenses © 2024 2 | # Author: Ameen Ahmed 3 | # Company: Level Up Marketing & Software Development Services 4 | # Licence: Please refer to LICENSE file 5 | 6 | 7 | from frappe.model.document import Document 8 | 9 | 10 | class ExpensesUpdateReceiver(Document): 11 | pass -------------------------------------------------------------------------------- /expenses/expenses/print_format/__init__.py: -------------------------------------------------------------------------------- 1 | # Expenses © 2024 2 | # Author: Ameen Ahmed 3 | # Company: Level Up Marketing & Software Development Services 4 | # Licence: Please refer to LICENSE file -------------------------------------------------------------------------------- /expenses/expenses/print_format/expense/__init__.py: -------------------------------------------------------------------------------- 1 | # Expenses © 2024 2 | # Author: Ameen Ahmed 3 | # Company: Level Up Marketing & Software Development Services 4 | # Licence: Please refer to LICENSE file -------------------------------------------------------------------------------- /expenses/expenses/print_format/expense/expense.html: -------------------------------------------------------------------------------- 1 | {%- from "templates/print_formats/standard_macros.html" import add_header -%} 2 |
3 | {%- if not doc.get("print_heading") and not doc.get("select_print_heading") 4 | and doc.set("select_print_heading", _("Expense")) -%}{%- endif -%} 5 | {{ add_header(0, 1, doc, letter_head, no_letterhead, print_settings) }} 6 |
7 |
8 | 9 | {%- for fcol, scol in ( 10 | ((_("Name"), doc.name), (_("Date"), frappe.format_date(doc.creation))), 11 | ((_("Company"), doc.company), (_("Status"), _(doc.status))), 12 | ((_("Expense Item"), doc.expense_item), (_("Required By"), frappe.format_date(doc.required_by))), 13 | ((_("Expense Account"), doc.expense_account), (_("Project"), doc.project if doc.project else _("N/A"))), 14 | ((_("Party Type"), doc.party_type if doc.party_type else _("N/A")), (_("Party"), doc.party if doc.party else _("N/A"))), 15 | ((_("Is Paid"), _("Yes") if cint(doc.is_paid) else _("No")), (_("Paid By"), doc.paid_by if doc.paid_by else _("N/A"))), 16 | ) -%} 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | {%- endfor -%} 26 |
{{ fcol[0] }}:{{ fcol[1] }}{{ scol[0] }}:{{ scol[1] }}
27 |
28 |
29 |
30 |
31 |
{{ _("Details") }}
32 |
33 |
34 |
35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 |
{{ _("Cost") }}{{ doc.get_formatted("cost") }}{{ _("Quantity") }}{{ doc.get_formatted("qty") }}
{{ _("Total") }}{{ doc.get_formatted("total") }}{{ _("Is Advance") }}{{ _("Yes") if cint(doc.is_advance) else _("No") }}
49 |
50 |
51 | {% if doc.description %} 52 |
53 |
54 |
55 | 56 |
57 |
58 | {{ doc.description }} 59 |
60 |
61 | {% endif %} 62 |
63 |
64 |
65 | 66 |
67 |
68 |
-------------------------------------------------------------------------------- /expenses/expenses/print_format/expense/expense.json: -------------------------------------------------------------------------------- 1 | { 2 | "align_labels_right": 0, 3 | "creation": "2023-04-04 04:04:04.119400", 4 | "custom_format": 0, 5 | "default_print_language": "en", 6 | "disabled": 0, 7 | "doc_type": "Expense", 8 | "docstatus": 0, 9 | "doctype": "Print Format", 10 | "font": "Default", 11 | "idx": 0, 12 | "line_breaks": 0, 13 | "css": ".text-start {\ntext-align: left;\n}\nhtml[dir=\"rtl\"] .text-start {\ntext-align: right;\n}\n.text-end {\ntext-align: right;\n}\nhtml[dir=\"rtl\"] .text-end {\ntext-align: left;\n}\n.table {\ntable-layout: auto;\nmargin-bottom: 0;\n}\n.table th, .table td {\nvertical-align: middle;\nwhite-space: nowrap;\ntext-align: center;\nwidth: auto;\n}\n.table th.w-15, .table td.w-15 {\nwidth: 15%;\n}\n.table th.fit, .table td.fit {\nwidth: 1%;\n}\n.subheader {\nfont-size: 1.1rem;\n}", 14 | "modified": "2024-05-22 04:04:04", 15 | "modified_by": "Administrator", 16 | "module": "Expenses", 17 | "name": "Expense", 18 | "owner": "Administrator", 19 | "print_format_builder": 0, 20 | "print_format_type": "Jinja", 21 | "show_section_headings": 0, 22 | "standard": "Yes" 23 | } -------------------------------------------------------------------------------- /expenses/expenses/print_format/expenses_entry/__init__.py: -------------------------------------------------------------------------------- 1 | # Expenses © 2024 2 | # Author: Ameen Ahmed 3 | # Company: Level Up Marketing & Software Development Services 4 | # Licence: Please refer to LICENSE file -------------------------------------------------------------------------------- /expenses/expenses/print_format/expenses_entry/expenses_entry.html: -------------------------------------------------------------------------------- 1 | {%- from "templates/print_formats/standard_macros.html" import add_header -%} 2 |
3 | {%- if not doc.get("print_heading") and not doc.get("select_print_heading") 4 | and doc.set("select_print_heading", _("Expenses Entry")) -%}{%- endif -%} 5 | {{ add_header(0, 1, doc, letter_head, no_letterhead, print_settings) }} 6 |
7 |
8 | 9 | {%- for fcol, scol in ( 10 | ((_("Name"), doc.name), (_("Date"), frappe.format_date(doc.posting_date))), 11 | ((_("Company"), doc.company), (_("Status"), _("Submitted") if doc.docstatus == 1 else (_("Cancelled") if doc.docstatus == 2 else _("Draft")))), 12 | ((_("Mode of Payment"), doc.mode_of_payment), (_("Payment Account"), doc.payment_account)), 13 | ((_("Payment Reference"), doc.payment_reference or "N/A"), (_("Payment Clearance"), frappe.format_date(doc.clearance_date) if doc.clearance_date else _("N/A"))), 14 | ) -%} 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | {%- endfor -%} 24 |
{{ fcol[0] }}:{{ fcol[1] }}{{ scol[0] }}:{{ scol[1] }}
25 |
26 |
27 |
28 |
29 |
{{ _("Expenses") }}
30 |
31 |
32 |
33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | {% for exp in doc.expenses %} 41 | 42 | 43 | 44 | 45 | 46 | 47 | {% endfor %} 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 |
{{ _("Expense Account") }}{{ _("Description") }}{{ _("Is Advance") }}{{ _("Cost") }}
{{ exp.account }}{{ exp.description }}{{ _("Yes") if exp.is_advance else _("No") }}{{ exp.get_formatted("cost"}) }}
{{ _("Exchange Rate") }}{{ doc.get_formatted("exchange_rate") }}
{{ _("Total") }}{{ doc.get_formatted("total") }}
57 |
58 |
59 | {% if doc.remarks %} 60 |
61 |
62 |
63 | 64 |
65 |
66 | {{ doc.remarks }} 67 |
68 |
69 | {% endif %} 70 |
71 |
72 |
73 | 74 |
75 |
76 |
-------------------------------------------------------------------------------- /expenses/expenses/print_format/expenses_entry/expenses_entry.json: -------------------------------------------------------------------------------- 1 | { 2 | "align_labels_right": 0, 3 | "creation": "2023-04-04 04:04:04.119400", 4 | "custom_format": 0, 5 | "default_print_language": "en", 6 | "disabled": 0, 7 | "doc_type": "Expenses Entry", 8 | "docstatus": 0, 9 | "doctype": "Print Format", 10 | "font": "Default", 11 | "idx": 0, 12 | "line_breaks": 0, 13 | "css": ".text-start {\ntext-align: left;\n}\nhtml[dir=\"rtl\"] .text-start {\ntext-align: right;\n}\n.text-end {\ntext-align: right;\n}\nhtml[dir=\"rtl\"] .text-end {\ntext-align: left;\n}\n.table {\ntable-layout: auto;\nmargin-bottom: 0;\n}\n.table th, .table td {\nvertical-align: middle;\nwhite-space: nowrap;\ntext-align: center;\nwidth: auto;\n}\n.table th.w-15, .table td.w-15 {\nwidth: 15%;\n}\n.table th.fit, .table td.fit {\nwidth: 1%;\n}\n.subheader {\nfont-size: 1.1rem;\n}", 14 | "modified": "2024-05-22 04:04:04", 15 | "modified_by": "Administrator", 16 | "module": "Expenses", 17 | "name": "Expenses Entry", 18 | "owner": "Administrator", 19 | "print_format_builder": 0, 20 | "print_format_type": "Jinja", 21 | "show_section_headings": 0, 22 | "standard": "Yes" 23 | } -------------------------------------------------------------------------------- /expenses/expenses/print_format/expenses_request/__init__.py: -------------------------------------------------------------------------------- 1 | # Expenses © 2024 2 | # Author: Ameen Ahmed 3 | # Company: Level Up Marketing & Software Development Services 4 | # Licence: Please refer to LICENSE file -------------------------------------------------------------------------------- /expenses/expenses/print_format/expenses_request/expenses_request.html: -------------------------------------------------------------------------------- 1 | {%- from "templates/print_formats/standard_macros.html" import add_header -%} 2 |
3 | {%- if not doc.get("print_heading") and not doc.get("select_print_heading") 4 | and doc.set("select_print_heading", _("Expenses Request")) -%}{%- endif -%} 5 | {{ add_header(0, 1, doc, letter_head, no_letterhead, print_settings) }} 6 |
7 |
8 | 9 | {%- for fcol, scol in ( 10 | ((_("Name"), doc.name), (_("Date"), frappe.format_date(doc.posting_date))), 11 | ((_("Company"), doc.company), (_("Status"), _(doc.status))), 12 | ) -%} 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | {%- endfor -%} 22 |
{{ fcol[0] }}:{{ fcol[1] }}{{ scol[0] }}:{{ scol[1] }}
23 |
24 |
25 |
26 |
27 |
{{ _("Expenses") }}
28 |
29 |
30 |
31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | {% set expenses = frappe.get_all('Expense', filters={'name': ["in", [v.expense for v in doc.expenses]]}, fields=["name", "expense_item", "description", "total", "is_advance", "required_by"]) %} 41 | {% for exp in expenses %} 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | {% endfor %} 52 |
{{ _("Expense") }}{{ _("Expense Item") }}{{ _("Description") }}{{ _("Total") }}{{ _("Is Advance") }}{{ _("Required By") }}
{{ exp.name }}{{ exp.expense_item }}{{ exp.description }}{{ exp.get_formatted("total") }}{{ _("Yes") if exp.is_advance else _("No") }}{{ frappe.format_date(exp.required_by) }}
53 |
54 |
55 | {% if doc.remarks %} 56 |
57 |
58 |
59 | 60 |
61 |
62 | {{ doc.remarks }} 63 |
64 |
65 | {% endif %} 66 |
67 |
68 |
69 | 70 |
71 |
72 |
-------------------------------------------------------------------------------- /expenses/expenses/print_format/expenses_request/expenses_request.json: -------------------------------------------------------------------------------- 1 | { 2 | "align_labels_right": 0, 3 | "creation": "2023-04-04 04:04:04.119400", 4 | "custom_format": 0, 5 | "default_print_language": "en", 6 | "disabled": 0, 7 | "doc_type": "Expenses Request", 8 | "docstatus": 0, 9 | "doctype": "Print Format", 10 | "font": "Default", 11 | "idx": 0, 12 | "line_breaks": 0, 13 | "css": ".text-start {\ntext-align: left;\n}\nhtml[dir=\"rtl\"] .text-start {\ntext-align: right;\n}\n.text-end {\ntext-align: right;\n}\nhtml[dir=\"rtl\"] .text-end {\ntext-align: left;\n}\n.table {\ntable-layout: auto;\nmargin-bottom: 0;\n}\n.table th, .table td {\nvertical-align: middle;\nwhite-space: nowrap;\ntext-align: center;\nwidth: auto;\n}\n.table th.w-15, .table td.w-15 {\nwidth: 15%;\n}\n.table th.fit, .table td.fit {\nwidth: 1%;\n}\n.subheader {\nfont-size: 1.1rem;\n}", 14 | "modified": "2024-05-22 04:04:04", 15 | "modified_by": "Administrator", 16 | "module": "Expenses", 17 | "name": "Expenses Request", 18 | "owner": "Administrator", 19 | "print_format_builder": 0, 20 | "print_format_type": "Jinja", 21 | "show_section_headings": 0, 22 | "standard": "Yes" 23 | } -------------------------------------------------------------------------------- /expenses/expenses/report/__init__.py: -------------------------------------------------------------------------------- 1 | # Expenses © 2024 2 | # Author: Ameen Ahmed 3 | # Company: Level Up Marketing & Software Development Services 4 | # Licence: Please refer to LICENSE file -------------------------------------------------------------------------------- /expenses/expenses/report/expenses_entry_report/__init__.py: -------------------------------------------------------------------------------- 1 | # Expenses © 2024 2 | # Author: Ameen Ahmed 3 | # Company: Level Up Marketing & Software Development Services 4 | # Licence: Please refer to LICENSE file -------------------------------------------------------------------------------- /expenses/expenses/report/expenses_entry_report/expenses_entry_report.html: -------------------------------------------------------------------------------- 1 |

{%= __("Expenses Entry Report") %}

2 | 3 |

{%= filters.company %}

4 | 5 |

6 | {% if (filters.mode_of_payment) { %} 7 | {%= __("Mode of Payment: ")%}{%= filters.mode_of_payment %} 8 | {% } %} 9 |

10 | 11 |
12 | {%= frappe.datetime.str_to_user(filters.from_date) %} 13 | {%= __("to") %} 14 | {%= frappe.datetime.str_to_user(filters.to_date) %} 15 |
16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | {% for(var i=0, l=data.length; i 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | {% } %} 43 | 44 |
{%= __("Expenses Entry") %}{%= __("Mode of Payment") %}{%= __("Posting Date") %}{%= __("Total") %}{%= __("Payment Reference") %}{%= __("Clearance Date") %}{%= __("Remarks") %}
{%= frappe.format(data[i].expenses_entry, {fieldtype: "Link"}) || " " %}{%= frappe.format(data[i].mode_of_payment, {fieldtype: "Link"}) || " " %}{%= frappe.datetime.str_to_user(data[i].posting_date) %}{%= format_currency(data[i].total, data[i].currency) %}{%= data[i].payment_reference || " " %}{%= frappe.datetime.str_to_user(data[i].clearance_date) || " " %}{%= data[i].payment_reference %}
45 | 46 |

{%= __("Printed On") %}: {%= frappe.datetime.str_to_user(frappe.datetime.get_datetime_as_string()) %}

-------------------------------------------------------------------------------- /expenses/expenses/report/expenses_entry_report/expenses_entry_report.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Expenses © 2024 3 | * Author: Ameen Ahmed 4 | * Company: Level Up Marketing & Software Development Services 5 | * Licence: Please refer to LICENSE file 6 | */ 7 | 8 | 9 | frappe.query_reports['Expenses Entry Report'] = { 10 | filters: [ 11 | { 12 | 'fieldname': 'company', 13 | 'label': __('Company'), 14 | 'fieldtype': 'Link', 15 | 'options': 'Company', 16 | 'default': frappe.defaults.get_user_default('Company'), 17 | 'reqd': 1 18 | }, 19 | { 20 | 'fieldname': 'from_date', 21 | 'label': __('From Date'), 22 | 'fieldtype': 'Date', 23 | 'default': frappe.datetime.add_months(frappe.datetime.get_today(), -1), 24 | 'reqd': 1, 25 | 'width': '60px' 26 | }, 27 | { 28 | 'fieldname': 'to_date', 29 | 'label': __('To Date'), 30 | 'fieldtype': 'Date', 31 | 'default': frappe.datetime.get_today(), 32 | 'reqd': 1, 33 | 'width': '60px' 34 | }, 35 | { 36 | 'fieldtype': 'Break', 37 | }, 38 | { 39 | 'fieldname': 'mode_of_payment', 40 | 'label': __('Mode of Payment'), 41 | 'fieldtype': 'Link', 42 | 'options': 'Mode of Payment' 43 | }, 44 | { 45 | 'fieldname': 'show_cancelled_entries', 46 | 'label': __('Show Cancelled Entries'), 47 | 'fieldtype': 'Check' 48 | }, 49 | ] 50 | }; -------------------------------------------------------------------------------- /expenses/expenses/report/expenses_entry_report/expenses_entry_report.json: -------------------------------------------------------------------------------- 1 | { 2 | "add_total_row": 0, 3 | "apply_user_permissions": 1, 4 | "creation": "2023-04-04 04:04:04.119400", 5 | "disabled": 0, 6 | "docstatus": 0, 7 | "doctype": "Report", 8 | "is_standard": "Yes", 9 | "modified": "2023-04-04 04:04:04.119400", 10 | "modified_by": "Administrator", 11 | "module": "Expenses", 12 | "name": "Expenses Entry Report", 13 | "owner": "Administrator", 14 | "ref_doctype": "Expenses Entry", 15 | "report_name": "Expenses Entry Report", 16 | "report_type": "Script Report", 17 | "roles": [ 18 | { 19 | "role": "Expenses Reviewer" 20 | }, 21 | { 22 | "role": "Accounts Manager" 23 | }, 24 | { 25 | "role": "System Manager" 26 | }, 27 | { 28 | "role": "Administrator" 29 | } 30 | ] 31 | } -------------------------------------------------------------------------------- /expenses/expenses/report/expenses_entry_report/expenses_entry_report.py: -------------------------------------------------------------------------------- 1 | # Expenses © 2024 2 | # Author: Ameen Ahmed 3 | # Company: Level Up Marketing & Software Development Services 4 | # Licence: Please refer to LICENSE file 5 | 6 | 7 | import frappe 8 | from frappe import _, throw 9 | from frappe.utils import cint, flt 10 | 11 | 12 | def execute(filters=None): 13 | if not filters: 14 | return [], [] 15 | 16 | from expenses.libs import get_cached_value 17 | 18 | validate_filters(filters) 19 | currency = get_cached_value( 20 | "Company", 21 | filters.get("company"), 22 | "default_currency" 23 | ) 24 | columns = get_columns(currency) 25 | data = get_result(filters, currency) 26 | totals = get_totals(data) 27 | chart = get_chart_data(totals, currency) 28 | summary = get_report_summary(totals, currency) 29 | return columns, data, None, chart, summary 30 | 31 | 32 | def validate_filters(filters): 33 | if not filters.get("company"): 34 | throw(_("{0} is mandatory").format(_("Company"))) 35 | 36 | if not filters.get("from_date") or not filters.get("to_date"): 37 | throw(_("{0} and {1} are mandatory").format( 38 | frappe.bold(_("From Date")), frappe.bold(_("To Date")) 39 | )) 40 | 41 | if filters.from_date > filters.to_date: 42 | throw(_("From Date must be before To Date")) 43 | 44 | 45 | def get_columns(currency): 46 | dt = "Expenses Entry" 47 | return [ 48 | { 49 | "label": _(dt), 50 | "fieldname": "expenses_entry", 51 | "fieldtype": "Link", 52 | "options": dt, 53 | }, 54 | { 55 | "label": _("Mode of Payment"), 56 | "fieldname": "mode_of_payment", 57 | "width": 100, 58 | }, 59 | { 60 | "label": _("Posting Date"), 61 | "fieldname": "posting_date", 62 | "fieldtype": "Date", 63 | "width": 90, 64 | }, 65 | { 66 | "label": _("Total ({0})").format(currency), 67 | "fieldname": "total", 68 | "fieldtype": "Float", 69 | "width": 100, 70 | }, 71 | { 72 | "label": _("Payment Reference"), 73 | "fieldname": "payment_reference", 74 | "width": 100, 75 | }, 76 | { 77 | "label": _("Clearance Date"), 78 | "fieldname": "clearance_date", 79 | "fieldtype": "Date", 80 | "width": 90, 81 | }, 82 | { 83 | "label": _("Remarks"), 84 | "fieldname": "remarks", 85 | "width": 400, 86 | }, 87 | ] 88 | 89 | 90 | def get_result(filters, currency): 91 | doc = frappe.qb.DocType("Expenses Entry") 92 | qry = ( 93 | frappe.qb.from_(doc) 94 | .select( 95 | doc.name.as_("expenses_entry"), 96 | doc.mode_of_payment, 97 | doc.posting_date, 98 | doc.total, 99 | doc.payment_reference, 100 | doc.clearance_date, 101 | doc.remarks 102 | ) 103 | .where(doc.company == filters.get("company")) 104 | .where(doc.posting_date.between([ 105 | filters.get("from_date"), 106 | filters.get("to_date") 107 | ])) 108 | .orderby(doc.posting_date, doc.creation) 109 | ) 110 | 111 | if (mode_of_payment := filters.get("mode_of_payment")): 112 | qry = qry.where(doc.mode_of_payment == mode_of_payment) 113 | 114 | if cint(filters.get("show_cancelled_entries")): 115 | qry = qry.where(doc.docstatus > 0) 116 | else: 117 | qry = qry.where(doc.docstatus == 1) 118 | 119 | data = qry.run(as_dict=True) 120 | for i in range(len(data)): 121 | data[i]["currency"] = currency 122 | 123 | return data 124 | 125 | 126 | def get_totals(data): 127 | totals = {"*": 0} 128 | for v in get_mode_of_payments(): 129 | totals[v] = 0 130 | for v in data: 131 | totals["*"] += flt(v["total"]) 132 | if v["mode_of_payment"] in totals: 133 | totals[v["mode_of_payment"]] += flt(v["total"]) 134 | 135 | return totals 136 | 137 | 138 | def get_chart_data(totals, currency): 139 | labels = [] 140 | datasets = [] 141 | for k, v in totals.items(): 142 | if k != "*": 143 | labels.append(k) 144 | datasets.append({ 145 | "name": k, 146 | "values": [v], 147 | }) 148 | 149 | return { 150 | "data": { 151 | "labels": labels, 152 | "datasets": datasets 153 | }, 154 | "type": "bar", 155 | "fieldtype": "Currency", 156 | "currency": currency, 157 | } 158 | 159 | 160 | def get_report_summary(totals, currency): 161 | summary = [] 162 | for k, v in totals.items(): 163 | label = "Total Expenses" 164 | if k != "*": 165 | label += f" ({k})" 166 | summary.append({ 167 | "value": v, 168 | "label": _(label), 169 | "datatype": "Currency", 170 | "currency": currency, 171 | }) 172 | 173 | return summary 174 | 175 | 176 | def get_mode_of_payments(): 177 | return frappe.list_all( 178 | "Mode of Payment", 179 | fields=["name"], 180 | filters={ 181 | "type": ["in", ["Bank", "Cash"]] 182 | }, 183 | pluck="name" 184 | ) -------------------------------------------------------------------------------- /expenses/fixtures/role.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "docstatus": 0, 4 | "doctype": "Role", 5 | "creation": "2024-05-11 04:04:04", 6 | "modified": "2024-05-11 04:04:04", 7 | "name": "Expense Manager", 8 | "role_name": "Expense Manager", 9 | "home_page": "/desk", 10 | "disabled": 0, 11 | "is_custom": 1, 12 | "desk_access": 1, 13 | "two_factor_auth": 0, 14 | "search_bar": 1, 15 | "notifications": 1, 16 | "list_sidebar": 1, 17 | "bulk_actions": 1, 18 | "view_switcher": 1, 19 | "form_sidebar": 1, 20 | "timeline": 1, 21 | "dashboard": 1 22 | }, 23 | { 24 | "docstatus": 0, 25 | "doctype": "Role", 26 | "creation": "2023-04-04 04:04:04", 27 | "modified": "2023-04-04 04:04:04", 28 | "name": "Expense Moderator", 29 | "role_name": "Expense Moderator", 30 | "home_page": "/desk", 31 | "disabled": 0, 32 | "is_custom": 1, 33 | "desk_access": 1, 34 | "two_factor_auth": 0, 35 | "search_bar": 1, 36 | "notifications": 1, 37 | "list_sidebar": 1, 38 | "bulk_actions": 1, 39 | "view_switcher": 1, 40 | "form_sidebar": 1, 41 | "timeline": 1, 42 | "dashboard": 1 43 | }, 44 | { 45 | "docstatus": 0, 46 | "doctype": "Role", 47 | "creation": "2023-04-04 04:04:04", 48 | "modified": "2023-04-04 04:04:04", 49 | "name": "Expenses Request Moderator", 50 | "role_name": "Expenses Request Moderator", 51 | "home_page": "/desk", 52 | "disabled": 0, 53 | "is_custom": 1, 54 | "desk_access": 1, 55 | "two_factor_auth": 0, 56 | "search_bar": 1, 57 | "notifications": 1, 58 | "list_sidebar": 1, 59 | "bulk_actions": 1, 60 | "view_switcher": 1, 61 | "form_sidebar": 1, 62 | "timeline": 1, 63 | "dashboard": 1 64 | }, 65 | { 66 | "docstatus": 0, 67 | "doctype": "Role", 68 | "creation": "2023-04-04 04:04:04", 69 | "modified": "2023-04-04 04:04:04", 70 | "name": "Expenses Request Reviewer", 71 | "role_name": "Expenses Request Reviewer", 72 | "home_page": "/desk", 73 | "disabled": 0, 74 | "is_custom": 1, 75 | "desk_access": 1, 76 | "two_factor_auth": 0, 77 | "search_bar": 1, 78 | "notifications": 1, 79 | "list_sidebar": 1, 80 | "bulk_actions": 1, 81 | "view_switcher": 1, 82 | "form_sidebar": 1, 83 | "timeline": 1, 84 | "dashboard": 1 85 | }, 86 | { 87 | "docstatus": 0, 88 | "doctype": "Role", 89 | "creation": "2023-04-04 04:04:04", 90 | "modified": "2023-04-04 04:04:04", 91 | "name": "Expenses Entry Moderator", 92 | "role_name": "Expenses Entry Moderator", 93 | "home_page": "/desk", 94 | "disabled": 0, 95 | "is_custom": 1, 96 | "desk_access": 1, 97 | "two_factor_auth": 0, 98 | "search_bar": 1, 99 | "notifications": 1, 100 | "list_sidebar": 1, 101 | "bulk_actions": 1, 102 | "view_switcher": 1, 103 | "form_sidebar": 1, 104 | "timeline": 1, 105 | "dashboard": 1 106 | } 107 | ] -------------------------------------------------------------------------------- /expenses/fixtures/workflow.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "docstatus": 0, 4 | "doctype": "Workflow", 5 | "document_type": "Expenses Request", 6 | "is_active": 1, 7 | "creation": "2023-04-04 04:04:04", 8 | "modified": "2023-04-04 04:04:04", 9 | "name": "Expenses Request Review", 10 | "override_status": 0, 11 | "send_email_alert": 0, 12 | "states": [ 13 | { 14 | "allow_edit": "Accounts User", 15 | "doc_status": "0", 16 | "is_optional_state": 0, 17 | "message": "Draft", 18 | "parent": "Expenses Request Review", 19 | "parentfield": "states", 20 | "parenttype": "Workflow", 21 | "state": "Draft", 22 | "update_field": "status", 23 | "update_value": "Draft" 24 | }, 25 | { 26 | "allow_edit": "Accounts User", 27 | "doc_status": "1", 28 | "is_optional_state": 0, 29 | "message": "Pending", 30 | "parent": "Expenses Request Review", 31 | "parentfield": "states", 32 | "parenttype": "Workflow", 33 | "state": "Pending", 34 | "update_field": "status", 35 | "update_value": "Pending" 36 | }, 37 | { 38 | "allow_edit": "Accounts User", 39 | "doc_status": "2", 40 | "is_optional_state": 1, 41 | "message": "Cancelled", 42 | "parent": "Expenses Request Review", 43 | "parentfield": "states", 44 | "parenttype": "Workflow", 45 | "state": "Cancelled", 46 | "update_field": "status", 47 | "update_value": "Cancelled" 48 | }, 49 | { 50 | "allow_edit": "Expenses Request Reviewer", 51 | "doc_status": "1", 52 | "is_optional_state": 1, 53 | "message": "Approved", 54 | "parent": "Expenses Request Review", 55 | "parentfield": "states", 56 | "parenttype": "Workflow", 57 | "state": "Approved", 58 | "update_field": "status", 59 | "update_value": "Approved" 60 | }, 61 | { 62 | "allow_edit": "Accounts Manager", 63 | "doc_status": "1", 64 | "is_optional_state": 1, 65 | "message": "Approved", 66 | "parent": "Expenses Request Review", 67 | "parentfield": "states", 68 | "parenttype": "Workflow", 69 | "state": "Approved", 70 | "update_field": "status", 71 | "update_value": "Approved" 72 | }, 73 | { 74 | "allow_edit": "Expenses Request Reviewer", 75 | "doc_status": "2", 76 | "is_optional_state": 1, 77 | "message": "Rejected", 78 | "parent": "Expenses Request Review", 79 | "parentfield": "states", 80 | "parenttype": "Workflow", 81 | "state": "Rejected", 82 | "update_field": "status", 83 | "update_value": "Rejected" 84 | }, 85 | { 86 | "allow_edit": "Accounts Manager", 87 | "doc_status": "2", 88 | "is_optional_state": 1, 89 | "message": "Rejected", 90 | "parent": "Expenses Request Review", 91 | "parentfield": "states", 92 | "parenttype": "Workflow", 93 | "state": "Rejected", 94 | "update_field": "status", 95 | "update_value": "Rejected" 96 | }, 97 | { 98 | "allow_edit": "Expenses Request Reviewer", 99 | "doc_status": "1", 100 | "is_optional_state": 1, 101 | "message": "Processed", 102 | "parent": "Expenses Request Review", 103 | "parentfield": "states", 104 | "parenttype": "Workflow", 105 | "state": "Processed", 106 | "update_field": "status", 107 | "update_value": "Processed" 108 | }, 109 | { 110 | "allow_edit": "Accounts Manager", 111 | "doc_status": "1", 112 | "is_optional_state": 1, 113 | "message": "Processed", 114 | "parent": "Expenses Request Review", 115 | "parentfield": "states", 116 | "parenttype": "Workflow", 117 | "state": "Processed", 118 | "update_field": "status", 119 | "update_value": "Processed" 120 | } 121 | ], 122 | "transitions": [ 123 | { 124 | "action": "Submit", 125 | "allow_self_approval": 1, 126 | "allowed": "Accounts User", 127 | "next_state": "Pending", 128 | "parent": "Expenses Request Review", 129 | "parentfield": "transitions", 130 | "parenttype": "Workflow", 131 | "state": "Draft" 132 | }, 133 | { 134 | "action": "Cancel", 135 | "allow_self_approval": 1, 136 | "allowed": "Accounts User", 137 | "next_state": "Cancelled", 138 | "parent": "Expenses Request Review", 139 | "parentfield": "transitions", 140 | "parenttype": "Workflow", 141 | "state": "Pending" 142 | }, 143 | { 144 | "action": "Cancel", 145 | "allow_self_approval": 0, 146 | "allowed": "Accounts Manager", 147 | "next_state": "Cancelled", 148 | "parent": "Expenses Request Review", 149 | "parentfield": "transitions", 150 | "parenttype": "Workflow", 151 | "state": "Pending" 152 | }, 153 | { 154 | "action": "Cancel", 155 | "allow_self_approval": 0, 156 | "allowed": "System Manager", 157 | "next_state": "Cancelled", 158 | "parent": "Expenses Request Review", 159 | "parentfield": "transitions", 160 | "parenttype": "Workflow", 161 | "state": "Pending" 162 | }, 163 | { 164 | "action": "Cancel", 165 | "allow_self_approval": 0, 166 | "allowed": "Administrator", 167 | "next_state": "Cancelled", 168 | "parent": "Expenses Request Review", 169 | "parentfield": "transitions", 170 | "parenttype": "Workflow", 171 | "state": "Pending" 172 | }, 173 | { 174 | "action": "Approve", 175 | "allow_self_approval": 0, 176 | "allowed": "Expenses Request Reviewer", 177 | "next_state": "Approved", 178 | "parent": "Expenses Request Review", 179 | "parentfield": "transitions", 180 | "parenttype": "Workflow", 181 | "state": "Pending" 182 | }, 183 | { 184 | "action": "Approve", 185 | "allow_self_approval": 0, 186 | "allowed": "Accounts Manager", 187 | "next_state": "Approved", 188 | "parent": "Expenses Request Review", 189 | "parentfield": "transitions", 190 | "parenttype": "Workflow", 191 | "state": "Pending" 192 | }, 193 | { 194 | "action": "Approve", 195 | "allow_self_approval": 0, 196 | "allowed": "System Manager", 197 | "next_state": "Approved", 198 | "parent": "Expenses Request Review", 199 | "parentfield": "transitions", 200 | "parenttype": "Workflow", 201 | "state": "Pending" 202 | }, 203 | { 204 | "action": "Approve", 205 | "allow_self_approval": 0, 206 | "allowed": "Administrator", 207 | "next_state": "Approved", 208 | "parent": "Expenses Request Review", 209 | "parentfield": "transitions", 210 | "parenttype": "Workflow", 211 | "state": "Pending" 212 | }, 213 | { 214 | "action": "Reject", 215 | "allow_self_approval": 0, 216 | "allowed": "Expenses Request Reviewer", 217 | "next_state": "Rejected", 218 | "parent": "Expenses Request Review", 219 | "parentfield": "transitions", 220 | "parenttype": "Workflow", 221 | "state": "Pending" 222 | }, 223 | { 224 | "action": "Reject", 225 | "allow_self_approval": 0, 226 | "allowed": "Accounts Manager", 227 | "next_state": "Rejected", 228 | "parent": "Expenses Request Review", 229 | "parentfield": "transitions", 230 | "parenttype": "Workflow", 231 | "state": "Pending" 232 | }, 233 | { 234 | "action": "Reject", 235 | "allow_self_approval": 0, 236 | "allowed": "System Manager", 237 | "next_state": "Rejected", 238 | "parent": "Expenses Request Review", 239 | "parentfield": "transitions", 240 | "parenttype": "Workflow", 241 | "state": "Pending" 242 | }, 243 | { 244 | "action": "Reject", 245 | "allow_self_approval": 0, 246 | "allowed": "Administrator", 247 | "next_state": "Rejected", 248 | "parent": "Expenses Request Review", 249 | "parentfield": "transitions", 250 | "parenttype": "Workflow", 251 | "state": "Pending" 252 | }, 253 | { 254 | "action": "Reject", 255 | "allow_self_approval": 0, 256 | "allowed": "Accounts Manager", 257 | "next_state": "Rejected", 258 | "parent": "Expenses Request Review", 259 | "parentfield": "transitions", 260 | "parenttype": "Workflow", 261 | "state": "Approved" 262 | }, 263 | { 264 | "action": "Reject", 265 | "allow_self_approval": 0, 266 | "allowed": "System Manager", 267 | "next_state": "Rejected", 268 | "parent": "Expenses Request Review", 269 | "parentfield": "transitions", 270 | "parenttype": "Workflow", 271 | "state": "Approved" 272 | }, 273 | { 274 | "action": "Reject", 275 | "allow_self_approval": 0, 276 | "allowed": "Administrator", 277 | "next_state": "Rejected", 278 | "parent": "Expenses Request Review", 279 | "parentfield": "transitions", 280 | "parenttype": "Workflow", 281 | "state": "Approved" 282 | } 283 | ], 284 | "workflow_name": "Expenses Request Review", 285 | "workflow_state_field": "workflow_state" 286 | } 287 | ] -------------------------------------------------------------------------------- /expenses/fixtures/workflow_action_master.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "docstatus": 0, 4 | "doctype": "Workflow Action Master", 5 | "creation": "2023-04-04 04:04:04", 6 | "modified": "2023-04-04 04:04:04", 7 | "name": "Submit", 8 | "workflow_action_name": "Submit" 9 | }, 10 | { 11 | "docstatus": 0, 12 | "doctype": "Workflow Action Master", 13 | "creation": "2023-04-04 04:04:04", 14 | "modified": "2023-04-04 04:04:04", 15 | "name": "Cancel", 16 | "workflow_action_name": "Cancel" 17 | }, 18 | { 19 | "docstatus": 0, 20 | "doctype": "Workflow Action Master", 21 | "creation": "2023-04-04 04:04:04", 22 | "modified": "2023-04-04 04:04:04", 23 | "name": "Approve", 24 | "workflow_action_name": "Approve" 25 | }, 26 | { 27 | "docstatus": 0, 28 | "doctype": "Workflow Action Master", 29 | "creation": "2023-04-04 04:04:04", 30 | "modified": "2023-04-04 04:04:04", 31 | "name": "Reject", 32 | "workflow_action_name": "Reject" 33 | } 34 | ] -------------------------------------------------------------------------------- /expenses/fixtures/workflow_state.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "docstatus": 0, 4 | "doctype": "Workflow State", 5 | "icon": "file", 6 | "creation": "2023-04-04 04:04:04", 7 | "modified": "2023-04-04 04:04:04", 8 | "name": "Draft", 9 | "style": "", 10 | "workflow_state_name": "Draft" 11 | }, 12 | { 13 | "docstatus": 0, 14 | "doctype": "Workflow State", 15 | "icon": "time", 16 | "creation": "2023-04-04 04:04:04", 17 | "modified": "2023-04-04 04:04:04", 18 | "name": "Pending", 19 | "style": "Warning", 20 | "workflow_state_name": "Pending" 21 | }, 22 | { 23 | "docstatus": 0, 24 | "doctype": "Workflow State", 25 | "icon": "remove", 26 | "creation": "2023-04-04 04:04:04", 27 | "modified": "2023-04-04 04:04:04", 28 | "name": "Cancelled", 29 | "style": "Danger", 30 | "workflow_state_name": "Cancelled" 31 | }, 32 | { 33 | "docstatus": 0, 34 | "doctype": "Workflow State", 35 | "icon": "ok-circle", 36 | "creation": "2023-04-04 04:04:04", 37 | "modified": "2023-04-04 04:04:04", 38 | "name": "Approved", 39 | "style": "Success", 40 | "workflow_state_name": "Approved" 41 | }, 42 | { 43 | "docstatus": 0, 44 | "doctype": "Workflow State", 45 | "icon": "ban-circle", 46 | "creation": "2023-04-04 04:04:04", 47 | "modified": "2023-04-04 04:04:04", 48 | "name": "Rejected", 49 | "style": "Danger", 50 | "workflow_state_name": "Rejected" 51 | }, 52 | { 53 | "docstatus": 0, 54 | "doctype": "Workflow State", 55 | "icon": "check", 56 | "creation": "2023-04-04 04:04:04", 57 | "modified": "2023-04-04 04:04:04", 58 | "name": "Processed", 59 | "style": "Primary", 60 | "workflow_state_name": "Processed" 61 | } 62 | ] -------------------------------------------------------------------------------- /expenses/hooks.py: -------------------------------------------------------------------------------- 1 | # Expenses © 2024 2 | # Author: Ameen Ahmed 3 | # Company: Level Up Marketing & Software Development Services 4 | # Licence: Please refer to LICENSE file 5 | 6 | 7 | app_name = "expenses" 8 | app_title = "Expenses" 9 | app_publisher = "Ameen Ahmed (Level Up)" 10 | app_description = "An expenses management module for ERPNext." 11 | app_icon = "octicon octicon-note" 12 | app_color = "blue" 13 | app_email = "kid1194@gmail.com" 14 | app_license = "MIT" 15 | 16 | 17 | doctype_js = { 18 | "Expenses Settings": "public/js/expenses.bundle.js", 19 | "Expense Type": "public/js/expenses.bundle.js", 20 | "Expense Item": "public/js/expenses.bundle.js", 21 | "Expense": "public/js/expenses.bundle.js", 22 | "Expenses Request": "public/js/expenses.bundle.js", 23 | "Expenses Entry": "public/js/expenses.bundle.js", 24 | } 25 | 26 | 27 | doctype_list_js = { 28 | "Expense Item": "public/js/expenses.bundle.js", 29 | "Expense Type": "public/js/expenses.bundle.js", 30 | "Expense": "public/js/expenses.bundle.js", 31 | "Expenses Request": "public/js/expenses.bundle.js", 32 | "Expenses Entry": "public/js/expenses.bundle.js", 33 | } 34 | 35 | 36 | before_install = "expenses.setup.install.before_install" 37 | after_sync = "expenses.setup.install.after_sync" 38 | after_migrate = "expenses.setup.migrate.after_migrate" 39 | after_uninstall = "expenses.setup.uninstall.after_uninstall" 40 | 41 | 42 | fixtures = [ 43 | "Role", 44 | "Workflow", 45 | "Workflow State", 46 | "Workflow Action Master" 47 | ] 48 | 49 | 50 | scheduler_events = { 51 | "daily": [ 52 | "expenses.libs.update.auto_check_for_update" 53 | ] 54 | } 55 | 56 | 57 | treeviews = [ 58 | "Expense Type" 59 | ] 60 | 61 | 62 | required_apps = ["erpnext"] -------------------------------------------------------------------------------- /expenses/libs/__init__.py: -------------------------------------------------------------------------------- 1 | # Expenses © 2024 2 | # Author: Ameen Ahmed 3 | # Company: Level Up Marketing & Software Development Services 4 | # Licence: Please refer to LICENSE file 5 | 6 | 7 | from .account import ( 8 | get_account_currency, 9 | get_accounts_currencies 10 | ) 11 | from .attachment import delete_attach_files 12 | from .cache import * 13 | from .check import * 14 | from .common import * 15 | from .company import get_company_currency 16 | from .entry import ( 17 | get_mode_of_payment_data, 18 | is_entry_moderator, 19 | entry_form_setup, 20 | get_request_data 21 | ) 22 | from .exchange import get_exchange_rate 23 | from .expense import ( 24 | ExpenseStatus, 25 | item_expense_data, 26 | expense_form_setup, 27 | is_expense_moderator, 28 | has_expense_claim, 29 | expense_claim_reqd_if_paid, 30 | is_valid_claim, 31 | expense_requests_exists, 32 | expense_entries_exists, 33 | get_expenses_data 34 | ) 35 | from .filter import * 36 | from .item import ( 37 | search_item_types, 38 | get_type_accounts_list, 39 | search_items 40 | ) 41 | from .journal import ( 42 | enqueue_journal_entry, 43 | cancel_journal_entry 44 | ) 45 | from .logger import ( 46 | get_log_files, 47 | load_log_file 48 | ) 49 | from .realtime import * 50 | from .request import ( 51 | RequestStatus, 52 | get_filtered_company_expenses, 53 | is_request_amended, 54 | restore_expenses, 55 | request_expenses, 56 | approve_expenses, 57 | reject_expenses, 58 | request_form_setup, 59 | is_request_moderator, 60 | is_request_reviewer, 61 | get_expenses_data, 62 | search_company_expenses, 63 | filter_company_expenses, 64 | reject_request, 65 | process_request, 66 | reject_request 67 | ) 68 | from .system import ( 69 | settings, 70 | is_enabled, 71 | check_app_status 72 | ) 73 | from .type import ( 74 | disable_type_descendants, 75 | reload_type_linked_items, 76 | search_types, 77 | get_companies_accounts, 78 | convert_group_to_item, 79 | convert_item_to_group, 80 | get_type_children 81 | ) 82 | from .update import check_for_update -------------------------------------------------------------------------------- /expenses/libs/account.py: -------------------------------------------------------------------------------- 1 | # Expenses © 2024 2 | # Author: Ameen Ahmed 3 | # Company: Level Up Marketing & Software Development Services 4 | # Licence: Please refer to LICENSE file 5 | 6 | 7 | import frappe 8 | 9 | 10 | # [Item] 11 | def get_type_accounts(type_name: str, defs: dict=None): 12 | from .cache import get_cache 13 | 14 | dt = "Expense Type" 15 | key = f"{type_name}-expense-accounts" 16 | data = get_cache(dt, key) 17 | if data: 18 | return data 19 | 20 | raw = get_type_accounts_data(type_name) 21 | if not raw: 22 | return raw 23 | 24 | from .cache import set_cache 25 | 26 | data = [] 27 | exists = [] 28 | for i in range(len(raw)): 29 | v = raw.pop(0) 30 | if v["company"] in exists: 31 | continue 32 | 33 | exists.append(v["company"]) 34 | if defs: 35 | v.update(defs) 36 | 37 | data.append(v) 38 | 39 | exists.clear() 40 | set_cache(dt, key, data) 41 | return data 42 | 43 | 44 | # [Internal] 45 | def get_type_accounts_data(type_name: str, company: str=None): 46 | from .cache import get_cached_value 47 | 48 | dt = "Expense Type" 49 | parent = get_cached_value(dt, type_name, ["lft", "rgt"]) 50 | if not parent: 51 | return None 52 | 53 | from pypika.enums import Order 54 | 55 | doc = frappe.qb.DocType(f"{dt} Account") 56 | pdoc = frappe.qb.DocType(dt) 57 | adoc = frappe.qb.DocType("Account") 58 | qry = ( 59 | frappe.qb.from_(doc) 60 | .select( 61 | doc.company, 62 | doc.account, 63 | adoc.account_currency.as_("currency") 64 | ) 65 | .inner_join(pdoc) 66 | .on(pdoc.name == doc.parent) 67 | .left_join(adoc) 68 | .on(adoc.name == doc.account) 69 | .where(doc.parenttype == dt) 70 | .where(doc.parentfield == "expense_accounts") 71 | .where(pdoc.disabled == 0) 72 | .where(pdoc.lft.lte(parent.lft)) 73 | .where(pdoc.rgt.gte(parent.rgt)) 74 | .orderby(pdoc.rgt - pdoc.lft, order=Order.asc) 75 | ) 76 | if company: 77 | qry = qry.where(doc.company == company) 78 | qry = qry.limit(1) 79 | 80 | data = qry.run(as_dict=True) 81 | if not data or not isinstance(data, list): 82 | return None if company else [] 83 | 84 | return data.pop(0) if company else data 85 | 86 | 87 | # [Item] 88 | def query_items_with_company_account(company: str): 89 | dt = "Expense Item" 90 | doc = frappe.qb.DocType(f"{dt} Account") 91 | pdoc = frappe.qb.DocType(dt).as_("parent") 92 | return ( 93 | frappe.qb.from_(doc) 94 | .select(doc.parent) 95 | .distinct() 96 | .inner_join(pdoc) 97 | .on(pdoc.name == doc.parent) 98 | .where(doc.parenttype == dt) 99 | .where(doc.parentfield == "expense_accounts") 100 | .where(doc.company == company) 101 | .where(pdoc.disabled == 0) 102 | ) 103 | 104 | 105 | # [Item] 106 | def get_item_company_account_data(parent: str, company: str): 107 | dt = "Expense Item" 108 | doc = frappe.qb.DocType(f"{dt} Account") 109 | pdoc = frappe.qb.DocType(dt) 110 | adoc = frappe.qb.DocType("Account") 111 | data = ( 112 | frappe.qb.from_(doc) 113 | .select( 114 | pdoc.uom, 115 | doc.account, 116 | adoc.account_currency.as_("currency"), 117 | doc.cost, 118 | doc.min_cost, 119 | doc.max_cost, 120 | doc.qty, 121 | doc.min_qty, 122 | doc.max_qty 123 | ) 124 | .inner_join(pdoc) 125 | .on(pdoc.name == doc.parent) 126 | .inner_join(adoc) 127 | .on(adoc.name == doc.account) 128 | .where(doc.parenttype == dt) 129 | .where(doc.parentfield == "expense_accounts") 130 | .where(doc.parent == parent) 131 | .where(doc.company == company) 132 | .limit(1) 133 | ).run(as_dict=True) 134 | if not data or not isinstance(data, list): 135 | return None 136 | 137 | from frappe.utils import flt 138 | 139 | data = data.pop(0) 140 | for k in ["cost", "qty"]: 141 | for x in [k, f"min_{k}", f"max_{k}"]: 142 | data[x] = flt(data[x]) 143 | if data[x] < 0.0: 144 | data[x] = 0.0 145 | 146 | return data 147 | 148 | 149 | # [E Entry, Entry] 150 | def get_account_currency(account: str): 151 | return frappe.db.get_value("Account", account, "account_currency") 152 | 153 | 154 | # [E Entry, Internal] 155 | def get_accounts_currencies(accounts: list): 156 | doc = frappe.qb.DocType("Account") 157 | cdoc = frappe.qb.DocType("Currency") 158 | data = ( 159 | frappe.qb.from_(doc) 160 | .select( 161 | doc.name, 162 | doc.account_currency 163 | ) 164 | .inner_join(cdoc) 165 | .on(cdoc.name == doc.account_currency) 166 | .where(doc.name.isin(accounts)) 167 | .where(cdoc.enabled == 1) 168 | ).run(as_dict=True) 169 | if not data or not isinstance(data, list): 170 | return None 171 | 172 | return {v["name"]:v["account_currency"] for v in data} -------------------------------------------------------------------------------- /expenses/libs/attachment.py: -------------------------------------------------------------------------------- 1 | # Expenses © 2024 2 | # Author: Ameen Ahmed 3 | # Company: Level Up Marketing & Software Development Services 4 | # Licence: Please refer to LICENSE file 5 | 6 | 7 | import frappe 8 | 9 | 10 | # [E Entry, E Entry Form, E Expense, E Expense Form] 11 | @frappe.whitelist(methods=["POST"]) 12 | def delete_attach_files(doctype, name, files): 13 | if ( 14 | not doctype or not isinstance(doctype, str) or 15 | not name or not isinstance(name, str) or 16 | not files or not isinstance(files, (str, list)) 17 | ): 18 | return 0 19 | 20 | from .common import json_to_list 21 | 22 | files = json_to_list(files) 23 | 24 | if not files or not isinstance(files, list): 25 | return 0 26 | 27 | from .background import ( 28 | uuid_key, 29 | is_job_running 30 | ) 31 | 32 | job_id = uuid_key([doctype, files]) 33 | job_id = f"exp-files-delete-{job_id}" 34 | if is_job_running(job_id): 35 | return 1 36 | 37 | files = frappe.get_all( 38 | "File", 39 | fields=["name"], 40 | filters=[ 41 | ["file_url", "in", files], 42 | ["attached_to_doctype", "=", doctype], 43 | ["ifnull(`attached_to_name`,\"\")", "in", [name, ""]] 44 | ], 45 | pluck="name", 46 | ignore_permissions=True, 47 | strict=False 48 | ) 49 | if not files or not isinstance(files, list): 50 | return 0 51 | 52 | from .background import enqueue_job 53 | 54 | enqueue_job( 55 | "expenses.libs.attachment.files_delete", 56 | job_id, 57 | timeout=len(files) * 3, 58 | files=files 59 | ) 60 | 61 | return 1 62 | 63 | 64 | # [Internal] 65 | def files_delete(files: list): 66 | files = list(set(files)) 67 | for i in range(len(files)): 68 | frappe.get_doc("File", files.pop(0)).delete(ignore_permissions=True) 69 | 70 | 71 | # [Expense] 72 | def get_files_by_parents(parents: list, parent_type: str, parent_field: str): 73 | dt = "Expense Attachment" 74 | raw = frappe.get_all( 75 | dt, 76 | fields=["parent", "file", "description"], 77 | filters=[ 78 | [dt, "parent", "in", parents], 79 | [dt, "parenttype", "=", parent_type], 80 | [dt, "parentfield", "=", parent_field] 81 | ], 82 | ignore_permissions=True, 83 | strict=False 84 | ) 85 | if not raw or not isinstance(raw, list): 86 | return None 87 | 88 | data = {} 89 | for i in range(len(raw)): 90 | v = raw.pop(0) 91 | k = v.pop("parent") 92 | if k not in data: 93 | data[k] = [] 94 | 95 | data[k].append(v) 96 | 97 | return data -------------------------------------------------------------------------------- /expenses/libs/background.py: -------------------------------------------------------------------------------- 1 | # Expenses © 2024 2 | # Author: Ameen Ahmed 3 | # Company: Level Up Marketing & Software Development Services 4 | # Licence: Please refer to LICENSE file 5 | 6 | 7 | import frappe 8 | 9 | from expenses.version import is_version_gt 10 | 11 | 12 | # [Attachment, Expense, Item] 13 | def uuid_key(args): 14 | import hashlib 15 | import uuid 16 | 17 | from frappe.utils import cstr 18 | 19 | from .common import to_json 20 | 21 | return cstr(uuid.UUID(hashlib.sha256( 22 | to_json(args, "").encode("utf-8") 23 | ).hexdigest()[::2])) 24 | 25 | 26 | # [Attachment, Expense, Item, Journal, Update] 27 | def is_job_running(name: str): 28 | if is_version_gt(14): 29 | from frappe.utils.background_jobs import is_job_enqueued 30 | 31 | return is_job_enqueued(name) 32 | 33 | else: 34 | from frappe.core.page.background_jobs.background_jobs import get_info 35 | 36 | jobs = [d.get("job_name") for d in get_info("Jobs", job_status="active")] 37 | return True if name in jobs else False 38 | 39 | 40 | # [Attachment, Expense, Item, Journal, Update] 41 | def enqueue_job(method: str, job_name: str, **kwargs): 42 | if "timeout" in kwargs and "queue" not in kwargs: 43 | from frappe.utils import cint 44 | 45 | if cint(kwargs["timeout"]) >= 1500: 46 | kwargs["queue"] = "long" 47 | else: 48 | kwargs["queue"] = "short" 49 | 50 | if is_version_gt(14): 51 | frappe.enqueue( 52 | method, 53 | job_id=job_name, 54 | is_async=True, 55 | **kwargs 56 | ) 57 | 58 | else: 59 | frappe.enqueue( 60 | method, 61 | job_name=job_name, 62 | is_async=True, 63 | **kwargs 64 | ) -------------------------------------------------------------------------------- /expenses/libs/cache.py: -------------------------------------------------------------------------------- 1 | # Expenses © 2024 2 | # Author: Ameen Ahmed 3 | # Company: Level Up Marketing & Software Development Services 4 | # Licence: Please refer to LICENSE file 5 | 6 | 7 | import frappe 8 | 9 | 10 | # [Account, Expense, Item, Type] 11 | def get_cache(dt: str, key: str, expires=False): 12 | return frappe.cache().get_value(f"{dt}-{key}", expires=expires) 13 | 14 | 15 | # [Account, Expense, Item, Type] 16 | def set_cache(dt: str, key: str, data, expiry: int=None): 17 | frappe.cache().set_value(f"{dt}-{key}", data, expires_in_sec=expiry) 18 | 19 | 20 | # [Entry, Request, Settings, Type, Internal] 21 | def get_cached_doc(dt: str, name: str=None): 22 | if name is None: 23 | name = dt 24 | 25 | if dt != name and not frappe.db.exists(dt, name): 26 | return None 27 | 28 | return frappe.get_cached_doc(dt, name) 29 | 30 | 31 | # [E Entry, E Expense, E Item, E Request, E Settings, E Type] 32 | def clear_doc_cache(dt: str, name: str=None): 33 | frappe.cache().delete_keys(dt) 34 | frappe.clear_cache(doctype=dt) 35 | frappe.clear_document_cache(dt, name or dt) 36 | 37 | 38 | # [] 39 | def get_cached_value(dt: str, name: str, field, raw: bool=False): 40 | if not field or not isinstance(field, (str, list)): 41 | return None 42 | 43 | doc = get_cached_doc(dt, name) 44 | if not doc: 45 | return None 46 | 47 | if isinstance(field, str): 48 | return doc.get(field) 49 | 50 | values = {} 51 | for f in field: 52 | if f and isinstance(f, str): 53 | values[f] = doc.get(f, None) 54 | 55 | if not values: 56 | return None 57 | 58 | return values if raw else frappe._dict(values) -------------------------------------------------------------------------------- /expenses/libs/check.py: -------------------------------------------------------------------------------- 1 | # Expenses © 2024 2 | # Author: Ameen Ahmed 3 | # Company: Level Up Marketing & Software Development Services 4 | # Licence: Please refer to LICENSE file 5 | 6 | 7 | import frappe 8 | 9 | 10 | # [E Settings, Update] 11 | def user_exists(name: str, attrs: dict=None, enabled: bool=None): 12 | return _exists("User", name, attrs, enabled, "enabled", 1) 13 | 14 | 15 | # [E Type] 16 | def type_children_exists(parent: str): 17 | return _has("Expense Type", {"parent_type": parent}) 18 | 19 | 20 | # [E Type] 21 | def type_items_exists(expense_type: str): 22 | return _has("Expense Item", {"expense_type": expense_type}) 23 | 24 | 25 | # [E Item, E Type] 26 | def type_exists(name: str, attrs: dict=None, enabled: bool=None): 27 | return _exists("Expense Type", name, attrs, enabled) 28 | 29 | 30 | # [E Expense] 31 | def item_exists(name: str, attrs: dict=None, enabled: bool=None): 32 | return _exists("Expense Item", name, attrs, enabled) 33 | 34 | 35 | # [E Item] 36 | def uom_exists(name: str, attrs: dict=None): 37 | return _exists("UOM", name, attrs) 38 | 39 | 40 | # [E Item] 41 | def has_item_expenses(expense_item: str): 42 | return _has("Expense", {"expense_item": expense_item}) 43 | 44 | 45 | # [E Entry, E Expense, E Request] 46 | def company_exists(name: str, attrs: dict=None): 47 | return _exists("Company", name, attrs) 48 | 49 | 50 | # [E Entry, E Expense] 51 | def party_exists(dt: str, name: str, attrs: dict=None, enabled: bool=None): 52 | return _exists(dt, name, attrs, enabled) 53 | 54 | 55 | # [Entry, Expense] 56 | def can_use_expense_claim(): 57 | return _exists("DocType", "Expense Claim") 58 | 59 | 60 | # [Expense] 61 | def expense_claim_exists(name: str, attrs: dict=None): 62 | return _exists("Expense Claim", name, attrs) 63 | 64 | 65 | # [Expense] 66 | def expense_exists(name: str, attrs: dict=None): 67 | return _exists("Expense", name, attrs) 68 | 69 | 70 | # [E Entry] 71 | def mode_of_payment_exists(name: str, attrs: dict=None, enabled: bool=None): 72 | return _exists("Mode of Payment", name, attrs, enabled, "enabled", 1) 73 | 74 | 75 | # [E Entry] 76 | def project_exists(name: str, attrs: dict=None): 77 | return _exists("Project", name, attrs) 78 | 79 | 80 | # [E Entry] 81 | def cost_center_exists(name: str, attrs: dict=None, enabled: bool=None): 82 | return _exists("Cost Center", name, attrs, enabled, "disabled", 0) 83 | 84 | 85 | # [Request, Internal] 86 | def get_count(dt: str, filters: dict): 87 | return frappe.db.count(dt, filters) 88 | 89 | 90 | # [Internal] 91 | def _has(dt: str, filters: dict): 92 | return get_count(dt, filters) > 0 93 | 94 | 95 | # [Internal] 96 | def _exists( 97 | dt: str, name: str, attrs: dict=None, enabled: bool=None, 98 | status_col: str="disabled", status_val: str|int=0 99 | ): 100 | params = {"doctype": dt} 101 | if name: 102 | params["name"] = name 103 | if attrs: 104 | params.update(attrs) 105 | if enabled == True: 106 | params[status_col] = ["=", status_val] 107 | elif enabled == False: 108 | params[status_col] = ["!=", status_val] 109 | if frappe.db.exists(params) is None: 110 | return False 111 | return True 112 | 113 | 114 | # [Internal] 115 | def _all_exists( 116 | dt: str, field: str, names: list, attrs: dict=None, 117 | enabled: bool=None, status_col: str="disabled", status_val: int=0 118 | ): 119 | from .filter import all_filter 120 | 121 | data = all_filter(dt, field, names, attrs, enabled, status_col, status_val) 122 | if not data or len(data) != len(names): 123 | return False 124 | 125 | for v in names: 126 | if v not in data: 127 | return False 128 | 129 | return True -------------------------------------------------------------------------------- /expenses/libs/common.py: -------------------------------------------------------------------------------- 1 | # Expenses © 2024 2 | # Author: Ameen Ahmed 3 | # Company: Level Up Marketing & Software Development Services 4 | # Licence: Please refer to LICENSE file 5 | 6 | 7 | import json 8 | 9 | import frappe 10 | 11 | from expenses import __production__ 12 | 13 | 14 | # [Internal] 15 | if not __production__: 16 | from .logger import get_logger 17 | 18 | _LOGGER_ERROR = get_logger("error") 19 | _LOGGER_INFO = get_logger("info") 20 | else: 21 | _LOGGER_ERROR = None 22 | _LOGGER_INFO = None 23 | 24 | 25 | # [Entry, Journal, Update] 26 | def store_error(data): 27 | if _LOGGER_ERROR: 28 | _LOGGER_ERROR.error(data) 29 | 30 | 31 | # [Update] 32 | def store_info(data): 33 | if _LOGGER_INFO: 34 | _LOGGER_INFO.info(data) 35 | 36 | 37 | # [Entry, Journal] 38 | def log_error(text): 39 | text = get_str(text) 40 | if not text: 41 | return 0 42 | 43 | from expenses import __module__ 44 | 45 | from expenses.version import is_version_lt 46 | 47 | if is_version_lt(14): 48 | frappe.log_error(text, __module__) 49 | else: 50 | frappe.log_error(__module__, text) 51 | 52 | 53 | # [E Entry, E Expense, E Item, E Request, E Settings, E Type, Entry, Journal, System] 54 | def error(text: str|list, title: str= None): 55 | as_list = True if isinstance(text, list) else False 56 | if not title: 57 | from expenses import __module__ 58 | 59 | title = __module__ 60 | 61 | frappe.throw(text, title=title, as_list=as_list) 62 | 63 | 64 | # [Item, Request, Type, Update, Internal] 65 | def parse_json(data, default=None): 66 | if isinstance(data, (list, dict)): 67 | return data 68 | try: 69 | return json.loads(data) 70 | except Exception: 71 | return default 72 | 73 | 74 | # [Background, Expense, Internal] 75 | def to_json(data, default=None): 76 | if ( 77 | data and isinstance(data, str) and ( 78 | (data.startswith("{") and data.endswith("}")) or 79 | (data.startswith("[") and data.endswith("]")) 80 | ) 81 | ): 82 | return data 83 | try: 84 | return json.dumps(data) 85 | except Exception: 86 | return default 87 | 88 | 89 | # [Internal] 90 | def to_str(data, default=None): 91 | if isinstance(data, str): 92 | return data 93 | try: 94 | return str(data) 95 | except Exception: 96 | return default 97 | 98 | 99 | # [Internal] 100 | def get_str(data, default=None): 101 | val = to_str(data) 102 | if val is None: 103 | val = to_json(data) 104 | if val is None: 105 | return default 106 | return val 107 | 108 | 109 | # [Attachment, Exchange, Request] 110 | def json_to_list(data): 111 | if data: 112 | tmp = parse_json(data) 113 | if isinstance(tmp, list): 114 | return tmp 115 | if isinstance(data, str): 116 | return [data] 117 | return None -------------------------------------------------------------------------------- /expenses/libs/company.py: -------------------------------------------------------------------------------- 1 | # Expenses © 2024 2 | # Author: Ameen Ahmed 3 | # Company: Level Up Marketing & Software Development Services 4 | # Licence: Please refer to LICENSE file 5 | 6 | 7 | import frappe 8 | from frappe import _ 9 | 10 | 11 | # [Entry] 12 | def get_company_account(company: str, type_val: str): 13 | types = { 14 | "Bank": "default_bank_account", 15 | "Cash": "default_cash_account" 16 | } 17 | if type_val not in types: 18 | return None 19 | 20 | return frappe.db.get_value("Company", company, types[type_val]) 21 | 22 | 23 | # [E Entry, E Entry Form] 24 | @frappe.whitelist(methods=["POST"]) 25 | def get_company_currency(name, local=False): 26 | if not name or not isinstance(name, str): 27 | if local: 28 | return None 29 | return {"error": _("Arguments required to get company currency are invalid.")} 30 | 31 | val = frappe.db.get_value("Company", name, "default_currency") 32 | if val is None and not local: 33 | return {"error": _("Company \"{0}\" doesn't exist.").format(name)} 34 | 35 | return val -------------------------------------------------------------------------------- /expenses/libs/entry.py: -------------------------------------------------------------------------------- 1 | # Expenses © 2024 2 | # Author: Ameen Ahmed 3 | # Company: Level Up Marketing & Software Development Services 4 | # Licence: Please refer to LICENSE file 5 | 6 | 7 | import frappe 8 | from frappe import _ 9 | 10 | 11 | # [E Entry, E Entry Form] 12 | @frappe.whitelist(methods=["POST"]) 13 | def get_mode_of_payment_data(mode_of_payment, company, local=False): 14 | if ( 15 | not mode_of_payment or 16 | not isinstance(mode_of_payment, str) or 17 | not company or not isinstance(company, str) 18 | ): 19 | if local: 20 | return None 21 | 22 | return {"error": _("Arguments required to get mode of payment data are invalid.")} 23 | 24 | dt = "Mode of Payment" 25 | mop_type = frappe.db.get_value(dt, mode_of_payment, "type") 26 | if not mop_type: 27 | if local: 28 | return None 29 | 30 | return {"error": _("Mode of payment \"{0}\" doesn't exist.").format(mode_of_payment)} 31 | 32 | adt = "Account" 33 | doc = frappe.qb.DocType(f"{dt} {adt}") 34 | adoc = frappe.qb.DocType(adt) 35 | data = ( 36 | frappe.qb.from_(doc) 37 | .select( 38 | adoc.name.as_("account"), 39 | adoc.account_currency.as_("currency") 40 | ) 41 | .inner_join(adoc) 42 | .on(adoc.name == doc.default_account) 43 | .where(doc.parent == mode_of_payment) 44 | .where(doc.parenttype == dt) 45 | .where(doc.parentfield == "accounts") 46 | .where(doc.company == company) 47 | .limit(1) 48 | ).run(as_dict=True) 49 | if data and isinstance(data, list): 50 | data = data.pop(0) 51 | data["type"] = mop_type 52 | return data 53 | 54 | from .company import get_company_account 55 | 56 | account = get_company_account(company, mop_type) 57 | if not account: 58 | if local: 59 | return None 60 | 61 | return {"error": _("{0} account for company \"{1}\" isn't found.").format(mop_type, company)} 62 | 63 | from .account import get_account_currency 64 | 65 | currency = get_account_currency(account) 66 | if not currency: 67 | if local: 68 | return None 69 | 70 | return {"error": _("Account currency for \"{0}\" isn't found.").format(account)} 71 | 72 | return { 73 | "type": mop_type, 74 | "account": account, 75 | "currency": currency 76 | } 77 | 78 | 79 | # [E Entry, Internal] 80 | def is_entry_moderator(): 81 | return 1 if "Expenses Entry Moderator" in frappe.get_roles() else 0 82 | 83 | 84 | # [E Entry, E Entry Form] 85 | @frappe.whitelist() 86 | def entry_form_setup(): 87 | from .expense import ( 88 | has_expense_claim, 89 | expense_claim_reqd_if_paid 90 | ) 91 | 92 | has_expense_claim = has_expense_claim() 93 | return { 94 | "is_moderator": is_entry_moderator(), 95 | "has_expense_claim": has_expense_claim, 96 | "expense_claim_reqd": expense_claim_reqd_if_paid() if has_expense_claim else 0 97 | } 98 | 99 | 100 | # [E Entry Form] 101 | @frappe.whitelist(methods=["POST"]) 102 | def get_request_data(name): 103 | if not name or not isinstance(name, str): 104 | return 0 105 | 106 | from .request import get_request 107 | 108 | data = get_request(name) 109 | if not data: 110 | data = 0 111 | return data 112 | 113 | 114 | # [Journal] 115 | def get_entry_data(name: str): 116 | from .cache import get_cached_doc 117 | 118 | return get_cached_doc("Expenses Entry", name) -------------------------------------------------------------------------------- /expenses/libs/entry_details.py: -------------------------------------------------------------------------------- 1 | # Expenses © 2024 2 | # Author: Ameen Ahmed 3 | # Company: Level Up Marketing & Software Development Services 4 | # Licence: Please refer to LICENSE file 5 | 6 | 7 | import frappe 8 | 9 | 10 | # [Expense] 11 | def get_expense_entries(expense: str): 12 | dt = "Expenses Entry" 13 | doc = frappe.qb.DocType(f"{dt} Details") 14 | data = ( 15 | frappe.qb.from_(doc) 16 | .select(doc.parent) 17 | .where(doc.expense_ref == expense) 18 | .where(doc.parenttype == dt) 19 | .where(doc.parentfield == "expenses") 20 | ).run(as_dict=True) 21 | if not data or not isinstance(data, list): 22 | return None 23 | 24 | return data -------------------------------------------------------------------------------- /expenses/libs/exchange.py: -------------------------------------------------------------------------------- 1 | # Expenses © 2024 2 | # Author: Ameen Ahmed 3 | # Company: Level Up Marketing & Software Development Services 4 | # Licence: Please refer to LICENSE file 5 | 6 | 7 | import frappe 8 | from frappe import _ 9 | 10 | 11 | # [E Entry, E Entry Form] 12 | @frappe.whitelist(methods=["POST"]) 13 | def get_exchange_rate(_from: str|list, _to: str, date: str=None, args: str=None, local: bool=False): 14 | from .common import json_to_list 15 | 16 | _from = json_to_list(_from) 17 | 18 | if ( 19 | not _from or not isinstance(_from, list) or 20 | not _to or not isinstance(_to, str) or 21 | (date and not isinstance(date, str)) or 22 | (args and not isinstance(args, str)) 23 | ): 24 | if local: 25 | return 1.0 26 | 27 | return {"error": _("Arguments required to get the exchange rate/rates are invalid.")} 28 | 29 | _from = [v for v in _from if v and isinstance(v, str)] 30 | if _from and _to in _from: 31 | _from.remove(_to) 32 | 33 | if not _from: 34 | if local: 35 | return 1.0 36 | 37 | return {"error": _("From currency/currencies required to get the exchange rate/rates is empty.")} 38 | 39 | if not date: 40 | from frappe.utils import nowdate 41 | 42 | date = nowdate() 43 | 44 | from frappe.utils import flt 45 | 46 | multi = len(_from) > 1 47 | dt = "Currency Exchange" 48 | fields = ["from_currency", "exchange_rate"] 49 | filters = [ 50 | [dt, "date", "<=", f"{date} 00:00:00.000"], 51 | [ 52 | dt, fields[0], 53 | "in" if multi else "=", 54 | _from if multi else _from[0] 55 | ], 56 | [dt, fields[1], "=", _to] 57 | ] 58 | if args == "for_buying": 59 | filters.append([dt, args, "=", 1]) 60 | elif args == "for_selling": 61 | filters.append([dt, args, "=", 1]) 62 | 63 | settings = frappe.get_doc("Accounts Settings") 64 | if settings: 65 | from frappe.utils import cint 66 | 67 | if ( 68 | not cint(settings.get("allow_stale")) and 69 | cint(settings.get("stale_days")) 70 | ): 71 | from frappe.utils import ( 72 | add_days, 73 | get_datetime_str 74 | ) 75 | 76 | days = cint(settings.stale_days) 77 | check_date = get_datetime_str(add_days(date, -days)) 78 | filters.append([dt, "date", ">", check_date]) 79 | 80 | data = frappe.get_all( 81 | dt, 82 | fields=fields, 83 | filters=filters, 84 | order_by="date desc", 85 | ignore_permissions=True, 86 | strict=False 87 | ) 88 | if data and isinstance(data, list): 89 | if not multi: 90 | return flt(data.pop(0)[fields[1]]) 91 | 92 | return {v[fields[0]]:flt(v[fields[1]]) for v in data} 93 | 94 | data = {k:1.0 for k in _from} 95 | found = 0 96 | try: 97 | for k in _from: 98 | key = "currency_exchange_rate_{0}:{1}:{2}".format(date, k, _to) 99 | val = frappe.cache().get(key) 100 | if flt(val) >= 1: 101 | found += 1 102 | data[k] = flt(val) 103 | 104 | except Exception: 105 | from .common import ( 106 | store_error, 107 | log_error 108 | ) 109 | 110 | found = 0 111 | store_error({ 112 | "error": "Failed to get exchange rate/rates", 113 | "_from": _from, 114 | "_to": _to, 115 | "date": date, 116 | "args": args 117 | }) 118 | log_error(_("Failed to get exchange rate/rates.")) 119 | 120 | if found or local: 121 | return data if multi else data[_from[0]] 122 | 123 | return {"error": _("Unable to get the exchange rate/rates value.")} -------------------------------------------------------------------------------- /expenses/libs/expense.py: -------------------------------------------------------------------------------- 1 | # Expenses © 2024 2 | # Author: Ameen Ahmed 3 | # Company: Level Up Marketing & Software Development Services 4 | # Licence: Please refer to LICENSE file 5 | 6 | 7 | import frappe 8 | 9 | 10 | # [E Expense, Internal] 11 | ExpenseStatus = frappe._dict({ 12 | "d": "Draft", 13 | "p": "Pending", 14 | "r": "Requested", 15 | "a": "Approved", 16 | "j": "Rejected", 17 | "c": "Cancelled" 18 | }) 19 | 20 | 21 | # [E Expense, E Expense Form] 22 | @frappe.whitelist(methods=["POST"]) 23 | def item_expense_data(item, company): 24 | if ( 25 | not item or not isinstance(item, str) or 26 | not company or not isinstance(company, str) 27 | ): 28 | return {} 29 | 30 | from .item import get_item_company_account 31 | 32 | return get_item_company_account(item, company) 33 | 34 | 35 | # [E Expense, Internal] 36 | def is_expense_moderator(): 37 | return 1 if "Expense Moderator" in frappe.get_roles() else 0 38 | 39 | 40 | # [E Entry, E Expense, Entry, Internal] 41 | def has_expense_claim(): 42 | from .check import can_use_expense_claim 43 | 44 | return 1 if can_use_expense_claim() else 0 45 | 46 | 47 | # [E Entry, E Expense, Entry, Internal] 48 | def expense_claim_reqd_if_paid(): 49 | from .system import settings 50 | 51 | return 1 if settings()._reqd_expense_claim_if_paid else 0 52 | 53 | 54 | # [E Expense Form] 55 | @frappe.whitelist() 56 | def expense_form_setup(): 57 | has_claim = has_expense_claim() 58 | return { 59 | "is_moderator": is_expense_moderator(), 60 | "has_expense_claim": has_claim, 61 | "expense_claim_reqd": expense_claim_reqd_if_paid() if has_claim else 0 62 | } 63 | 64 | 65 | # [E Expense] 66 | def is_valid_claim(expense_claim: str, paid_by: str, company: str): 67 | from .check import expense_claim_exists 68 | 69 | return expense_claim_exists(expense_claim, { 70 | "employee": paid_by, 71 | "company": company, 72 | "is_paid": 1, 73 | "status": "Paid", 74 | "docstatus": 1 75 | }) 76 | 77 | 78 | # [E Expense] 79 | def expense_requests_exists(name: str): 80 | from .request_details import get_expense_requests 81 | 82 | return 1 if get_expense_requests(name) else 0 83 | 84 | 85 | # [E Expense] 86 | def expense_entries_exists(name: str): 87 | from .entry_details import get_expense_entries 88 | 89 | return 1 if get_expense_entries(name) else 0 90 | 91 | 92 | # [Request] 93 | def get_expenses_for_company(names: list, company: str): 94 | dt = "Expense" 95 | return frappe.get_all( 96 | dt, 97 | fields=["name"], 98 | filters=[ 99 | [dt, "name", ["in", names]], 100 | [dt, "company", company], 101 | [dt, "docstatus", 1] 102 | ], 103 | pluck="name", 104 | ignore_permissions=True, 105 | strict=False 106 | ) 107 | 108 | 109 | # [Request] 110 | def set_expenses_restored(names: list): 111 | enqueue_expenses_status_change(names, "restore") 112 | 113 | 114 | # [Request] 115 | def set_expenses_requested(names: list): 116 | enqueue_expenses_status_change(names, ExpenseStatus.r) 117 | 118 | 119 | # [Request] 120 | def set_expenses_approved(names: list): 121 | enqueue_expenses_status_change(names, ExpenseStatus.a) 122 | 123 | 124 | # [Request] 125 | def set_expenses_rejected(names: list): 126 | enqueue_expenses_status_change(names, ExpenseStatus.j) 127 | 128 | 129 | # [Request] 130 | def get_expenses(names: list): 131 | from .background import uuid_key 132 | from .cache import get_cache 133 | 134 | dt = "Expense" 135 | key = uuid_key(names) 136 | data = get_cache(dt, key) 137 | if data and isinstance(data, list): 138 | return data 139 | 140 | doc = frappe.qb.DocType(dt) 141 | data = ( 142 | frappe.qb.from_(doc) 143 | .select( 144 | doc.name, 145 | doc.company, 146 | doc.expense_account, 147 | doc.required_by, 148 | doc.description, 149 | doc.currency, 150 | doc.total, 151 | doc.is_paid, 152 | doc.paid_by, 153 | doc.is_advance, 154 | doc.party_type, 155 | doc.party, 156 | doc.project 157 | ) 158 | .where(doc.name.isin(names)) 159 | .where(doc.status.isin([ExpenseStatus.p, ExpenseStatus.r])) 160 | .where(doc.docstatus == 1) 161 | ).run(as_dict=True) 162 | if not data or not isinstance(data, list): 163 | from frappe import _ 164 | 165 | return {"error": _("Unable to get the expenses data.")} 166 | 167 | from .attachment import get_files_by_parents 168 | 169 | attachments = get_files_by_parents([v["name"] for v in data], dt, "attachments") 170 | if attachments: 171 | for v in data: 172 | if v["name"] in attachments: 173 | v["attachments"] = attachments[v["name"]] 174 | 175 | from .cache import set_cache 176 | 177 | set_cache(dt, key, data) 178 | return data 179 | 180 | 181 | # [Request] 182 | def search_expenses_by_company(company, filters, search=None, as_dict=False): 183 | from .search import filter_search, prepare_data 184 | 185 | dt = "Expense" 186 | doc = frappe.qb.DocType(dt) 187 | qry = ( 188 | frappe.qb.from_(doc) 189 | .select( 190 | doc.name, 191 | doc.expense_item, 192 | doc.description, 193 | doc.total, 194 | doc.is_advance, 195 | doc.required_by 196 | ) 197 | .where(doc.company == company) 198 | .where(doc.status == ExpenseStatus.p) 199 | .where(doc.docstatus == 1) 200 | ) 201 | qry = filter_search(doc, qry, dt, search, doc.name, "name") 202 | if "ignored" in filters: 203 | qry = qry.where(doc.name.notin(filters["ignored"])) 204 | if "max_date" in filters: 205 | qry = qry.where(doc.required_by.lte(filters["max_date"])) 206 | if "owner" in filters: 207 | qry = qry.where(doc.owner == filters["owner"]) 208 | data = qry.run(as_dict=as_dict) 209 | data = prepare_data(data, dt, "name", search, as_dict) 210 | return data 211 | 212 | 213 | # [Internal] 214 | def enqueue_expenses_status_change(names: list, status: str): 215 | from .background import ( 216 | uuid_key, 217 | is_job_running 218 | ) 219 | 220 | key = uuid_key([names, status]) 221 | job_name = f"exp-set-expenses-status-{key}" 222 | if not is_job_running(job_name): 223 | from .background import enqueue_job 224 | 225 | enqueue_job( 226 | "expenses.libs.expense.set_expenses_status", 227 | job_name, 228 | timeout=len(names) * 5, 229 | names=names, 230 | status=status 231 | ) 232 | 233 | 234 | # [Internal] 235 | def set_expenses_status(names: list, status: str): 236 | from .check import expense_exists 237 | 238 | for name in names: 239 | if expense_exists(name): 240 | doc = frappe.get_doc("Expense", name) 241 | if status == ExpenseStatus.r: 242 | doc.request() 243 | elif status == ExpenseStatus.a: 244 | doc.approve() 245 | elif status == ExpenseStatus.j: 246 | doc.reject() 247 | elif status == "restore": 248 | doc.restore() 249 | 250 | 251 | # [E Entry] 252 | def get_expenses_data(names: list, company: str): 253 | from .background import uuid_key 254 | from .cache import get_cache 255 | 256 | dt = "Expense" 257 | key = uuid_key([names, company]) 258 | data = get_cache(dt, key) 259 | if data and isinstance(data, dict): 260 | return data 261 | 262 | doc = frappe.qb.DocType(dt) 263 | data = ( 264 | frappe.qb.from_(doc) 265 | .select( 266 | doc.name, 267 | doc.expense_account, 268 | doc.required_by, 269 | doc.description, 270 | doc.currency, 271 | doc.total, 272 | doc.is_advance, 273 | doc.is_paid, 274 | doc.paid_by, 275 | doc.expense_claim, 276 | doc.party_type, 277 | doc.party, 278 | doc.project 279 | ) 280 | .where(doc.name.isin(names)) 281 | .where(doc.company == company) 282 | .where(doc.status == ExpenseStatus.a) 283 | .where(doc.docstatus == 1) 284 | ).run(as_dict=True) 285 | if not data or not isinstance(data, list): 286 | return {} 287 | 288 | from frappe.utils import cint, flt 289 | 290 | ret = {} 291 | str_keys = [ 292 | "expense_account", "required_by", 293 | "description", "currency", 294 | "paid_by", "party_type", 295 | "party", "expense_claim", "project" 296 | ] 297 | int_keys = ["is_advance", "is_paid"] 298 | for v in data: 299 | for k in str_keys: 300 | if not v[k]: 301 | v[k] = None 302 | 303 | for k in int_keys: 304 | v[k] = 1 if cint(v[k]) > 0 else 0 305 | 306 | v["account"] = v.pop("expense_account") 307 | v["account_currency"] = v.pop("currency") 308 | v["cost_in_account_currency"] = flt(v.pop("total")) 309 | ret[v.pop("name")] = v 310 | 311 | from .cache import set_cache 312 | 313 | set_cache(dt, key, ret) 314 | return ret -------------------------------------------------------------------------------- /expenses/libs/filter.py: -------------------------------------------------------------------------------- 1 | # Expenses © 2024 2 | # Author: Ameen Ahmed 3 | # Company: Level Up Marketing & Software Development Services 4 | # Licence: Please refer to LICENSE file 5 | 6 | 7 | import frappe 8 | 9 | 10 | # [E Settings, Update] 11 | def users_filter(names: list, attrs: dict=None, enabled: bool=None): 12 | return all_filter("User", "name", names, attrs, enabled, "enabled", 1) 13 | 14 | 15 | # [E Item, E Type] 16 | def companies_filter(names: list, attrs: dict=None): 17 | return all_filter("Company", "name", names, attrs) 18 | 19 | 20 | # [E Entry, E Item, E Type] 21 | def company_accounts_filter(names: list, attrs: dict=None, enabled: bool=None): 22 | return all_filter("Account", ["name", "company"], names, attrs, enabled, "disabled", 0) 23 | 24 | 25 | # [E Entry] 26 | def projects_filter(names: list, attrs: dict=None): 27 | return all_filter("Project", "name", names, attrs) 28 | 29 | 30 | # [E Entry] 31 | def cost_centers_filter(names: list, attrs: dict=None, enabled: bool=None): 32 | return all_filter("Cost Center", "name", names, attrs, enabled, "disabled", 0) 33 | 34 | 35 | # [E Entry] 36 | def employees_filter(names: list, attrs: dict=None, enabled: bool=None): 37 | return all_filter("Employee", "name", names, attrs, enabled, "status", "Active") 38 | 39 | 40 | # [E Entry] 41 | def expense_claims_filter(names: list, attrs: dict=None): 42 | return all_filter("Expense Claim", ["name", "employee"], names, attrs) 43 | 44 | 45 | # [E Entry] 46 | def parties_filter(dt: str, names: list, attrs: dict=None, enabled: bool=None): 47 | return all_filter(dt, "name", names, attrs, enabled) 48 | 49 | 50 | # [Check, Internal] 51 | def all_filter( 52 | dt: str, field: str|list, names: list, attrs: dict=None, 53 | enabled: bool=None, status_col: str="disabled", status_val: str|int=0 54 | ): 55 | if isinstance(field, str): 56 | field = [field] 57 | 58 | filters = [[dt, field[0], "in", list(set(names))]] 59 | if attrs: 60 | for k in attrs: 61 | if isinstance(attrs[k], list): 62 | if len(attrs[k]) > 1 and isinstance(attrs[k][1], list): 63 | filters.append([dt, k, attrs[k][0], attrs[k][1]]) 64 | else: 65 | filters.append([dt, k, "in", attrs[k]]) 66 | else: 67 | filters.append([dt, k, "=", attrs[k]]) 68 | 69 | if enabled == True: 70 | filters.append([dt, status_col, "=", status_val]) 71 | elif enabled == False: 72 | filters.append([dt, status_col, "!=", status_val]) 73 | 74 | flen = len(field) 75 | data = frappe.get_all( 76 | dt, 77 | fields=field, 78 | filters=filters, 79 | pluck=field[0] if flen == 1 else None, 80 | ignore_permissions=True, 81 | strict=False 82 | ) 83 | if not data or not isinstance(data, list): 84 | return None 85 | 86 | if flen == 1: 87 | return [v for v in data if v in names] 88 | 89 | if flen == 2: 90 | return {v[field[0]]:v[field[1]] for v in data if v[field[0]] in names} 91 | 92 | return {v[field[0]]:v for v in data if v[field[0]] in names} -------------------------------------------------------------------------------- /expenses/libs/item.py: -------------------------------------------------------------------------------- 1 | # Expenses © 2024 2 | # Author: Ameen Ahmed 3 | # Company: Level Up Marketing & Software Development Services 4 | # Licence: Please refer to LICENSE file 5 | 6 | 7 | import frappe 8 | 9 | 10 | # [Type] 11 | def reload_items_of_types(types): 12 | names = get_items_of_types(types) 13 | if not names: 14 | return 0 15 | 16 | from .background import ( 17 | uuid_key, 18 | is_job_running 19 | ) 20 | 21 | job = uuid_key(names) 22 | job = f"reload-items-{job}" 23 | if not is_job_running(job): 24 | from .background import enqueue_job 25 | 26 | enqueue_job( 27 | "expenses.libs.item.reload_items", 28 | job, 29 | names=names 30 | ) 31 | 32 | 33 | # [Internal] 34 | def get_items_of_types(types): 35 | dt = "Expense Item" 36 | names = frappe.get_list( 37 | dt, 38 | fields=["name"], 39 | filters=[[dt, "expense_type", "in", types]], 40 | pluck="name", 41 | ignore_permissions=True, 42 | strict=False 43 | ) 44 | if not names or not isinstance(names, list): 45 | return 0 46 | 47 | return names 48 | 49 | 50 | # [Internal] 51 | def reload_items(names): 52 | from .cache import get_cached_doc 53 | 54 | dt = "Expense Item" 55 | for name in names: 56 | doc = get_cached_doc(dt, name) 57 | if doc: 58 | doc.reload_expense_accounts() 59 | 60 | 61 | # [E Item Form] 62 | @frappe.whitelist() 63 | def search_item_types(doctype, txt, searchfield, start, page_len, filters, as_dict=False): 64 | if filters: 65 | from .common import parse_json 66 | 67 | filters = parse_json(filters) 68 | 69 | if not filters or not isinstance(filters, dict): 70 | filters = {} 71 | 72 | from .type import query_types 73 | 74 | filters["is_group"] = 0 75 | return query_types(txt, filters, start, page_len, as_dict) 76 | 77 | 78 | # [E Item, E Item Form] 79 | @frappe.whitelist(methods=["POST"]) 80 | def get_type_accounts_list(type_name): 81 | if not type_name or not isinstance(type_name, str): 82 | return {"error": "Arguments required to get type accounts list are invalid."} 83 | 84 | from .account import get_type_accounts 85 | 86 | data = get_type_accounts(type_name, { 87 | "cost": 0.0, 88 | "min_cost": 0.0, 89 | "max_cost": 0.0, 90 | "qty": 0.0, 91 | "min_qty": 0.0, 92 | "max_qty": 0.0, 93 | "inherited": 1 94 | }) 95 | if data is None: 96 | return {"error": "Expense type \"{0}\" doesn't exist.".format(type_name)} 97 | 98 | return data 99 | 100 | 101 | # [Expense] 102 | def get_item_company_account(item: str, company: str): 103 | from .cache import get_cache 104 | 105 | dt = "Expense Item" 106 | key = f"{item}-{company}-account-data" 107 | cache = get_cache(dt, key) 108 | if cache and isinstance(cache, dict): 109 | return cache 110 | 111 | from .account import get_item_company_account_data 112 | 113 | data = get_item_company_account_data(item, company) 114 | if not data: 115 | return {} 116 | 117 | from .cache import set_cache 118 | 119 | set_cache(dt, key, data) 120 | return data 121 | 122 | 123 | # [E Exoense Form] 124 | @frappe.whitelist() 125 | def search_items(doctype, txt, searchfield, start, page_len, filters, as_dict=False): 126 | if filters: 127 | from .common import parse_json 128 | 129 | filters = parse_json(filters) 130 | 131 | if ( 132 | not filters or not isinstance(filters, dict) or 133 | not filters.get("company", "") or 134 | not isinstance(filters["company"], str) 135 | ): 136 | return [] 137 | 138 | from .account import query_items_with_company_account 139 | from .search import ( 140 | filter_search, 141 | prepare_data 142 | ) 143 | from .type import get_types_filter_query 144 | 145 | dt = "Expense Item" 146 | doc = frappe.qb.DocType(dt) 147 | fqry = query_items_with_company_account(filters["company"]) 148 | tqry = get_types_filter_query() 149 | qry = ( 150 | frappe.qb.from_(doc) 151 | .select( 152 | doc.name.as_("label"), 153 | doc.name.as_("value") 154 | ) 155 | .where(doc.disabled == 0) 156 | .where(doc.name.isin(fqry)) 157 | .where(doc.expense_type.isin(tqry)) 158 | ) 159 | qry = filter_search(doc, qry, dt, txt, doc.name, "name") 160 | data = qry.run(as_dict=as_dict) 161 | data = prepare_data(data, dt, "name", txt, as_dict) 162 | return data -------------------------------------------------------------------------------- /expenses/libs/journal.py: -------------------------------------------------------------------------------- 1 | # Expenses © 2024 2 | # Author: Ameen Ahmed 3 | # Company: Level Up Marketing & Software Development Services 4 | # Licence: Please refer to LICENSE file 5 | 6 | 7 | import frappe 8 | from frappe import _ 9 | 10 | 11 | # [E Entry] 12 | def enqueue_journal_entry(entry: str): 13 | from .background import is_job_running 14 | 15 | job_name = f"exp-make-journal-entry-{entry}" 16 | if not is_job_running(job_name): 17 | from .background import enqueue_job 18 | 19 | enqueue_job( 20 | "expenses.libs.journal.make_journal_entry", 21 | job_name, 22 | entry=entry 23 | ) 24 | 25 | 26 | # [Internal] 27 | def make_journal_entry(entry: str): 28 | from .entry import get_entry_data 29 | 30 | doc = get_entry_data(entry) 31 | if not doc: 32 | return 0 33 | 34 | if not doc.is_submitted: 35 | return 0 36 | 37 | dt = "Journal Entry" 38 | if frappe.db.exists(dt, {"bill_no": entry}): 39 | _log_error(_("Expenses entry \"{0}\" has already been added to journal.").format(entry)) 40 | return 0 41 | 42 | if not doc.payment_account: 43 | _log_error(_("The Mode of Payment of expenses entry \"{0}\" has no linked account.").format(entry)) 44 | return 0 45 | 46 | if ( 47 | doc.payment_target == "Bank" and 48 | (not doc.payment_reference or not doc.clearance_date) 49 | ): 50 | _log_error(_("The payment reference and/or payment / clearance date for expenses entry \"{0}\" hasn't been set.").format(entry)) 51 | return 0 52 | 53 | from frappe.utils import ( 54 | cint, 55 | flt, 56 | cstr 57 | ) 58 | 59 | multi_currency = 0 60 | accounts = [] 61 | for v in doc.expenses: 62 | if v.account_currency != doc.payment_currency: 63 | multi_currency = 1 64 | 65 | accounts.append({ 66 | "account": v.account, 67 | "party_type": cstr(v.party_type), 68 | "party": cstr(v.party), 69 | "cost_center": cstr(v.cost_center), 70 | "project": cstr(v.project), 71 | "account_currency": v.account_currency, 72 | "exchange_rate": flt(v.exchange_rate), 73 | "debit_in_account_currency": flt(v.cost_in_account_currency), 74 | "debt": flt(v.cost), 75 | "is_advance": cint(v.is_advance), 76 | "user_remark": cstr(v.description) 77 | }) 78 | 79 | if doc.payment_target == "Cash": 80 | doc.payment_reference = "" 81 | doc.clearance_date = "" 82 | 83 | accounts.append({ 84 | "account": doc.payment_account, 85 | "account_currency": doc.payment_currency, 86 | "exchange_rate": flt(doc.exchange_rate), 87 | "credit_in_account_currency": flt(doc.total_in_payment_currency), 88 | "credit": flt(doc.total) 89 | }) 90 | 91 | from frappe.utils import today 92 | 93 | try: 94 | (frappe.new_doc(dt) 95 | .update({ 96 | "title": doc.name, 97 | "voucher_type": dt, 98 | "posting_date": today(), 99 | "company": doc.company, 100 | "bill_no": doc.name, 101 | "accounts": accounts, 102 | "user_remark": cstr(doc.remarks), 103 | "mode_of_payment": doc.mode_of_payment, 104 | "cheque_no": cstr(doc.payment_reference), 105 | "cheque_date": cstr(doc.clearance_date), 106 | "reference_date": cstr(doc.clearance_date), 107 | "multi_currency": multi_currency 108 | }) 109 | .insert(ignore_permissions=True, ignore_mandatory=True) 110 | .submit()) 111 | except Exception as exc: 112 | from .common import store_error 113 | 114 | store_error(exc) 115 | _log_error(_("Unable to create a journal entry for expenses entry \"{0}\".").format(entry)) 116 | 117 | 118 | # [E Entry] 119 | def cancel_journal_entry(entry: str): 120 | dt = "Journal Entry" 121 | filters = {"bill_no": entry} 122 | if frappe.db.exists(dt, filters): 123 | try: 124 | frappe.get_doc(dt, filters).cancel() 125 | except Exception as exc: 126 | from .common import store_error 127 | 128 | store_error(exc) 129 | _log_error(_("Unable to cancel the journal entry for expenses entry \"{0}\".").format(entry)) 130 | 131 | 132 | # [Internal] 133 | def _log_error(msg: str): 134 | from .common import log_error 135 | 136 | log_error(msg) -------------------------------------------------------------------------------- /expenses/libs/logger.py: -------------------------------------------------------------------------------- 1 | # Expenses © 2024 2 | # Author: Ameen Ahmed 3 | # Company: Level Up Marketing & Software Development Services 4 | # Licence: Please refer to LICENSE file 5 | 6 | 7 | import os 8 | import logging 9 | from logging.handlers import RotatingFileHandler 10 | 11 | import frappe 12 | 13 | 14 | # [Common] 15 | def get_logger(logType): 16 | from expenses import __abbr__ 17 | 18 | if not logType: 19 | logType = "error" 20 | site = getattr(frappe.local, "site", None) 21 | if not site: 22 | site = "Frappe" 23 | 24 | logger_name = "{}-{}-{}".format(__abbr__, site, logType) 25 | 26 | try: 27 | return frappe.loggers[logger_name] 28 | except KeyError: 29 | pass 30 | 31 | logfile = "{}-{}.log".format(__abbr__, logType) 32 | logfile = os.path.join("..", "logs", logfile) 33 | logger = logging.getLogger(logger_name) 34 | logger.setLevel(getattr(logging, logType.upper(), None) or logging.ERROR) 35 | logger.propagate = False 36 | handler = RotatingFileHandler(logfile, maxBytes=100_000, backupCount=20) 37 | handler.setLevel(getattr(logging, logType.upper(), None) or logging.ERROR) 38 | handler.setFormatter(LoggingCustomFormatter()) 39 | logger.addHandler(handler) 40 | frappe.loggers[logger_name] = logger 41 | return logger 42 | 43 | 44 | # [Internal] 45 | class LoggingCustomFormatter(logging.Formatter): 46 | def __init__(self): 47 | fmt = '%(asctime)s - %(name)s - %(levelname)s - %(message)s' 48 | super(LoggingCustomFormatter, self).__init__(fmt) 49 | 50 | def format(self, record): 51 | return super().format(record) -------------------------------------------------------------------------------- /expenses/libs/realtime.py: -------------------------------------------------------------------------------- 1 | # Expenses © 2024 2 | # Author: Ameen Ahmed 3 | # Company: Level Up Marketing & Software Development Services 4 | # Licence: Please refer to LICENSE file 5 | 6 | 7 | import frappe 8 | 9 | 10 | # [E Settings] 11 | def emit_settings_changed(data=None): 12 | emit_event("exp_settings_changed", data) 13 | 14 | 15 | # [Internal] 16 | def emit_event(event: str, data): 17 | frappe.publish_realtime(event=event, message=data) -------------------------------------------------------------------------------- /expenses/libs/request.py: -------------------------------------------------------------------------------- 1 | # Expenses © 2024 2 | # Author: Ameen Ahmed 3 | # Company: Level Up Marketing & Software Development Services 4 | # Licence: Please refer to LICENSE file 5 | 6 | 7 | import frappe 8 | 9 | 10 | # [E Request, Internal] 11 | RequestStatus = frappe._dict({ 12 | "d": "Draft", 13 | "p": "Pending", 14 | "c": "Cancelled", 15 | "a": "Approved", 16 | "r": "Rejected", 17 | "e": "Processed" 18 | }) 19 | 20 | 21 | # [Internal] 22 | def get_request_doc(name: str): 23 | from .cache import get_cached_doc 24 | 25 | return get_cached_doc("Expenses Request", name) 26 | 27 | 28 | # [E Request, Internal] 29 | def get_filtered_company_expenses(company: str, expenses: list): 30 | from .expense import get_expenses_for_company 31 | 32 | data = get_expenses_for_company(expenses, company) 33 | if not data or not isinstance(data, list): 34 | return [] 35 | 36 | return data 37 | 38 | 39 | # [E Request] 40 | def is_request_amended(name: str): 41 | from .check import get_count 42 | 43 | return get_count("Expenses Request", {"amended_from": name}) > 0 44 | 45 | 46 | # [E Request] 47 | def restore_expenses(expenses: list): 48 | from .expense import set_expenses_restored 49 | 50 | set_expenses_restored(expenses) 51 | 52 | 53 | # [E Request] 54 | def request_expenses(expenses: list): 55 | from .expense import set_expenses_requested 56 | 57 | set_expenses_requested(expenses) 58 | 59 | 60 | # [E Request] 61 | def approve_expenses(expenses: list): 62 | from .expense import set_expenses_approved 63 | 64 | set_expenses_approved(expenses) 65 | 66 | 67 | # [E Request] 68 | def reject_expenses(expenses: list): 69 | from .expense import set_expenses_rejected 70 | 71 | set_expenses_rejected(expenses) 72 | 73 | 74 | # [E Request Form] 75 | @frappe.whitelist() 76 | def request_form_setup(): 77 | return { 78 | "is_moderator": is_request_moderator(), 79 | "is_reviewer": is_request_reviewer() 80 | } 81 | 82 | 83 | # [E Request, Internal] 84 | def is_request_moderator(): 85 | return 1 if "Expenses Request Moderator" in frappe.get_roles() else 0 86 | 87 | 88 | # [E Request, Internal] 89 | def is_request_reviewer(): 90 | return 1 if "Expenses Request Reviewer" in frappe.get_roles() else 0 91 | 92 | 93 | # [E Request Form] 94 | @frappe.whitelist(methods=["POST"]) 95 | def get_expenses_data(expenses): 96 | from .common import json_to_list 97 | 98 | expenses = json_to_list(expenses) 99 | if not expenses or not isinstance(expenses, list): 100 | return [] 101 | 102 | tmp = expenses.copy() 103 | for i in range(len(tmp)): 104 | v = tmp.pop(0) 105 | if not v or not isinstance(v, str): 106 | expenses.remove(v) 107 | if not expenses: 108 | return [] 109 | 110 | from .expense import get_expenses 111 | 112 | return get_expenses(expenses) 113 | 114 | 115 | # [E Request Form] 116 | @frappe.whitelist() 117 | def search_company_expenses(doctype, txt, searchfield, start, page_len, filters, as_dict=False): 118 | if filters: 119 | from .common import parse_json 120 | 121 | filters = parse_json(filters) 122 | 123 | if ( 124 | not filters or not isinstance(filters, dict) or 125 | not filters.get("company", "") or 126 | not isinstance(filters["company"], str) 127 | ): 128 | return [] 129 | 130 | company = filters.pop("company") 131 | if "ignored" in filters: 132 | tmp = filters.pop("ignored") 133 | if tmp and isinstance(tmp, (str, list)): 134 | from .common import json_to_list 135 | 136 | tmp = json_to_list(tmp) 137 | if tmp and isinstance(tmp, list): 138 | xtmp = [] 139 | for i in range(len(tmp)): 140 | v = tmp.pop(0) 141 | if v and isinstance(v, str): 142 | xtmp.append(v) 143 | 144 | if xtmp: 145 | filters["ignored"] = xtmp 146 | if "max_date" in filters: 147 | tmp = filters.pop("max_date") 148 | if tmp and isinstance(tmp, str): 149 | filters["max_date"] = tmp 150 | if "owner" in filters: 151 | tmp = filters.pop("owner") 152 | if tmp and isinstance(tmp, str): 153 | filters["owner"] = tmp 154 | 155 | if not txt or not isinstance(txt, str): 156 | txt = None 157 | 158 | from .expense import search_expenses_by_company 159 | 160 | return search_expenses_by_company(company, filters, txt, as_dict) 161 | 162 | 163 | # [E Request Form] 164 | @frappe.whitelist(methods=["POST"]) 165 | def filter_company_expenses(company, expenses): 166 | from .common import json_to_list 167 | 168 | expenses = json_to_list(expenses) 169 | if ( 170 | not company or not isinstance(company, str) or 171 | not expenses or not isinstance(expenses, list) 172 | ): 173 | return 0 174 | 175 | tmp = expenses.copy() 176 | for i in range(len(tmp)): 177 | v = tmp.pop(0) 178 | if not v or not isinstance(v, str): 179 | expenses.remove(v) 180 | 181 | if not expenses: 182 | return 0 183 | 184 | data = get_filtered_company_expenses(company, expenses) 185 | if not data: 186 | return 0 187 | 188 | return data 189 | 190 | 191 | # [E Request Form] 192 | @frappe.whitelist(methods=["POST"]) 193 | def reject_request_reason(name, reason): 194 | if ( 195 | not name or not isinstance(name, str) or 196 | not reason or not isinstance(reason, str) 197 | ): 198 | return 0 199 | 200 | doc = get_request_doc(name) 201 | if not doc: 202 | return 0 203 | 204 | doc.add_comment( 205 | "Workflow", 206 | reason, 207 | doc.owner, 208 | comment_by=frappe.session.user 209 | ) 210 | return 1 211 | 212 | 213 | # [Entry] 214 | def get_request(name: str): 215 | doc = get_request_doc(name) 216 | if not doc or doc.status != RequestStatus.a: 217 | return None 218 | 219 | from .expense import get_expenses 220 | 221 | data = doc.as_dict( 222 | no_nulls=False, 223 | no_default_fields=False, 224 | convert_dates_to_str=True, 225 | no_child_table_fields=False 226 | ) 227 | data["expenses"] = get_expenses([v.expense for v in doc.expenses]) 228 | return data 229 | 230 | 231 | # [E Entry] 232 | def process_request(name: str): 233 | get_request_doc(name).process(ignore_permissions=True) 234 | 235 | 236 | # [E Entry] 237 | def reject_request(name: str): 238 | get_request_doc(name).reject(ignore_permissions=True) -------------------------------------------------------------------------------- /expenses/libs/request_details.py: -------------------------------------------------------------------------------- 1 | # Expenses © 2024 2 | # Author: Ameen Ahmed 3 | # Company: Level Up Marketing & Software Development Services 4 | # Licence: Please refer to LICENSE file 5 | 6 | 7 | import frappe 8 | 9 | 10 | # [Expense] 11 | def get_expense_requests(expense: str): 12 | dt = "Expenses Request" 13 | doc = frappe.qb.DocType(dt) 14 | ddoc = frappe.qb.DocType(f"{dt} Details") 15 | data = ( 16 | frappe.qb.from_(ddoc) 17 | .select(doc.name) 18 | .left_join(doc) 19 | .on(doc.name == ddoc.parent) 20 | .where(ddoc.expense == expense) 21 | .where(ddoc.parenttype == dt) 22 | .where(ddoc.parentfield == "expenses") 23 | ).run(as_dict=True) 24 | if not data or not isinstance(data, list): 25 | return None 26 | 27 | return data -------------------------------------------------------------------------------- /expenses/libs/search.py: -------------------------------------------------------------------------------- 1 | # Expenses © 2024 2 | # Author: Ameen Ahmed 3 | # Company: Level Up Marketing & Software Development Services 4 | # Licence: Please refer to LICENSE file 5 | 6 | 7 | import frappe 8 | from frappe import _ 9 | from frappe.utils import cstr 10 | 11 | 12 | # [Expense, Item, Type] 13 | def filter_search(doc, qry, doctype, search, relevance, filter_column=None): 14 | meta = frappe.get_meta(doctype) 15 | if search: 16 | from pypika.enums import Order 17 | 18 | from frappe.query_builder.functions import Locate 19 | 20 | qry = qry.select(Locate(search, relevance).as_("_relevance")) 21 | qry = qry.orderby("_relevance", doc.modified, doc.idx, order=Order.desc) 22 | 23 | translated_search_doctypes = frappe.get_hooks("translated_search_doctypes") 24 | search_filters = [] 25 | search_fields = [filter_column] if filter_column else [] 26 | 27 | if meta.title_field: 28 | search_fields.append(meta.title_field) 29 | if meta.search_fields: 30 | search_fields.extend(meta.get_search_fields()) 31 | 32 | field_types = [ 33 | "Data", 34 | "Text", 35 | "Small Text", 36 | "Long Text", 37 | "Link", 38 | "Select", 39 | "Read Only", 40 | "Text Editor" 41 | ] 42 | for f in search_fields: 43 | fmeta = meta.get_field(f.strip()) 44 | if ( 45 | doctype not in translated_search_doctypes and 46 | ( 47 | f == "name" or 48 | (fmeta and fmeta.fieldtype in field_types) 49 | ) 50 | ): 51 | search_filters.append(doc.field(f.strip()).like("%" + search + "%")) 52 | 53 | if len(search_filters) > 1: 54 | from pypika.terms import Criterion 55 | 56 | qry = qry.where(Criterion.any(search_filters)) 57 | else: 58 | qry = qry.where(search_filters.pop(0)) 59 | 60 | if meta.get("fields", {"fieldname": "enabled", "fieldtype": "Check"}): 61 | qry = qry.where(doc.enabled == 1) 62 | if meta.get("fields", {"fieldname": "disabled", "fieldtype": "Check"}): 63 | qry = qry.where(doc.disabled != 1) 64 | 65 | return qry 66 | 67 | 68 | # [Expense, Item, Type] 69 | def prepare_data(data, dt, column, search, as_dict): 70 | if search and dt in frappe.get_hooks("translated_search_doctypes"): 71 | import re 72 | 73 | data = [ 74 | v 75 | for v in data 76 | if re.search( 77 | re.escape(search) + ".*", 78 | _(v.get(column) if as_dict else v[0]), 79 | re.IGNORECASE 80 | ) 81 | ] 82 | 83 | args = [column, search, as_dict] 84 | def relevance_sorter(key): 85 | nonlocal args 86 | 87 | value = _(key.get(args[0]) if args[2] else key[0]) 88 | return (cstr(value).lower().startswith(args[1].lower()) is not True, value) 89 | 90 | data = sorted(data, key=relevance_sorter) 91 | if as_dict: 92 | for r in data: 93 | r.pop("_relevance") 94 | else: 95 | data = [r[:-1] for r in data] 96 | 97 | return data -------------------------------------------------------------------------------- /expenses/libs/system.py: -------------------------------------------------------------------------------- 1 | # Expenses © 2024 2 | # Author: Ameen Ahmed 3 | # Company: Level Up Marketing & Software Development Services 4 | # Licence: Please refer to LICENSE file 5 | 6 | 7 | import frappe 8 | 9 | 10 | # [Install, Expense, Update, Internal] 11 | def settings(): 12 | from .cache import get_cached_doc 13 | 14 | return get_cached_doc("Expenses Settings") 15 | 16 | 17 | # [EXP JS] 18 | @frappe.whitelist() 19 | def get_settings(): 20 | from expenses import __production__ 21 | 22 | doc = settings() 23 | return { 24 | "is_enabled": 1 if doc._is_enabled else 0, 25 | "prod": 1 if __production__ else 0 26 | } 27 | 28 | 29 | # [E Entry, E Expense, E Item, E Request, E Type] 30 | def check_app_status(): 31 | if not settings()._is_enabled: 32 | from frappe import _ 33 | 34 | from expenses import __module__ 35 | 36 | from .common import error 37 | 38 | error(_("{0} app is disabled.").format(_(__module__))) -------------------------------------------------------------------------------- /expenses/libs/type.py: -------------------------------------------------------------------------------- 1 | # Expenses © 2024 2 | # Author: Ameen Ahmed 3 | # Company: Level Up Marketing & Software Development Services 4 | # Licence: Please refer to LICENSE file 5 | 6 | 7 | import frappe 8 | from frappe import _ 9 | from frappe.utils import cint 10 | 11 | 12 | # [Internal] 13 | def get_type(name: str): 14 | from .cache import get_cached_doc 15 | 16 | return get_cached_doc("Expense Type", name) 17 | 18 | 19 | # [E Type] 20 | def disable_type_descendants(lft: int, rgt: int): 21 | names = get_type_descendants(lft, rgt, enabled=True) 22 | if not names: 23 | return 0 24 | 25 | from .cache import clear_doc_cache 26 | 27 | dt = "Expense Type" 28 | doc = frappe.qb.DocType(dt) 29 | ( 30 | frappe.qb.update(doc) 31 | .set(doc.disabled, 1) 32 | .where(doc.name.isin(names)) 33 | ).run() 34 | for name in names: 35 | clear_doc_cache(dt, name) 36 | 37 | 38 | # [E Type] 39 | def reload_type_linked_items(lft: int, rgt: int): 40 | names = get_type_descendants(lft, rgt, with_self=True) 41 | if not names: 42 | return 0 43 | 44 | from .item import reload_items_of_types 45 | 46 | reload_items_of_types(names) 47 | 48 | 49 | # [Internal] 50 | def get_type_descendants(lft: int, rgt: int, with_self=False, enabled=False): 51 | dt = "Expense Type" 52 | filters = [ 53 | [dt, "lft", ">=" if with_self else ">", cint(lft)], 54 | [dt, "rgt", "<=" if with_self else "<", cint(rgt)] 55 | ] 56 | if enabled: 57 | filters.append([dt, "disabled", "!=", 1]) 58 | 59 | names = frappe.get_list( 60 | dt, 61 | fields=["name"], 62 | filters=filters, 63 | pluck="name", 64 | ignore_permissions=True, 65 | strict=False 66 | ) 67 | if not names or not isinstance(names, list): 68 | return 0 69 | 70 | return names 71 | 72 | 73 | # [E Type Form] 74 | @frappe.whitelist() 75 | def search_types(doctype, txt, searchfield, start, page_len, filters, as_dict=False): 76 | if filters: 77 | from .common import parse_json 78 | 79 | filters = parse_json(filters) 80 | 81 | if not filters or not isinstance(filters, dict): 82 | filters = {} 83 | 84 | filters["is_group"] = 1 85 | return query_types(txt, filters, start, page_len, as_dict) 86 | 87 | 88 | # [Item, Internal] 89 | def query_types(txt, filters, start, page_len, as_dict=False): 90 | from pypika.functions import IfNull 91 | from pypika.terms import Criterion 92 | 93 | from .search import ( 94 | filter_search, 95 | prepare_data 96 | ) 97 | 98 | dt = "Expense Type" 99 | fdoc = frappe.qb.DocType(dt).as_("parent") 100 | fqry = ( 101 | frappe.qb.from_(fdoc) 102 | .select(fdoc.name) 103 | .where(fdoc.disabled == 0) 104 | .where(fdoc.is_group == 1) 105 | ) 106 | 107 | doc = frappe.qb.DocType(dt) 108 | qry = ( 109 | frappe.qb.from_(doc) 110 | .select( 111 | doc.name.as_("label"), 112 | doc.name.as_("value") 113 | ) 114 | .where(doc.disabled == 0) 115 | .where(Criterion.any([ 116 | IfNull(doc.parent_type, "") == "", 117 | doc.parent_type.isin(fqry) 118 | ])) 119 | ) 120 | qry = filter_search(doc, qry, dt, txt, doc.name, "name") 121 | if filters: 122 | if filters.get("is_not", "") and isinstance(filters["is_not"], str): 123 | qry = qry.where(doc.name != filters["is_not"]) 124 | 125 | if "is_group" in filters and isinstance(filters["is_group"], int): 126 | qry = qry.where(doc.is_group == filters["is_group"]) 127 | 128 | if filters.get("not_child_of", "") and isinstance(filters["not_child_of"], str): 129 | parent = get_type(filters["not_child_of"]) 130 | if parent: 131 | qry = qry.where(doc.lft.lt(cint(parent.lft))) 132 | qry = qry.where(doc.rgt.gt(cint(parent.rgt))) 133 | 134 | data = qry.run(as_dict=as_dict) 135 | data = prepare_data(data, dt, "name", txt, as_dict) 136 | return data 137 | 138 | 139 | # [E Type Form] 140 | @frappe.whitelist() 141 | def get_companies_accounts(): 142 | dt = "Company" 143 | return frappe.get_list( 144 | dt, 145 | fields=[ 146 | "name as company", 147 | "default_expense_account as account" 148 | ], 149 | filters=[[dt, "is_group", "=", 0]], 150 | ignore_permissions=True, 151 | strict=False 152 | ) 153 | 154 | 155 | # [E Type Form, E Type Tree] 156 | @frappe.whitelist(methods=["POST"]) 157 | def convert_group_to_item(name, parent_type=None): 158 | if ( 159 | not name or not isinstance(name, str) or 160 | (parent_type and not isinstance(parent_type, str)) 161 | ): 162 | return {"error": _("Arguments required to convert group to an item are invalid.")} 163 | 164 | doc = get_type(name) 165 | if not doc: 166 | return {"error": _("Expense type to be converted from a group to an item doesn't exist.")} 167 | 168 | return doc.convert_to_item(parent_type) 169 | 170 | 171 | # [E Type Form, E Type Tree] 172 | @frappe.whitelist(methods=["POST"]) 173 | def convert_item_to_group(name): 174 | if not name or not isinstance(name, str): 175 | return {"error": _("Arguments required to convert an item to a group are invalid.")} 176 | 177 | doc = get_type(name) 178 | if not doc: 179 | return {"error": _("Expense type to be converted from an item to a group doesn't exist.")} 180 | 181 | return doc.convert_to_group() 182 | 183 | 184 | # [E Type Tree] 185 | @frappe.whitelist() 186 | def get_type_children(doctype, parent=None, is_root=False): 187 | if not parent: 188 | return [{"value": "Expense Types", "expandable": 1, "parent": ""}] 189 | 190 | return frappe.get_list( 191 | "Expense Type", 192 | fields=[ 193 | "name as value", 194 | "is_group as expandable", 195 | "parent_type as parent" 196 | ], 197 | filters=[ 198 | ["docstatus", "=", 0], 199 | [ 200 | "ifnull(`parent_type`,\"\")", 201 | "=", 202 | "" if is_root else parent 203 | ] 204 | ], 205 | ignore_permissions=True, 206 | strict=False 207 | ) 208 | 209 | 210 | # [Item] 211 | def get_types_filter_query(): 212 | doc = frappe.qb.DocType("Expense Type") 213 | return ( 214 | frappe.qb.from_(doc) 215 | .select(doc.name) 216 | .where(doc.disabled == 0) 217 | .where(doc.is_group == 0) 218 | ) -------------------------------------------------------------------------------- /expenses/libs/update.py: -------------------------------------------------------------------------------- 1 | # Expenses © 2024 2 | # Author: Ameen Ahmed 3 | # Company: Level Up Marketing & Software Development Services 4 | # Licence: Please refer to LICENSE file 5 | 6 | 7 | import re 8 | 9 | import frappe 10 | from frappe import _ 11 | from frappe.utils import cint 12 | 13 | from expenses import __production__ 14 | 15 | 16 | # [Hooks] 17 | def auto_check_for_update(): 18 | if __production__: 19 | from .system import settings 20 | 21 | doc = settings() 22 | if doc._is_enabled and doc._auto_check_for_update: 23 | update_check(doc) 24 | 25 | 26 | # [EXP Settings Form] 27 | @frappe.whitelist() 28 | def check_for_update(): 29 | if not __production__: 30 | return 0 31 | 32 | return update_check() 33 | 34 | 35 | # [Internal] 36 | def update_check(doc=None): 37 | from frappe.utils import get_request_session 38 | 39 | from expenses import __api__ 40 | 41 | try: 42 | http = get_request_session() 43 | request = http.request("GET", __api__) 44 | status_code = request.status_code 45 | result = request.json() 46 | except Exception as exc: 47 | from .common import store_error 48 | 49 | store_error(exc) 50 | return 0 51 | 52 | if status_code != 200 and status_code != 201: 53 | log_response("Invalid response status", status_code, result) 54 | return 0 55 | 56 | from .common import parse_json 57 | 58 | data = parse_json(result) 59 | if ( 60 | not data or not isinstance(data, dict) or 61 | not data.get("tag_name", "") or 62 | not isinstance(data["tag_name"], (str, int, float)) 63 | ): 64 | log_response("Invalid response data", status_code, result) 65 | return 0 66 | 67 | latest_version = re.findall(r"(\d+(?:\.\d+)+)", str(data["tag_name"])) 68 | if not latest_version: 69 | log_response("Invalid response update version", status_code, result) 70 | return 0 71 | 72 | from frappe.utils import now 73 | 74 | from expenses import __version__ 75 | 76 | latest_version = latest_version.pop() 77 | has_update = compare_versions(latest_version, __version__) > 0 78 | 79 | if not doc: 80 | from .system import settings 81 | 82 | doc = settings() 83 | 84 | doc.latest_check = now() 85 | if has_update: 86 | doc.latest_version = latest_version 87 | doc.has_update = 1 88 | 89 | doc.save(ignore_permissions=True) 90 | 91 | if has_update and doc._is_enabled and doc._send_update_notification: 92 | from .background import is_job_running 93 | 94 | job_name = f"exp-send-notification-{latest_version}" 95 | if not is_job_running(job_name): 96 | from .background import enqueue_job 97 | 98 | enqueue_job( 99 | "expenses.libs.update.send_notification", 100 | job_name, 101 | version=latest_version, 102 | sender=doc.update_notification_sender, 103 | receivers=[v.user for v in doc.update_notification_receivers], 104 | message=data.get("body", "") 105 | ) 106 | 107 | return 1 108 | 109 | 110 | # [Internal] 111 | def log_response(message, status, result): 112 | from .common import store_info 113 | 114 | store_info({ 115 | "action": "check for update", 116 | "message": message, 117 | "response": { 118 | "status": status, 119 | "result": result 120 | } 121 | }) 122 | 123 | 124 | ## [Internal] 125 | def compare_versions(verA, verB): 126 | verA = verA.split(".") 127 | lenA = len(verA) 128 | verB = verB.split(".") 129 | lenB = len(verB) 130 | 131 | if lenA > lenB: 132 | for i in range(lenB, lenA): 133 | verB.append(0) 134 | elif lenA < lenB: 135 | for i in range(lenA, lenB): 136 | verA.append(0) 137 | 138 | for a, b in zip(verA, verB): 139 | d = cint(a) - cint(b) 140 | if d == 0: 141 | continue 142 | return 1 if d > 0 else -1 143 | 144 | return 0 145 | 146 | 147 | ## [Internal] 148 | def send_notification(version, sender, receivers, message): 149 | from .check import user_exists 150 | 151 | if not user_exists(sender, enabled=True): 152 | return 0 153 | 154 | from .filter import users_filter 155 | 156 | receivers = users_filter(receivers, enabled=True) 157 | if not receivers: 158 | return 0 159 | 160 | from frappe.desk.doctype.notification_settings.notification_settings import ( 161 | is_notifications_enabled 162 | ) 163 | 164 | from expenses import __module__ 165 | 166 | if message: 167 | from frappe.utils import markdown 168 | 169 | message = _(markdown(message)) 170 | else: 171 | message = _("No update message.") 172 | 173 | doc = { 174 | "document_type": "Expenses Settings", 175 | "document_name": "Expenses Settings", 176 | "from_user": sender, 177 | "subject": "{0}: {1}".format(__module__, _("New version available")), 178 | "type": "Alert", 179 | "email_content": "

{0} {1}

{2}

".format( 180 | _("Version"), version, message 181 | ) 182 | } 183 | for receiver in receivers: 184 | if is_notifications_enabled(receiver): 185 | (frappe.new_doc("Notification Log") 186 | .update(doc) 187 | .update({"for_user": receiver}) 188 | .insert(ignore_permissions=True, ignore_mandatory=True)) -------------------------------------------------------------------------------- /expenses/modules.txt: -------------------------------------------------------------------------------- 1 | Expenses -------------------------------------------------------------------------------- /expenses/patches.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kid1194/erpnext_expenses/38e410ab8c50861d98a65905253ae4dc612edb47/expenses/patches.txt -------------------------------------------------------------------------------- /expenses/setup/__init__.py: -------------------------------------------------------------------------------- 1 | # Expenses © 2024 2 | # Author: Ameen Ahmed 3 | # Company: Level Up Marketing & Software Development Services 4 | # Licence: Please refer to LICENSE file -------------------------------------------------------------------------------- /expenses/setup/install.py: -------------------------------------------------------------------------------- 1 | # Expenses © 2024 2 | # Author: Ameen Ahmed 3 | # Company: Level Up Marketing & Software Development Services 4 | # Licence: Please refer to LICENSE file 5 | 6 | 7 | # [Hooks] 8 | def before_install(): 9 | from expenses import __production__ 10 | 11 | if not __production__: 12 | from .uninstall import after_uninstall 13 | 14 | after_uninstall() 15 | 16 | 17 | # [Hooks] 18 | def after_sync(): 19 | _settings_setup() 20 | _workspace_setup() 21 | 22 | 23 | # [Internal] 24 | def _settings_setup(): 25 | from frappe.utils import now 26 | from frappe.utils.user import get_system_managers 27 | 28 | from expenses import __version__ 29 | 30 | from expenses.libs.system import settings 31 | 32 | try: 33 | doc = settings() 34 | managers = get_system_managers(only_name=True) 35 | if managers: 36 | if "Administrator" in managers: 37 | doc.update_notification_sender = "Administrator" 38 | else: 39 | doc.update_notification_sender = managers[0] 40 | if doc.update_notification_receivers: 41 | receivers = [v.user for v in doc.update_notification_receivers] 42 | else: 43 | receivers = [] 44 | for manager in managers: 45 | if manager not in receivers: 46 | receivers.append(manager) 47 | doc.append( 48 | "update_notification_receivers", 49 | {"user": manager} 50 | ) 51 | if not doc.update_notification_receivers: 52 | doc.send_update_notification = 0 53 | else: 54 | doc.send_update_notification = 1 55 | else: 56 | doc.send_update_notification = 0 57 | 58 | doc.current_version = __version__ 59 | doc.latest_version = __version__ 60 | doc.latest_check = now() 61 | doc.has_update = 0 62 | doc.save(ignore_permissions=True) 63 | except Exception: 64 | pass 65 | 66 | 67 | # [Internal] 68 | def _workspace_setup(): 69 | import frappe 70 | 71 | try: 72 | dt = "Workspace" 73 | name = "Accounting" 74 | if not frappe.db.exists(dt, name): 75 | return 0 76 | 77 | from .uninstall import get_doctypes 78 | 79 | doc = frappe.get_doc(dt, name) 80 | doctypes = get_doctypes() 81 | doctypes = doctypes[6:] 82 | for doctype in doctypes: 83 | doc.append("links", { 84 | "dependencies": "", 85 | "hidden": 0, 86 | "is_query_report": 0, 87 | "label": doctype, 88 | "link_count": 0, 89 | "link_to": doctype, 90 | "link_type": "DocType", 91 | "onboard": 0, 92 | "type": "Link" 93 | }) 94 | doc.save(ignore_permissions=True) 95 | except Exception: 96 | pass -------------------------------------------------------------------------------- /expenses/setup/migrate.py: -------------------------------------------------------------------------------- 1 | # Expenses © 2024 2 | # Author: Ameen Ahmed 3 | # Company: Level Up Marketing & Software Development Services 4 | # Licence: Please refer to LICENSE file 5 | 6 | 7 | # [Hooks] 8 | def after_migrate(): 9 | from expenses import __version__ 10 | 11 | from expenses.libs.settings import settings 12 | 13 | doc = settings() 14 | if doc.current_version != __version__: 15 | from frappe.utils import now 16 | 17 | doc.current_version = __version__ 18 | doc.latest_version = __version__ 19 | doc.latest_check = now() 20 | doc.has_update = 0 21 | doc.save(ignore_permissions=True) -------------------------------------------------------------------------------- /expenses/setup/uninstall.py: -------------------------------------------------------------------------------- 1 | # Expenses © 2024 2 | # Author: Ameen Ahmed 3 | # Company: Level Up Marketing & Software Development Services 4 | # Licence: Please refer to LICENSE file 5 | 6 | 7 | import frappe 8 | 9 | 10 | # [Install, Internal] 11 | def get_doctypes(): 12 | return [ 13 | "Expense Attachment", 14 | "Expense Item Account", 15 | "Expense Type Account", 16 | "Expenses Entry Details", 17 | "Expenses Request Details", 18 | "Expenses Update Receiver", 19 | "Expenses Entry", 20 | "Expenses Request", 21 | "Expense", 22 | "Expense Item", 23 | "Expense Type", 24 | "Expenses Settings" 25 | ] 26 | 27 | 28 | # [Install, Hooks] 29 | def after_uninstall(): 30 | doctypes = get_doctypes() 31 | roles = [ 32 | "Expense Supervisor", 33 | "Expenses Reviewer", 34 | 35 | "Expense Moderator", 36 | "Expenses Request Moderator", 37 | "Expenses Request Reviewer", 38 | "Expenses Entry Moderator" 39 | ] 40 | _workspace_uninstall(doctypes) 41 | _fixtures_uninstall(roles) 42 | _doctypes_uninstall(doctypes, roles) 43 | _fixtures_cleanup() 44 | frappe.clear_cache() 45 | 46 | 47 | # [Internal] 48 | def _workspace_uninstall(doctypes): 49 | try: 50 | dt = "Workspace" 51 | name = "Accounting" 52 | if not frappe.db.exists(dt, name): 53 | return 0 54 | 55 | doc = frappe.get_doc(dt, name) 56 | remove = [] 57 | for v in doc.links: 58 | if v.link_to and v.link_to in doctypes: 59 | remove.append(v) 60 | if remove: 61 | for i in range(len(remove)): 62 | doc.links.remove(remove.pop(0)) 63 | doc.save(ignore_permissions=True) 64 | except Exception: 65 | pass 66 | 67 | 68 | # [Internal] 69 | def _fixtures_uninstall(roles): 70 | try: 71 | doc = frappe.qb.DocType("Has Role") 72 | ( 73 | frappe.qb.from_(doc) 74 | .delete() 75 | .where(doc.role.isin(roles)) 76 | ).run() 77 | except Exception: 78 | pass 79 | 80 | 81 | # [Internal] 82 | def _doctypes_uninstall(doctypes, roles): 83 | from frappe.model.delete_doc import delete_doc 84 | 85 | from expenses import __module__ 86 | 87 | docs = [ 88 | ["Workspace", [__module__]], 89 | ["Report", ["Expenses Entry Report"]], 90 | ["Workflow", ["Expenses Request Review"]], 91 | ["Print Format", [ 92 | "Expense", 93 | "Expenses Entry", 94 | "Expenses Request" 95 | ]], 96 | ["DocType", doctypes], 97 | ["Role", roles], 98 | ["Module Def", [__module__]] 99 | ] 100 | for v in docs: 101 | for name in v[1]: 102 | try: 103 | delete_doc( 104 | v[0], name, 105 | ignore_permissions=True, 106 | ignore_missing=True, 107 | ignore_on_trash=True, 108 | delete_permanently=True 109 | ) 110 | except Exception: 111 | pass 112 | 113 | 114 | # [Internal] 115 | def _fixtures_cleanup(): 116 | try: 117 | actions = [ 118 | "Submit", 119 | "Cancel", 120 | "Approve", 121 | "Reject" 122 | ] 123 | count = frappe.db.count( 124 | "Workflow Transition", 125 | {"action": ["in", actions]} 126 | ) 127 | if not count: 128 | doc = frappe.qb.DocType("Workflow Action Master") 129 | ( 130 | frappe.qb.from_(doc) 131 | .delete() 132 | .where(doc.name.isin(actions)) 133 | ).run() 134 | except Exception: 135 | pass 136 | 137 | try: 138 | states = [ 139 | "Draft", 140 | "Pending", 141 | "Cancelled", 142 | "Approved", 143 | "Rejected", 144 | "Processed" 145 | ] 146 | count = frappe.db.count( 147 | "Workflow Transition", 148 | {"state": ["in", states]} 149 | ) 150 | if not count: 151 | count = frappe.doc.count( 152 | "Workflow Document State", 153 | {"state": ["in", states]} 154 | ) 155 | if not count: 156 | doc = frappe.qb.DocType("Workflow State") 157 | ( 158 | frappe.qb.from_(doc) 159 | .delete() 160 | .where(doc.name.isin(states)) 161 | ).run() 162 | except Exception: 163 | pass -------------------------------------------------------------------------------- /expenses/version.py: -------------------------------------------------------------------------------- 1 | # Expenses © 2024 2 | # Author: Ameen Ahmed 3 | # Company: Level Up Marketing & Software Development Services 4 | # Licence: Please refer to LICENSE file 5 | 6 | 7 | from frappe import __version__ 8 | 9 | 10 | # [Internal] 11 | __frappe_version__ = int(__version__.split(".")[0]) 12 | 13 | 14 | # [Background] 15 | def is_version_gt(num: int): 16 | return __frappe_version__ > num 17 | 18 | 19 | # [Common] 20 | def is_version_lt(num: int): 21 | return __frappe_version__ < num -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "expenses" 3 | authors = [ 4 | {name = "Ameen Ahmed (Level Up)", email = "kid1194@gmail.com"} 5 | ] 6 | description = "An expenses management module for ERPNext." 7 | keywords = ["frappe", "erpnext", "expenses"] 8 | classifiers = [ 9 | "Development Status :: 3 - Alpha", 10 | "License :: OSI Approved :: MIT License", 11 | "Programming Language :: Python" 12 | ] 13 | requires-python = ">=3.6" 14 | readme = "README.md" 15 | dynamic = ["version"] 16 | dependencies = [ 17 | "frappe>=13.0.0", 18 | "erpnext>=13.0.0" 19 | ] 20 | 21 | [project.urls] 22 | Documentation = "https://github.com/kid1194/erpnext_expenses" 23 | Source = "https://github.com/kid1194/erpnext_expenses" -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | frappe>=13.0.0 2 | erpnext>=13.0.0 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Expenses © 2024 2 | # Author: Ameen Ahmed 3 | # Company: Level Up Marketing & Software Development Services 4 | # Licence: Please refer to LICENSE file 5 | 6 | 7 | from setuptools import setup, find_packages 8 | 9 | 10 | with open("requirements.txt") as f: 11 | install_requires = f.read().strip().split("\n") 12 | 13 | 14 | setup( 15 | name="expenses", 16 | version="1.0.0", 17 | description="An expenses management module for ERPNext", 18 | author="Ameen Ahmed (Level Up)", 19 | author_email="kid1194@gmail.com", 20 | packages=find_packages(), 21 | zip_safe=False, 22 | include_package_data=True, 23 | install_requires=install_requires 24 | ) --------------------------------------------------------------------------------