├── .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 | 
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 | - [](https://github.com/monolithon)
14 | - [](https://github.com/agrogers)
15 | - [](https://github.com/washaqq)
16 | - [](https://github.com/hassan-youssef)
17 | - [](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 | \
48 | ' + (cint(frm.doc.has_update) > 0
49 | ? '\
50 | - \
51 | ' + __('Status') + ': \
52 | ' + __('New version available') + '\
53 |
\
54 | - \
55 | ' + __('Latest Version') + ': \
56 | ' + frm.doc.latest_version + '\
57 |
\
58 | '
59 | : '\
60 | - \
61 | ' + __('Status') + ': \
62 | ' + __('App is up to date') + '\
63 |
\
64 | ') + '\
65 | - \
66 | ' + __('Current Version') + ': \
67 | ' + frm.doc.current_version + '\
68 |
\
69 | - \
70 | ' + __('Latest Check') + ': \
71 | ' + frappe.datetime.user_to_str(frm.doc.latest_check) + '\
72 |
\
73 |
\
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 | {{ fcol[0] }} |
19 | : |
20 | {{ fcol[1] }} |
21 | {{ scol[0] }} |
22 | : |
23 | {{ scol[1] }} |
24 |
25 | {%- endfor -%}
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | {{ _("Cost") }} |
38 | {{ doc.get_formatted("cost") }} |
39 | {{ _("Quantity") }} |
40 | {{ doc.get_formatted("qty") }} |
41 |
42 |
43 | {{ _("Total") }} |
44 | {{ doc.get_formatted("total") }} |
45 | {{ _("Is Advance") }} |
46 | {{ _("Yes") if cint(doc.is_advance) else _("No") }} |
47 |
48 |
49 |
50 |
51 | {% if doc.description %}
52 |
53 |
54 |
55 |
56 |
57 |
58 | {{ doc.description }}
59 |
60 |
61 | {% endif %}
62 |
63 |
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 | {{ fcol[0] }} |
17 | : |
18 | {{ fcol[1] }} |
19 | {{ scol[0] }} |
20 | : |
21 | {{ scol[1] }} |
22 |
23 | {%- endfor -%}
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | {{ _("Expense Account") }} |
36 | {{ _("Description") }} |
37 | {{ _("Is Advance") }} |
38 | {{ _("Cost") }} |
39 |
40 | {% for exp in doc.expenses %}
41 |
42 | {{ exp.account }} |
43 | {{ exp.description }} |
44 | {{ _("Yes") if exp.is_advance else _("No") }} |
45 | {{ exp.get_formatted("cost"}) }} |
46 |
47 | {% endfor %}
48 |
49 | {{ _("Exchange Rate") }} |
50 | {{ doc.get_formatted("exchange_rate") }} |
51 |
52 |
53 | {{ _("Total") }} |
54 | {{ doc.get_formatted("total") }} |
55 |
56 |
57 |
58 |
59 | {% if doc.remarks %}
60 |
61 |
62 |
63 |
64 |
65 |
66 | {{ doc.remarks }}
67 |
68 |
69 | {% endif %}
70 |
71 |
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 | {{ fcol[0] }} |
15 | : |
16 | {{ fcol[1] }} |
17 | {{ scol[0] }} |
18 | : |
19 | {{ scol[1] }} |
20 |
21 | {%- endfor -%}
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | {{ _("Expense") }} |
34 | {{ _("Expense Item") }} |
35 | {{ _("Description") }} |
36 | {{ _("Total") }} |
37 | {{ _("Is Advance") }} |
38 | {{ _("Required By") }} |
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 | {{ exp.name }} |
45 | {{ exp.expense_item }} |
46 | {{ exp.description }} |
47 | {{ exp.get_formatted("total") }} |
48 | {{ _("Yes") if exp.is_advance else _("No") }} |
49 | {{ frappe.format_date(exp.required_by) }} |
50 |
51 | {% endfor %}
52 |
53 |
54 |
55 | {% if doc.remarks %}
56 |
57 |
58 |
59 |
60 |
61 |
62 | {{ doc.remarks }}
63 |
64 |
65 | {% endif %}
66 |
67 |
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 | {%= __("Expenses Entry") %} |
23 | {%= __("Mode of Payment") %} |
24 | {%= __("Posting Date") %} |
25 | {%= __("Total") %} |
26 | {%= __("Payment Reference") %} |
27 | {%= __("Clearance Date") %} |
28 | {%= __("Remarks") %} |
29 |
30 |
31 |
32 | {% for(var i=0, l=data.length; i
34 | {%= frappe.format(data[i].expenses_entry, {fieldtype: "Link"}) || " " %} |
35 | {%= frappe.format(data[i].mode_of_payment, {fieldtype: "Link"}) || " " %} |
36 | {%= frappe.datetime.str_to_user(data[i].posting_date) %} |
37 | {%= format_currency(data[i].total, data[i].currency) %} |
38 | {%= data[i].payment_reference || " " %} |
39 | {%= frappe.datetime.str_to_user(data[i].clearance_date) || " " %} |
40 | {%= data[i].payment_reference %} |
41 |
42 | {% } %}
43 |
44 |
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 | )
--------------------------------------------------------------------------------