mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-29 18:04:46 +00:00
Merge branch 'develop' into opportunity-status
This commit is contained in:
48
.github/helper/documentation.py
vendored
Normal file
48
.github/helper/documentation.py
vendored
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import sys
|
||||||
|
import requests
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
|
||||||
|
docs_repos = [
|
||||||
|
"frappe_docs",
|
||||||
|
"erpnext_documentation",
|
||||||
|
"erpnext_com",
|
||||||
|
"frappe_io",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def uri_validator(x):
|
||||||
|
result = urlparse(x)
|
||||||
|
return all([result.scheme, result.netloc, result.path])
|
||||||
|
|
||||||
|
def docs_link_exists(body):
|
||||||
|
for line in body.splitlines():
|
||||||
|
for word in line.split():
|
||||||
|
if word.startswith('http') and uri_validator(word):
|
||||||
|
parsed_url = urlparse(word)
|
||||||
|
if parsed_url.netloc == "github.com":
|
||||||
|
_, org, repo, _type, ref = parsed_url.path.split('/')
|
||||||
|
if org == "frappe" and repo in docs_repos:
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
pr = sys.argv[1]
|
||||||
|
response = requests.get("https://api.github.com/repos/frappe/erpnext/pulls/{}".format(pr))
|
||||||
|
|
||||||
|
if response.ok:
|
||||||
|
payload = response.json()
|
||||||
|
title = payload.get("title", "").lower()
|
||||||
|
head_sha = payload.get("head", {}).get("sha")
|
||||||
|
body = payload.get("body", "").lower()
|
||||||
|
|
||||||
|
if title.startswith("feat") and head_sha and "no-docs" not in body:
|
||||||
|
if docs_link_exists(body):
|
||||||
|
print("Documentation Link Found. You're Awesome! 🎉")
|
||||||
|
|
||||||
|
else:
|
||||||
|
print("Documentation Link Not Found! ⚠️")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
else:
|
||||||
|
print("Skipping documentation checks... 🏃")
|
||||||
60
.github/helper/translation.py
vendored
Normal file
60
.github/helper/translation.py
vendored
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
errors_encounter = 0
|
||||||
|
pattern = re.compile(r"_\(([\"']{,3})(?P<message>((?!\1).)*)\1(\s*,\s*context\s*=\s*([\"'])(?P<py_context>((?!\5).)*)\5)*(\s*,\s*(.)*?\s*(,\s*([\"'])(?P<js_context>((?!\11).)*)\11)*)*\)")
|
||||||
|
words_pattern = re.compile(r"_{1,2}\([\"'`]{1,3}.*?[a-zA-Z]")
|
||||||
|
start_pattern = re.compile(r"_{1,2}\([f\"'`]{1,3}")
|
||||||
|
f_string_pattern = re.compile(r"_\(f[\"']")
|
||||||
|
starts_with_f_pattern = re.compile(r"_\(f")
|
||||||
|
|
||||||
|
# skip first argument
|
||||||
|
files = sys.argv[1:]
|
||||||
|
files_to_scan = [_file for _file in files if _file.endswith(('.py', '.js'))]
|
||||||
|
|
||||||
|
for _file in files_to_scan:
|
||||||
|
with open(_file, 'r') as f:
|
||||||
|
print(f'Checking: {_file}')
|
||||||
|
file_lines = f.readlines()
|
||||||
|
for line_number, line in enumerate(file_lines, 1):
|
||||||
|
if 'frappe-lint: disable-translate' in line:
|
||||||
|
continue
|
||||||
|
|
||||||
|
start_matches = start_pattern.search(line)
|
||||||
|
if start_matches:
|
||||||
|
starts_with_f = starts_with_f_pattern.search(line)
|
||||||
|
|
||||||
|
if starts_with_f:
|
||||||
|
has_f_string = f_string_pattern.search(line)
|
||||||
|
if has_f_string:
|
||||||
|
errors_encounter += 1
|
||||||
|
print(f'\nF-strings are not supported for translations at line number {line_number + 1}\n{line.strip()[:100]}')
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
|
||||||
|
match = pattern.search(line)
|
||||||
|
error_found = False
|
||||||
|
|
||||||
|
if not match and line.endswith(',\n'):
|
||||||
|
# concat remaining text to validate multiline pattern
|
||||||
|
line = "".join(file_lines[line_number - 1:])
|
||||||
|
line = line[start_matches.start() + 1:]
|
||||||
|
match = pattern.match(line)
|
||||||
|
|
||||||
|
if not match:
|
||||||
|
error_found = True
|
||||||
|
print(f'\nTranslation syntax error at line number {line_number + 1}\n{line.strip()[:100]}')
|
||||||
|
|
||||||
|
if not error_found and not words_pattern.search(line):
|
||||||
|
error_found = True
|
||||||
|
print(f'\nTranslation is useless because it has no words at line number {line_number + 1}\n{line.strip()[:100]}')
|
||||||
|
|
||||||
|
if error_found:
|
||||||
|
errors_encounter += 1
|
||||||
|
|
||||||
|
if errors_encounter > 0:
|
||||||
|
print('\nVisit "https://frappeframework.com/docs/user/en/translations" to learn about valid translation strings.')
|
||||||
|
sys.exit(1)
|
||||||
|
else:
|
||||||
|
print('\nGood To Go!')
|
||||||
24
.github/workflows/docs-checker.yml
vendored
Normal file
24
.github/workflows/docs-checker.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
name: 'Documentation Required'
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [ opened, synchronize, reopened, edited ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: 'Setup Environment'
|
||||||
|
uses: actions/setup-python@v2
|
||||||
|
with:
|
||||||
|
python-version: 3.6
|
||||||
|
|
||||||
|
- name: 'Clone repo'
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Validate Docs
|
||||||
|
env:
|
||||||
|
PR_NUMBER: ${{ github.event.number }}
|
||||||
|
run: |
|
||||||
|
pip install requests --quiet
|
||||||
|
python $GITHUB_WORKSPACE/.github/helper/documentation.py $PR_NUMBER
|
||||||
22
.github/workflows/translation_linter.yml
vendored
Normal file
22
.github/workflows/translation_linter.yml
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
name: Frappe Linter
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
- version-12-hotfix
|
||||||
|
- version-11-hotfix
|
||||||
|
jobs:
|
||||||
|
check_translation:
|
||||||
|
name: Translation Syntax Check
|
||||||
|
runs-on: ubuntu-18.04
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Setup python3
|
||||||
|
uses: actions/setup-python@v1
|
||||||
|
with:
|
||||||
|
python-version: 3.6
|
||||||
|
- name: Validating Translation Syntax
|
||||||
|
run: |
|
||||||
|
git fetch origin $GITHUB_BASE_REF:$GITHUB_BASE_REF -q
|
||||||
|
files=$(git diff --name-only --diff-filter=d $GITHUB_BASE_REF)
|
||||||
|
python $GITHUB_WORKSPACE/.github/helper/translation.py $files
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
"_comments": null,
|
"_comments": null,
|
||||||
"_liked_by": null,
|
"_liked_by": null,
|
||||||
"_user_tags": null,
|
"_user_tags": null,
|
||||||
|
"allow_in_quick_entry": 0,
|
||||||
"allow_on_submit": 0,
|
"allow_on_submit": 0,
|
||||||
"bold": 0,
|
"bold": 0,
|
||||||
"collapsible": 0,
|
"collapsible": 0,
|
||||||
@@ -17,17 +18,24 @@
|
|||||||
"docstatus": 0,
|
"docstatus": 0,
|
||||||
"dt": "Address",
|
"dt": "Address",
|
||||||
"fetch_from": null,
|
"fetch_from": null,
|
||||||
|
"fetch_if_empty": 0,
|
||||||
"fieldname": "tax_category",
|
"fieldname": "tax_category",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"hidden": 0,
|
"hidden": 0,
|
||||||
"idx": 14,
|
"hide_border": 0,
|
||||||
|
"hide_days": 0,
|
||||||
|
"hide_seconds": 0,
|
||||||
|
"idx": 15,
|
||||||
"ignore_user_permissions": 0,
|
"ignore_user_permissions": 0,
|
||||||
"ignore_xss_filter": 0,
|
"ignore_xss_filter": 0,
|
||||||
"in_global_search": 0,
|
"in_global_search": 0,
|
||||||
"in_list_view": 0,
|
"in_list_view": 0,
|
||||||
|
"in_preview": 0,
|
||||||
"in_standard_filter": 0,
|
"in_standard_filter": 0,
|
||||||
"insert_after": "fax",
|
"insert_after": "fax",
|
||||||
"label": "Tax Category",
|
"label": "Tax Category",
|
||||||
|
"length": 0,
|
||||||
|
"mandatory_depends_on": null,
|
||||||
"modified": "2018-12-28 22:29:21.828090",
|
"modified": "2018-12-28 22:29:21.828090",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"name": "Address-tax_category",
|
"name": "Address-tax_category",
|
||||||
@@ -43,6 +51,66 @@
|
|||||||
"print_hide_if_no_value": 0,
|
"print_hide_if_no_value": 0,
|
||||||
"print_width": null,
|
"print_width": null,
|
||||||
"read_only": 0,
|
"read_only": 0,
|
||||||
|
"read_only_depends_on": null,
|
||||||
|
"report_hide": 0,
|
||||||
|
"reqd": 0,
|
||||||
|
"search_index": 0,
|
||||||
|
"translatable": 0,
|
||||||
|
"unique": 0,
|
||||||
|
"width": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"_assign": null,
|
||||||
|
"_comments": null,
|
||||||
|
"_liked_by": null,
|
||||||
|
"_user_tags": null,
|
||||||
|
"allow_in_quick_entry": 0,
|
||||||
|
"allow_on_submit": 0,
|
||||||
|
"bold": 0,
|
||||||
|
"collapsible": 0,
|
||||||
|
"collapsible_depends_on": null,
|
||||||
|
"columns": 0,
|
||||||
|
"creation": "2020-10-14 17:41:40.878179",
|
||||||
|
"default": "0",
|
||||||
|
"depends_on": null,
|
||||||
|
"description": null,
|
||||||
|
"docstatus": 0,
|
||||||
|
"dt": "Address",
|
||||||
|
"fetch_from": null,
|
||||||
|
"fetch_if_empty": 0,
|
||||||
|
"fieldname": "is_your_company_address",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"hidden": 0,
|
||||||
|
"hide_border": 0,
|
||||||
|
"hide_days": 0,
|
||||||
|
"hide_seconds": 0,
|
||||||
|
"idx": 20,
|
||||||
|
"ignore_user_permissions": 0,
|
||||||
|
"ignore_xss_filter": 0,
|
||||||
|
"in_global_search": 0,
|
||||||
|
"in_list_view": 0,
|
||||||
|
"in_preview": 0,
|
||||||
|
"in_standard_filter": 0,
|
||||||
|
"insert_after": "linked_with",
|
||||||
|
"label": "Is Your Company Address",
|
||||||
|
"length": 0,
|
||||||
|
"mandatory_depends_on": null,
|
||||||
|
"modified": "2020-10-14 17:41:40.878179",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"name": "Address-is_your_company_address",
|
||||||
|
"no_copy": 0,
|
||||||
|
"options": null,
|
||||||
|
"owner": "Administrator",
|
||||||
|
"parent": null,
|
||||||
|
"parentfield": null,
|
||||||
|
"parenttype": null,
|
||||||
|
"permlevel": 0,
|
||||||
|
"precision": "",
|
||||||
|
"print_hide": 0,
|
||||||
|
"print_hide_if_no_value": 0,
|
||||||
|
"print_width": null,
|
||||||
|
"read_only": 0,
|
||||||
|
"read_only_depends_on": null,
|
||||||
"report_hide": 0,
|
"report_hide": 0,
|
||||||
"reqd": 0,
|
"reqd": 0,
|
||||||
"search_index": 0,
|
"search_index": 0,
|
||||||
|
|||||||
42
erpnext/accounts/custom/address.py
Normal file
42
erpnext/accounts/custom/address.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import frappe
|
||||||
|
from frappe import _
|
||||||
|
from frappe.contacts.doctype.address.address import Address
|
||||||
|
from frappe.contacts.doctype.address.address import get_address_templates
|
||||||
|
|
||||||
|
class ERPNextAddress(Address):
|
||||||
|
def validate(self):
|
||||||
|
self.validate_reference()
|
||||||
|
super(ERPNextAddress, self).validate()
|
||||||
|
|
||||||
|
def link_address(self):
|
||||||
|
"""Link address based on owner"""
|
||||||
|
if self.is_your_company_address:
|
||||||
|
return
|
||||||
|
|
||||||
|
return super(ERPNextAddress, self).link_address()
|
||||||
|
|
||||||
|
def validate_reference(self):
|
||||||
|
if self.is_your_company_address and not [
|
||||||
|
row for row in self.links if row.link_doctype == "Company"
|
||||||
|
]:
|
||||||
|
frappe.throw(_("Address needs to be linked to a Company. Please add a row for Company in the Links table."),
|
||||||
|
title=_("Company Not Linked"))
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def get_shipping_address(company, address = None):
|
||||||
|
filters = [
|
||||||
|
["Dynamic Link", "link_doctype", "=", "Company"],
|
||||||
|
["Dynamic Link", "link_name", "=", company],
|
||||||
|
["Address", "is_your_company_address", "=", 1]
|
||||||
|
]
|
||||||
|
fields = ["*"]
|
||||||
|
if address and frappe.db.get_value('Dynamic Link',
|
||||||
|
{'parent': address, 'link_name': company}):
|
||||||
|
filters.append(["Address", "name", "=", address])
|
||||||
|
|
||||||
|
address = frappe.get_all("Address", filters=filters, fields=fields) or {}
|
||||||
|
|
||||||
|
if address:
|
||||||
|
address_as_dict = address[0]
|
||||||
|
name, address_template = get_address_templates(address_as_dict)
|
||||||
|
return address_as_dict.get("name"), frappe.render_template(address_template, address_as_dict)
|
||||||
@@ -108,7 +108,7 @@
|
|||||||
"pin_to_top": 0,
|
"pin_to_top": 0,
|
||||||
"shortcuts": [
|
"shortcuts": [
|
||||||
{
|
{
|
||||||
"label": "Chart Of Accounts",
|
"label": "Chart of Accounts",
|
||||||
"link_to": "Account",
|
"link_to": "Account",
|
||||||
"type": "DocType"
|
"type": "DocType"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ frappe.provide("frappe.treeview_settings")
|
|||||||
|
|
||||||
frappe.treeview_settings["Account"] = {
|
frappe.treeview_settings["Account"] = {
|
||||||
breadcrumb: "Accounts",
|
breadcrumb: "Accounts",
|
||||||
title: __("Chart Of Accounts"),
|
title: __("Chart of Accounts"),
|
||||||
get_tree_root: false,
|
get_tree_root: false,
|
||||||
filters: [
|
filters: [
|
||||||
{
|
{
|
||||||
@@ -97,7 +97,7 @@ frappe.treeview_settings["Account"] = {
|
|||||||
treeview.page.add_inner_button(__("Journal Entry"), function() {
|
treeview.page.add_inner_button(__("Journal Entry"), function() {
|
||||||
frappe.new_doc('Journal Entry', {company: get_company()});
|
frappe.new_doc('Journal Entry', {company: get_company()});
|
||||||
}, __('Create'));
|
}, __('Create'));
|
||||||
treeview.page.add_inner_button(__("New Company"), function() {
|
treeview.page.add_inner_button(__("Company"), function() {
|
||||||
frappe.new_doc('Company');
|
frappe.new_doc('Company');
|
||||||
}, __('Create'));
|
}, __('Create'));
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,7 @@
|
|||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"default": "1",
|
"default": "1",
|
||||||
"description": "If enabled, the system will post accounting entries for inventory automatically.",
|
"description": "If enabled, the system will post accounting entries for inventory automatically",
|
||||||
"fieldname": "auto_accounting_for_stock",
|
"fieldname": "auto_accounting_for_stock",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"hidden": 1,
|
"hidden": 1,
|
||||||
@@ -48,23 +48,23 @@
|
|||||||
"label": "Make Accounting Entry For Every Stock Movement"
|
"label": "Make Accounting Entry For Every Stock Movement"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "Accounting entry frozen up to this date, nobody can do / modify entry except role specified below.",
|
"description": "Accounting entries are frozen up to this date. Nobody can create or modify entries except users with the role specified below",
|
||||||
"fieldname": "acc_frozen_upto",
|
"fieldname": "acc_frozen_upto",
|
||||||
"fieldtype": "Date",
|
"fieldtype": "Date",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Accounts Frozen Upto"
|
"label": "Accounts Frozen Till Date"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "Users with this role are allowed to set frozen accounts and create / modify accounting entries against frozen accounts",
|
"description": "Users with this role are allowed to set frozen accounts and create / modify accounting entries against frozen accounts",
|
||||||
"fieldname": "frozen_accounts_modifier",
|
"fieldname": "frozen_accounts_modifier",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Role Allowed to Set Frozen Accounts & Edit Frozen Entries",
|
"label": "Role Allowed to Set Frozen Accounts and Edit Frozen Entries",
|
||||||
"options": "Role"
|
"options": "Role"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "Billing Address",
|
"default": "Billing Address",
|
||||||
"description": "Address used to determine Tax Category in transactions.",
|
"description": "Address used to determine Tax Category in transactions",
|
||||||
"fieldname": "determine_address_tax_category_from",
|
"fieldname": "determine_address_tax_category_from",
|
||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"label": "Determine Address Tax Category From",
|
"label": "Determine Address Tax Category From",
|
||||||
@@ -75,7 +75,7 @@
|
|||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "Role that is allowed to submit transactions that exceed credit limits set.",
|
"description": "This role is allowed to submit transactions that exceed credit limits",
|
||||||
"fieldname": "credit_controller",
|
"fieldname": "credit_controller",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
@@ -127,7 +127,7 @@
|
|||||||
"default": "0",
|
"default": "0",
|
||||||
"fieldname": "show_inclusive_tax_in_print",
|
"fieldname": "show_inclusive_tax_in_print",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Show Inclusive Tax In Print"
|
"label": "Show Inclusive Tax in Print"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "column_break_12",
|
"fieldname": "column_break_12",
|
||||||
@@ -165,7 +165,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
"description": "Only select if you have setup Cash Flow Mapper documents",
|
"description": "Only select this if you have set up the Cash Flow Mapper documents",
|
||||||
"fieldname": "use_custom_cash_flow",
|
"fieldname": "use_custom_cash_flow",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Use Custom Cash Flow Format"
|
"label": "Use Custom Cash Flow Format"
|
||||||
@@ -177,7 +177,7 @@
|
|||||||
"label": "Automatically Fetch Payment Terms"
|
"label": "Automatically Fetch Payment Terms"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "Percentage you are allowed to bill more against the amount ordered. For example: If the order value is $100 for an item and tolerance is set as 10% then you are allowed to bill for $110.",
|
"description": "The percentage you are allowed to bill more against the amount ordered. For example, if the order value is $100 for an item and tolerance is set as 10%, then you are allowed to bill up to $110 ",
|
||||||
"fieldname": "over_billing_allowance",
|
"fieldname": "over_billing_allowance",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"label": "Over Billing Allowance (%)"
|
"label": "Over Billing Allowance (%)"
|
||||||
@@ -199,7 +199,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
"description": "If this is unchecked direct GL Entries will be created to book Deferred Revenue/Expense",
|
"description": "If this is unchecked, direct GL entries will be created to book deferred revenue or expense",
|
||||||
"fieldname": "book_deferred_entries_via_journal_entry",
|
"fieldname": "book_deferred_entries_via_journal_entry",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Book Deferred Entries Via Journal Entry"
|
"label": "Book Deferred Entries Via Journal Entry"
|
||||||
@@ -214,7 +214,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "Days",
|
"default": "Days",
|
||||||
"description": "If \"Months\" is selected then fixed amount will be booked as deferred revenue or expense for each month irrespective of number of days in a month. Will be prorated if deferred revenue or expense is not booked for an entire month.",
|
"description": "If \"Months\" is selected, a fixed amount will be booked as deferred revenue or expense for each month irrespective of the number of days in a month. It will be prorated if deferred revenue or expense is not booked for an entire month",
|
||||||
"fieldname": "book_deferred_entries_based_on",
|
"fieldname": "book_deferred_entries_based_on",
|
||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"label": "Book Deferred Entries Based On",
|
"label": "Book Deferred Entries Based On",
|
||||||
@@ -226,7 +226,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-10-07 14:58:50.325577",
|
"modified": "2020-10-13 11:32:52.268826",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Accounts Settings",
|
"name": "Accounts Settings",
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class CashierClosing(Document):
|
|||||||
for i in self.payments:
|
for i in self.payments:
|
||||||
total += flt(i.amount)
|
total += flt(i.amount)
|
||||||
|
|
||||||
self.net_amount = total + self.outstanding_amount + self.expense - self.custody + self.returns
|
self.net_amount = total + self.outstanding_amount + flt(self.expense) - flt(self.custody) + flt(self.returns)
|
||||||
|
|
||||||
def validate_time(self):
|
def validate_time(self):
|
||||||
if self.from_time >= self.time:
|
if self.from_time >= self.time:
|
||||||
|
|||||||
@@ -195,7 +195,7 @@ def build_response_as_excel(writer):
|
|||||||
reader = csv.reader(f)
|
reader = csv.reader(f)
|
||||||
|
|
||||||
from frappe.utils.xlsxutils import make_xlsx
|
from frappe.utils.xlsxutils import make_xlsx
|
||||||
xlsx_file = make_xlsx(reader, "Chart Of Accounts Importer Template")
|
xlsx_file = make_xlsx(reader, "Chart of Accounts Importer Template")
|
||||||
|
|
||||||
f.close()
|
f.close()
|
||||||
os.remove(filename)
|
os.remove(filename)
|
||||||
|
|||||||
@@ -210,7 +210,7 @@ erpnext.accounts.JournalEntry = frappe.ui.form.Controller.extend({
|
|||||||
$.each(this.frm.doc.accounts || [], function(i, jvd) {
|
$.each(this.frm.doc.accounts || [], function(i, jvd) {
|
||||||
frappe.model.set_default_values(jvd);
|
frappe.model.set_default_values(jvd);
|
||||||
});
|
});
|
||||||
var posting_date = this.frm.posting_date;
|
var posting_date = this.frm.doc.posting_date;
|
||||||
if(!this.frm.doc.amended_from) this.frm.set_value('posting_date', posting_date || frappe.datetime.get_today());
|
if(!this.frm.doc.amended_from) this.frm.set_value('posting_date', posting_date || frappe.datetime.get_today());
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"actions": [],
|
||||||
"allow_import": 1,
|
"allow_import": 1,
|
||||||
"allow_rename": 1,
|
"allow_rename": 1,
|
||||||
"autoname": "field:mode_of_payment",
|
"autoname": "field:mode_of_payment",
|
||||||
@@ -28,7 +29,7 @@
|
|||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"in_standard_filter": 1,
|
"in_standard_filter": 1,
|
||||||
"label": "Type",
|
"label": "Type",
|
||||||
"options": "Cash\nBank\nGeneral"
|
"options": "Cash\nBank\nGeneral\nPhone"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "accounts",
|
"fieldname": "accounts",
|
||||||
@@ -45,7 +46,9 @@
|
|||||||
],
|
],
|
||||||
"icon": "fa fa-credit-card",
|
"icon": "fa fa-credit-card",
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"modified": "2020-09-18 17:26:09.703215",
|
"index_web_pages_for_search": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2020-09-18 17:57:23.835236",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Mode of Payment",
|
"name": "Mode of Payment",
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ frappe.ui.form.on('Opening Invoice Creation Tool', {
|
|||||||
frm.set_query('party_type', 'invoices', function(doc, cdt, cdn) {
|
frm.set_query('party_type', 'invoices', function(doc, cdt, cdn) {
|
||||||
return {
|
return {
|
||||||
filters: {
|
filters: {
|
||||||
'name': ['in', 'Customer,Supplier']
|
'name': ['in', 'Customer, Supplier']
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -14,29 +14,46 @@ frappe.ui.form.on('Opening Invoice Creation Tool', {
|
|||||||
if (frm.doc.company) {
|
if (frm.doc.company) {
|
||||||
frm.trigger('setup_company_filters');
|
frm.trigger('setup_company_filters');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
frappe.realtime.on('opening_invoice_creation_progress', data => {
|
||||||
|
if (!frm.doc.import_in_progress) {
|
||||||
|
frm.dashboard.reset();
|
||||||
|
frm.doc.import_in_progress = true;
|
||||||
|
}
|
||||||
|
if (data.user != frappe.session.user) return;
|
||||||
|
if (data.count == data.total) {
|
||||||
|
setTimeout((title) => {
|
||||||
|
frm.doc.import_in_progress = false;
|
||||||
|
frm.clear_table("invoices");
|
||||||
|
frm.refresh_fields();
|
||||||
|
frm.page.clear_indicator();
|
||||||
|
frm.dashboard.hide_progress(title);
|
||||||
|
frappe.msgprint(__("Opening {0} Invoice created", [frm.doc.invoice_type]));
|
||||||
|
}, 1500, data.title);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
frm.dashboard.show_progress(data.title, (data.count / data.total) * 100, data.message);
|
||||||
|
frm.page.set_indicator(__('In Progress'), 'orange');
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
refresh: function(frm) {
|
refresh: function(frm) {
|
||||||
frm.disable_save();
|
frm.disable_save();
|
||||||
frm.trigger("make_dashboard");
|
!frm.doc.import_in_progress && frm.trigger("make_dashboard");
|
||||||
frm.page.set_primary_action(__('Create Invoices'), () => {
|
frm.page.set_primary_action(__('Create Invoices'), () => {
|
||||||
let btn_primary = frm.page.btn_primary.get(0);
|
let btn_primary = frm.page.btn_primary.get(0);
|
||||||
return frm.call({
|
return frm.call({
|
||||||
doc: frm.doc,
|
doc: frm.doc,
|
||||||
freeze: true,
|
|
||||||
btn: $(btn_primary),
|
btn: $(btn_primary),
|
||||||
method: "make_invoices",
|
method: "make_invoices",
|
||||||
freeze_message: __("Creating {0} Invoice", [frm.doc.invoice_type]),
|
freeze_message: __("Creating {0} Invoice", [frm.doc.invoice_type])
|
||||||
callback: (r) => {
|
|
||||||
if(!r.exc){
|
|
||||||
frappe.msgprint(__("Opening {0} Invoice created", [frm.doc.invoice_type]));
|
|
||||||
frm.clear_table("invoices");
|
|
||||||
frm.refresh_fields();
|
|
||||||
frm.reload_doc();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (frm.doc.create_missing_party) {
|
||||||
|
frm.set_df_property("party", "fieldtype", "Data", frm.doc.name, "invoices");
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
setup_company_filters: function(frm) {
|
setup_company_filters: function(frm) {
|
||||||
|
|||||||
@@ -4,9 +4,12 @@
|
|||||||
|
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
import frappe
|
import frappe
|
||||||
|
import traceback
|
||||||
|
from json import dumps
|
||||||
from frappe import _, scrub
|
from frappe import _, scrub
|
||||||
from frappe.utils import flt, nowdate
|
from frappe.utils import flt, nowdate
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
|
from frappe.utils.background_jobs import enqueue
|
||||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions
|
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions
|
||||||
|
|
||||||
|
|
||||||
@@ -62,66 +65,47 @@ class OpeningInvoiceCreationTool(Document):
|
|||||||
|
|
||||||
return invoices_summary, max_count
|
return invoices_summary, max_count
|
||||||
|
|
||||||
def make_invoices(self):
|
def validate_company(self):
|
||||||
names = []
|
|
||||||
mandatory_error_msg = _("Row {0}: {1} is required to create the Opening {2} Invoices")
|
|
||||||
if not self.company:
|
if not self.company:
|
||||||
frappe.throw(_("Please select the Company"))
|
frappe.throw(_("Please select the Company"))
|
||||||
|
|
||||||
company_details = frappe.get_cached_value('Company', self.company,
|
def set_missing_values(self, row):
|
||||||
["default_currency", "default_letter_head"], as_dict=1) or {}
|
row.qty = row.qty or 1.0
|
||||||
|
row.temporary_opening_account = row.temporary_opening_account or get_temporary_opening_account(self.company)
|
||||||
|
row.party_type = "Customer" if self.invoice_type == "Sales" else "Supplier"
|
||||||
|
row.item_name = row.item_name or _("Opening Invoice Item")
|
||||||
|
row.posting_date = row.posting_date or nowdate()
|
||||||
|
row.due_date = row.due_date or nowdate()
|
||||||
|
|
||||||
|
def validate_mandatory_invoice_fields(self, row):
|
||||||
|
if not frappe.db.exists(row.party_type, row.party):
|
||||||
|
if self.create_missing_party:
|
||||||
|
self.add_party(row.party_type, row.party)
|
||||||
|
else:
|
||||||
|
frappe.throw(_("Row #{}: {} {} does not exist.").format(row.idx, frappe.bold(row.party_type), frappe.bold(row.party)))
|
||||||
|
|
||||||
|
mandatory_error_msg = _("Row #{0}: {1} is required to create the Opening {2} Invoices")
|
||||||
|
for d in ("Party", "Outstanding Amount", "Temporary Opening Account"):
|
||||||
|
if not row.get(scrub(d)):
|
||||||
|
frappe.throw(mandatory_error_msg.format(row.idx, d, self.invoice_type))
|
||||||
|
|
||||||
|
def get_invoices(self):
|
||||||
|
invoices = []
|
||||||
for row in self.invoices:
|
for row in self.invoices:
|
||||||
if not row.qty:
|
if not row:
|
||||||
row.qty = 1.0
|
|
||||||
|
|
||||||
# always mandatory fields for the invoices
|
|
||||||
if not row.temporary_opening_account:
|
|
||||||
row.temporary_opening_account = get_temporary_opening_account(self.company)
|
|
||||||
row.party_type = "Customer" if self.invoice_type == "Sales" else "Supplier"
|
|
||||||
|
|
||||||
# Allow to create invoice even if no party present in customer or supplier.
|
|
||||||
if not frappe.db.exists(row.party_type, row.party):
|
|
||||||
if self.create_missing_party:
|
|
||||||
self.add_party(row.party_type, row.party)
|
|
||||||
else:
|
|
||||||
frappe.throw(_("{0} {1} does not exist.").format(frappe.bold(row.party_type), frappe.bold(row.party)))
|
|
||||||
|
|
||||||
if not row.item_name:
|
|
||||||
row.item_name = _("Opening Invoice Item")
|
|
||||||
if not row.posting_date:
|
|
||||||
row.posting_date = nowdate()
|
|
||||||
if not row.due_date:
|
|
||||||
row.due_date = nowdate()
|
|
||||||
|
|
||||||
for d in ("Party", "Outstanding Amount", "Temporary Opening Account"):
|
|
||||||
if not row.get(scrub(d)):
|
|
||||||
frappe.throw(mandatory_error_msg.format(row.idx, _(d), self.invoice_type))
|
|
||||||
|
|
||||||
args = self.get_invoice_dict(row=row)
|
|
||||||
if not args:
|
|
||||||
continue
|
continue
|
||||||
|
self.set_missing_values(row)
|
||||||
|
self.validate_mandatory_invoice_fields(row)
|
||||||
|
invoice = self.get_invoice_dict(row)
|
||||||
|
company_details = frappe.get_cached_value('Company', self.company, ["default_currency", "default_letter_head"], as_dict=1) or {}
|
||||||
if company_details:
|
if company_details:
|
||||||
args.update({
|
invoice.update({
|
||||||
"currency": company_details.get("default_currency"),
|
"currency": company_details.get("default_currency"),
|
||||||
"letter_head": company_details.get("default_letter_head")
|
"letter_head": company_details.get("default_letter_head")
|
||||||
})
|
})
|
||||||
|
invoices.append(invoice)
|
||||||
|
|
||||||
doc = frappe.get_doc(args).insert()
|
return invoices
|
||||||
doc.submit()
|
|
||||||
names.append(doc.name)
|
|
||||||
|
|
||||||
if len(self.invoices) > 5:
|
|
||||||
frappe.publish_realtime(
|
|
||||||
"progress", dict(
|
|
||||||
progress=[row.idx, len(self.invoices)],
|
|
||||||
title=_('Creating {0}').format(doc.doctype)
|
|
||||||
),
|
|
||||||
user=frappe.session.user
|
|
||||||
)
|
|
||||||
|
|
||||||
return names
|
|
||||||
|
|
||||||
def add_party(self, party_type, party):
|
def add_party(self, party_type, party):
|
||||||
party_doc = frappe.new_doc(party_type)
|
party_doc = frappe.new_doc(party_type)
|
||||||
@@ -140,14 +124,12 @@ class OpeningInvoiceCreationTool(Document):
|
|||||||
|
|
||||||
def get_invoice_dict(self, row=None):
|
def get_invoice_dict(self, row=None):
|
||||||
def get_item_dict():
|
def get_item_dict():
|
||||||
default_uom = frappe.db.get_single_value("Stock Settings", "stock_uom") or _("Nos")
|
cost_center = row.get('cost_center') or frappe.get_cached_value('Company', self.company, "cost_center")
|
||||||
cost_center = row.get('cost_center') or frappe.get_cached_value('Company',
|
|
||||||
self.company, "cost_center")
|
|
||||||
|
|
||||||
if not cost_center:
|
if not cost_center:
|
||||||
frappe.throw(
|
frappe.throw(_("Please set the Default Cost Center in {0} company.").format(frappe.bold(self.company)))
|
||||||
_("Please set the Default Cost Center in {0} company.").format(frappe.bold(self.company))
|
|
||||||
)
|
income_expense_account_field = "income_account" if row.party_type == "Customer" else "expense_account"
|
||||||
|
default_uom = frappe.db.get_single_value("Stock Settings", "stock_uom") or _("Nos")
|
||||||
rate = flt(row.outstanding_amount) / flt(row.qty)
|
rate = flt(row.outstanding_amount) / flt(row.qty)
|
||||||
|
|
||||||
return frappe._dict({
|
return frappe._dict({
|
||||||
@@ -161,18 +143,9 @@ class OpeningInvoiceCreationTool(Document):
|
|||||||
"cost_center": cost_center
|
"cost_center": cost_center
|
||||||
})
|
})
|
||||||
|
|
||||||
if not row:
|
|
||||||
return None
|
|
||||||
|
|
||||||
party_type = "Customer"
|
|
||||||
income_expense_account_field = "income_account"
|
|
||||||
if self.invoice_type == "Purchase":
|
|
||||||
party_type = "Supplier"
|
|
||||||
income_expense_account_field = "expense_account"
|
|
||||||
|
|
||||||
item = get_item_dict()
|
item = get_item_dict()
|
||||||
|
|
||||||
args = frappe._dict({
|
invoice = frappe._dict({
|
||||||
"items": [item],
|
"items": [item],
|
||||||
"is_opening": "Yes",
|
"is_opening": "Yes",
|
||||||
"set_posting_time": 1,
|
"set_posting_time": 1,
|
||||||
@@ -180,21 +153,76 @@ class OpeningInvoiceCreationTool(Document):
|
|||||||
"cost_center": self.cost_center,
|
"cost_center": self.cost_center,
|
||||||
"due_date": row.due_date,
|
"due_date": row.due_date,
|
||||||
"posting_date": row.posting_date,
|
"posting_date": row.posting_date,
|
||||||
frappe.scrub(party_type): row.party,
|
frappe.scrub(row.party_type): row.party,
|
||||||
|
"is_pos": 0,
|
||||||
"doctype": "Sales Invoice" if self.invoice_type == "Sales" else "Purchase Invoice"
|
"doctype": "Sales Invoice" if self.invoice_type == "Sales" else "Purchase Invoice"
|
||||||
})
|
})
|
||||||
|
|
||||||
accounting_dimension = get_accounting_dimensions()
|
accounting_dimension = get_accounting_dimensions()
|
||||||
|
|
||||||
for dimension in accounting_dimension:
|
for dimension in accounting_dimension:
|
||||||
args.update({
|
invoice.update({
|
||||||
dimension: item.get(dimension)
|
dimension: item.get(dimension)
|
||||||
})
|
})
|
||||||
|
|
||||||
if self.invoice_type == "Sales":
|
return invoice
|
||||||
args["is_pos"] = 0
|
|
||||||
|
|
||||||
return args
|
def make_invoices(self):
|
||||||
|
self.validate_company()
|
||||||
|
invoices = self.get_invoices()
|
||||||
|
if len(invoices) < 50:
|
||||||
|
return start_import(invoices)
|
||||||
|
else:
|
||||||
|
from frappe.core.page.background_jobs.background_jobs import get_info
|
||||||
|
from frappe.utils.scheduler import is_scheduler_inactive
|
||||||
|
|
||||||
|
if is_scheduler_inactive() and not frappe.flags.in_test:
|
||||||
|
frappe.throw(_("Scheduler is inactive. Cannot import data."), title=_("Scheduler Inactive"))
|
||||||
|
|
||||||
|
enqueued_jobs = [d.get("job_name") for d in get_info()]
|
||||||
|
if self.name not in enqueued_jobs:
|
||||||
|
enqueue(
|
||||||
|
start_import,
|
||||||
|
queue="default",
|
||||||
|
timeout=6000,
|
||||||
|
event="opening_invoice_creation",
|
||||||
|
job_name=self.name,
|
||||||
|
invoices=invoices,
|
||||||
|
now=frappe.conf.developer_mode or frappe.flags.in_test
|
||||||
|
)
|
||||||
|
|
||||||
|
def start_import(invoices):
|
||||||
|
errors = 0
|
||||||
|
names = []
|
||||||
|
for idx, d in enumerate(invoices):
|
||||||
|
try:
|
||||||
|
publish(idx, len(invoices), d.doctype)
|
||||||
|
doc = frappe.get_doc(d)
|
||||||
|
doc.insert()
|
||||||
|
doc.submit()
|
||||||
|
frappe.db.commit()
|
||||||
|
names.append(doc.name)
|
||||||
|
except Exception:
|
||||||
|
errors += 1
|
||||||
|
frappe.db.rollback()
|
||||||
|
message = "\n".join(["Data:", dumps(d, default=str, indent=4), "--" * 50, "\nException:", traceback.format_exc()])
|
||||||
|
frappe.log_error(title="Error while creating Opening Invoice", message=message)
|
||||||
|
frappe.db.commit()
|
||||||
|
if errors:
|
||||||
|
frappe.msgprint(_("You had {} errors while creating opening invoices. Check {} for more details")
|
||||||
|
.format(errors, "<a href='#List/Error Log' class='variant-click'>Error Log</a>"), indicator="red", title=_("Error Occured"))
|
||||||
|
return names
|
||||||
|
|
||||||
|
def publish(index, total, doctype):
|
||||||
|
if total < 5: return
|
||||||
|
frappe.publish_realtime(
|
||||||
|
"opening_invoice_creation_progress",
|
||||||
|
dict(
|
||||||
|
title=_("Opening Invoice Creation In Progress"),
|
||||||
|
message=_('Creating {} out of {} {}').format(index + 1, total, doctype),
|
||||||
|
user=frappe.session.user,
|
||||||
|
count=index+1,
|
||||||
|
total=total
|
||||||
|
))
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_temporary_opening_account(company=None):
|
def get_temporary_opening_account(company=None):
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ class TestOpeningInvoiceCreationTool(unittest.TestCase):
|
|||||||
0: ["_Test Supplier", 300, "Overdue"],
|
0: ["_Test Supplier", 300, "Overdue"],
|
||||||
1: ["_Test Supplier 1", 250, "Overdue"],
|
1: ["_Test Supplier 1", 250, "Overdue"],
|
||||||
}
|
}
|
||||||
self.check_expected_values(invoices, expected_value, invoice_type="Purchase", )
|
self.check_expected_values(invoices, expected_value, "Purchase")
|
||||||
|
|
||||||
def get_opening_invoice_creation_dict(**args):
|
def get_opening_invoice_creation_dict(**args):
|
||||||
party = "Customer" if args.get("invoice_type", "Sales") == "Sales" else "Supplier"
|
party = "Customer" if args.get("invoice_type", "Sales") == "Sales" else "Supplier"
|
||||||
|
|||||||
@@ -1,313 +1,98 @@
|
|||||||
{
|
{
|
||||||
"allow_copy": 0,
|
"actions": [],
|
||||||
"allow_guest_to_view": 0,
|
|
||||||
"allow_import": 0,
|
|
||||||
"allow_rename": 0,
|
|
||||||
"beta": 0,
|
|
||||||
"creation": "2015-12-23 21:31:52.699821",
|
"creation": "2015-12-23 21:31:52.699821",
|
||||||
"custom": 0,
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
"document_type": "",
|
|
||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
|
"field_order": [
|
||||||
|
"payment_gateway",
|
||||||
|
"payment_channel",
|
||||||
|
"is_default",
|
||||||
|
"column_break_4",
|
||||||
|
"payment_account",
|
||||||
|
"currency",
|
||||||
|
"payment_request_message",
|
||||||
|
"message",
|
||||||
|
"message_examples"
|
||||||
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "payment_gateway",
|
"fieldname": "payment_gateway",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Payment Gateway",
|
"label": "Payment Gateway",
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"options": "Payment Gateway",
|
"options": "Payment Gateway",
|
||||||
"permlevel": 0,
|
"reqd": 1
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 1,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
"default": "0",
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "is_default",
|
"fieldname": "is_default",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"hidden": 0,
|
"label": "Is Default"
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Is Default",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "column_break_4",
|
"fieldname": "column_break_4",
|
||||||
"fieldtype": "Column Break",
|
"fieldtype": "Column Break"
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "payment_account",
|
"fieldname": "payment_account",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Payment Account",
|
"label": "Payment Account",
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"options": "Account",
|
"options": "Account",
|
||||||
"permlevel": 0,
|
"reqd": 1
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 1,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fetch_from": "payment_account.account_currency",
|
"fetch_from": "payment_account.account_currency",
|
||||||
"fieldname": "currency",
|
"fieldname": "currency",
|
||||||
"fieldtype": "Read Only",
|
"fieldtype": "Read Only",
|
||||||
"hidden": 0,
|
"label": "Currency"
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Currency",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"options": "",
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
"depends_on": "eval: doc.payment_channel !== \"Phone\"",
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "payment_request_message",
|
"fieldname": "payment_request_message",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break"
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"default": "Please click on the link below to make your payment",
|
"default": "Please click on the link below to make your payment",
|
||||||
"fieldname": "message",
|
"fieldname": "message",
|
||||||
"fieldtype": "Small Text",
|
"fieldtype": "Small Text",
|
||||||
"hidden": 0,
|
"label": "Default Payment Request Message"
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Default Payment Request Message",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "message_examples",
|
"fieldname": "message_examples",
|
||||||
"fieldtype": "HTML",
|
"fieldtype": "HTML",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Message Examples",
|
"label": "Message Examples",
|
||||||
"length": 0,
|
"options": "<pre><h5>Message Example</h5>\n\n<p> Thank You for being a part of {{ doc.company }}! We hope you are enjoying the service.</p>\n\n<p> Please find enclosed the E Bill statement. The outstanding amount is {{ doc.grand_total }}.</p>\n\n<p> We don't want you to be spending time running around in order to pay for your Bill.<br>After all, life is beautiful and the time you have in hand should be spent to enjoy it!<br>So here are our little ways to help you get more time for life! </p>\n\n<a href=\"{{ payment_url }}\"> click here to pay </a>\n\n</pre>\n"
|
||||||
"no_copy": 0,
|
},
|
||||||
"options": "<pre><h5>Message Example</h5>\n\n<p> Thank You for being a part of {{ doc.company }}! We hope you are enjoying the service.</p>\n\n<p> Please find enclosed the E Bill statement. The outstanding amount is {{ doc.grand_total }}.</p>\n\n<p> We don't want you to be spending time running around in order to pay for your Bill.<br>After all, life is beautiful and the time you have in hand should be spent to enjoy it!<br>So here are our little ways to help you get more time for life! </p>\n\n<a href=\"{{ payment_url }}\"> click here to pay </a>\n\n</pre>\n",
|
{
|
||||||
"permlevel": 0,
|
"default": "Email",
|
||||||
"precision": "",
|
"fieldname": "payment_channel",
|
||||||
"print_hide": 0,
|
"fieldtype": "Select",
|
||||||
"print_hide_if_no_value": 0,
|
"label": "Payment Channel",
|
||||||
"read_only": 0,
|
"options": "\nEmail\nPhone"
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"has_web_view": 0,
|
"index_web_pages_for_search": 1,
|
||||||
"hide_heading": 0,
|
"links": [],
|
||||||
"hide_toolbar": 0,
|
"modified": "2020-09-20 13:30:27.722852",
|
||||||
"idx": 0,
|
|
||||||
"image_view": 0,
|
|
||||||
"in_create": 0,
|
|
||||||
"is_submittable": 0,
|
|
||||||
"issingle": 0,
|
|
||||||
"istable": 0,
|
|
||||||
"max_attachments": 0,
|
|
||||||
"modified": "2018-05-16 22:43:34.970491",
|
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Payment Gateway Account",
|
"name": "Payment Gateway Account",
|
||||||
"name_case": "",
|
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
{
|
{
|
||||||
"amend": 0,
|
|
||||||
"cancel": 0,
|
|
||||||
"create": 1,
|
"create": 1,
|
||||||
"delete": 1,
|
"delete": 1,
|
||||||
"email": 1,
|
"email": 1,
|
||||||
"export": 1,
|
"export": 1,
|
||||||
"if_owner": 0,
|
|
||||||
"import": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"print": 1,
|
"print": 1,
|
||||||
"read": 1,
|
"read": 1,
|
||||||
"report": 1,
|
"report": 1,
|
||||||
"role": "Accounts Manager",
|
"role": "Accounts Manager",
|
||||||
"set_user_permissions": 0,
|
|
||||||
"share": 1,
|
"share": 1,
|
||||||
"submit": 0,
|
|
||||||
"write": 1
|
"write": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"quick_entry": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"read_only_onload": 0,
|
|
||||||
"show_name_in_global_search": 0,
|
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC"
|
||||||
"track_changes": 0,
|
|
||||||
"track_seen": 0
|
|
||||||
}
|
}
|
||||||
@@ -37,6 +37,11 @@ frappe.ui.form.on("Payment Reconciliation Payment", {
|
|||||||
erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.extend({
|
erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.extend({
|
||||||
onload: function() {
|
onload: function() {
|
||||||
var me = this;
|
var me = this;
|
||||||
|
|
||||||
|
this.frm.set_query("party", function() {
|
||||||
|
check_mandatory(me.frm);
|
||||||
|
});
|
||||||
|
|
||||||
this.frm.set_query("party_type", function() {
|
this.frm.set_query("party_type", function() {
|
||||||
return {
|
return {
|
||||||
"filters": {
|
"filters": {
|
||||||
@@ -46,37 +51,39 @@ erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.ext
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.frm.set_query('receivable_payable_account', function() {
|
this.frm.set_query('receivable_payable_account', function() {
|
||||||
if(!me.frm.doc.company || !me.frm.doc.party_type) {
|
check_mandatory(me.frm);
|
||||||
frappe.msgprint(__("Please select Company and Party Type first"));
|
return {
|
||||||
} else {
|
filters: {
|
||||||
return{
|
"company": me.frm.doc.company,
|
||||||
filters: {
|
"is_group": 0,
|
||||||
"company": me.frm.doc.company,
|
"account_type": frappe.boot.party_account_types[me.frm.doc.party_type]
|
||||||
"is_group": 0,
|
}
|
||||||
"account_type": frappe.boot.party_account_types[me.frm.doc.party_type]
|
};
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.frm.set_query('bank_cash_account', function() {
|
this.frm.set_query('bank_cash_account', function() {
|
||||||
if(!me.frm.doc.company) {
|
check_mandatory(me.frm, true);
|
||||||
frappe.msgprint(__("Please select Company first"));
|
return {
|
||||||
} else {
|
filters:[
|
||||||
return{
|
['Account', 'company', '=', me.frm.doc.company],
|
||||||
filters:[
|
['Account', 'is_group', '=', 0],
|
||||||
['Account', 'company', '=', me.frm.doc.company],
|
['Account', 'account_type', 'in', ['Bank', 'Cash']]
|
||||||
['Account', 'is_group', '=', 0],
|
]
|
||||||
['Account', 'account_type', 'in', ['Bank', 'Cash']]
|
};
|
||||||
]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.frm.set_value('party_type', '');
|
this.frm.set_value('party_type', '');
|
||||||
this.frm.set_value('party', '');
|
this.frm.set_value('party', '');
|
||||||
this.frm.set_value('receivable_payable_account', '');
|
this.frm.set_value('receivable_payable_account', '');
|
||||||
|
|
||||||
|
var check_mandatory = (frm, only_company=false) => {
|
||||||
|
var title = __("Mandatory");
|
||||||
|
if (only_company && !frm.doc.company) {
|
||||||
|
frappe.throw({message: __("Please Select a Company First"), title: title});
|
||||||
|
} else if (!frm.doc.company || !frm.doc.party_type) {
|
||||||
|
frappe.throw({message: __("Please Select Both Company and Party Type First"), title: title});
|
||||||
|
}
|
||||||
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
refresh: function() {
|
refresh: function() {
|
||||||
@@ -90,7 +97,7 @@ erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.ext
|
|||||||
|
|
||||||
party: function() {
|
party: function() {
|
||||||
var me = this
|
var me = this
|
||||||
if(!me.frm.doc.receivable_payable_account && me.frm.doc.party_type && me.frm.doc.party) {
|
if (!me.frm.doc.receivable_payable_account && me.frm.doc.party_type && me.frm.doc.party) {
|
||||||
return frappe.call({
|
return frappe.call({
|
||||||
method: "erpnext.accounts.party.get_party_account",
|
method: "erpnext.accounts.party.get_party_account",
|
||||||
args: {
|
args: {
|
||||||
@@ -99,7 +106,7 @@ erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.ext
|
|||||||
party: me.frm.doc.party
|
party: me.frm.doc.party
|
||||||
},
|
},
|
||||||
callback: function(r) {
|
callback: function(r) {
|
||||||
if(!r.exc && r.message) {
|
if (!r.exc && r.message) {
|
||||||
me.frm.set_value("receivable_payable_account", r.message);
|
me.frm.set_value("receivable_payable_account", r.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ class PaymentReconciliation(Document):
|
|||||||
and `tabGL Entry`.against_voucher_type = %(voucher_type)s
|
and `tabGL Entry`.against_voucher_type = %(voucher_type)s
|
||||||
and `tab{doc}`.docstatus = 1 and `tabGL Entry`.party = %(party)s
|
and `tab{doc}`.docstatus = 1 and `tabGL Entry`.party = %(party)s
|
||||||
and `tabGL Entry`.party_type = %(party_type)s and `tabGL Entry`.account = %(account)s
|
and `tabGL Entry`.party_type = %(party_type)s and `tabGL Entry`.account = %(account)s
|
||||||
|
and `tabGL Entry`.is_cancelled = 0
|
||||||
GROUP BY `tab{doc}`.name
|
GROUP BY `tab{doc}`.name
|
||||||
Having
|
Having
|
||||||
amount > 0
|
amount > 0
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ frappe.ui.form.on("Payment Request", "onload", function(frm, dt, dn){
|
|||||||
})
|
})
|
||||||
|
|
||||||
frappe.ui.form.on("Payment Request", "refresh", function(frm) {
|
frappe.ui.form.on("Payment Request", "refresh", function(frm) {
|
||||||
if(frm.doc.payment_request_type == 'Inward' &&
|
if(frm.doc.payment_request_type == 'Inward' && frm.doc.payment_channel !== "Phone" &&
|
||||||
!in_list(["Initiated", "Paid"], frm.doc.status) && !frm.doc.__islocal && frm.doc.docstatus==1){
|
!in_list(["Initiated", "Paid"], frm.doc.status) && !frm.doc.__islocal && frm.doc.docstatus==1){
|
||||||
frm.add_custom_button(__('Resend Payment Email'), function(){
|
frm.add_custom_button(__('Resend Payment Email'), function(){
|
||||||
frappe.call({
|
frappe.call({
|
||||||
|
|||||||
@@ -48,6 +48,7 @@
|
|||||||
"section_break_7",
|
"section_break_7",
|
||||||
"payment_gateway",
|
"payment_gateway",
|
||||||
"payment_account",
|
"payment_account",
|
||||||
|
"payment_channel",
|
||||||
"payment_order",
|
"payment_order",
|
||||||
"amended_from"
|
"amended_from"
|
||||||
],
|
],
|
||||||
@@ -230,6 +231,7 @@
|
|||||||
"label": "Recipient Message And Payment Details"
|
"label": "Recipient Message And Payment Details"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"depends_on": "eval: doc.payment_channel != \"Phone\"",
|
||||||
"fieldname": "print_format",
|
"fieldname": "print_format",
|
||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"label": "Print Format"
|
"label": "Print Format"
|
||||||
@@ -241,6 +243,7 @@
|
|||||||
"label": "To"
|
"label": "To"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"depends_on": "eval: doc.payment_channel != \"Phone\"",
|
||||||
"fieldname": "subject",
|
"fieldname": "subject",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"in_global_search": 1,
|
"in_global_search": 1,
|
||||||
@@ -277,16 +280,18 @@
|
|||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "eval: doc.payment_request_type == 'Inward'",
|
"depends_on": "eval: doc.payment_request_type == 'Inward' || doc.payment_channel != \"Phone\"",
|
||||||
"fieldname": "section_break_10",
|
"fieldname": "section_break_10",
|
||||||
"fieldtype": "Section Break"
|
"fieldtype": "Section Break"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"depends_on": "eval: doc.payment_channel != \"Phone\"",
|
||||||
"fieldname": "message",
|
"fieldname": "message",
|
||||||
"fieldtype": "Text",
|
"fieldtype": "Text",
|
||||||
"label": "Message"
|
"label": "Message"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"depends_on": "eval: doc.payment_channel != \"Phone\"",
|
||||||
"fieldname": "message_examples",
|
"fieldname": "message_examples",
|
||||||
"fieldtype": "HTML",
|
"fieldtype": "HTML",
|
||||||
"label": "Message Examples",
|
"label": "Message Examples",
|
||||||
@@ -347,12 +352,21 @@
|
|||||||
"options": "Payment Request",
|
"options": "Payment Request",
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fetch_from": "payment_gateway_account.payment_channel",
|
||||||
|
"fieldname": "payment_channel",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"label": "Payment Channel",
|
||||||
|
"options": "\nEmail\nPhone",
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"in_create": 1,
|
"in_create": 1,
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-07-17 14:06:42.185763",
|
"modified": "2020-09-18 12:24:14.178853",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Payment Request",
|
"name": "Payment Request",
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ class PaymentRequest(Document):
|
|||||||
ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name)
|
ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name)
|
||||||
if (hasattr(ref_doc, "order_type") \
|
if (hasattr(ref_doc, "order_type") \
|
||||||
and getattr(ref_doc, "order_type") != "Shopping Cart"):
|
and getattr(ref_doc, "order_type") != "Shopping Cart"):
|
||||||
ref_amount = get_amount(ref_doc)
|
ref_amount = get_amount(ref_doc, self.payment_account)
|
||||||
|
|
||||||
if existing_payment_request_amount + flt(self.grand_total)> ref_amount:
|
if existing_payment_request_amount + flt(self.grand_total)> ref_amount:
|
||||||
frappe.throw(_("Total Payment Request amount cannot be greater than {0} amount")
|
frappe.throw(_("Total Payment Request amount cannot be greater than {0} amount")
|
||||||
@@ -76,11 +76,25 @@ class PaymentRequest(Document):
|
|||||||
or self.flags.mute_email:
|
or self.flags.mute_email:
|
||||||
send_mail = False
|
send_mail = False
|
||||||
|
|
||||||
if send_mail:
|
if send_mail and self.payment_channel != "Phone":
|
||||||
self.set_payment_request_url()
|
self.set_payment_request_url()
|
||||||
self.send_email()
|
self.send_email()
|
||||||
self.make_communication_entry()
|
self.make_communication_entry()
|
||||||
|
|
||||||
|
elif self.payment_channel == "Phone":
|
||||||
|
controller = get_payment_gateway_controller(self.payment_gateway)
|
||||||
|
payment_record = dict(
|
||||||
|
reference_doctype="Payment Request",
|
||||||
|
reference_docname=self.name,
|
||||||
|
payment_reference=self.reference_name,
|
||||||
|
grand_total=self.grand_total,
|
||||||
|
sender=self.email_to,
|
||||||
|
currency=self.currency,
|
||||||
|
payment_gateway=self.payment_gateway
|
||||||
|
)
|
||||||
|
controller.validate_transaction_currency(self.currency)
|
||||||
|
controller.request_for_payment(**payment_record)
|
||||||
|
|
||||||
def on_cancel(self):
|
def on_cancel(self):
|
||||||
self.check_if_payment_entry_exists()
|
self.check_if_payment_entry_exists()
|
||||||
self.set_as_cancelled()
|
self.set_as_cancelled()
|
||||||
@@ -105,13 +119,14 @@ class PaymentRequest(Document):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def set_payment_request_url(self):
|
def set_payment_request_url(self):
|
||||||
if self.payment_account:
|
if self.payment_account and self.payment_channel != "Phone":
|
||||||
self.payment_url = self.get_payment_url()
|
self.payment_url = self.get_payment_url()
|
||||||
|
|
||||||
if self.payment_url:
|
if self.payment_url:
|
||||||
self.db_set('payment_url', self.payment_url)
|
self.db_set('payment_url', self.payment_url)
|
||||||
|
|
||||||
if self.payment_url or not self.payment_gateway_account:
|
if self.payment_url or not self.payment_gateway_account \
|
||||||
|
or (self.payment_gateway_account and self.payment_channel == "Phone"):
|
||||||
self.db_set('status', 'Initiated')
|
self.db_set('status', 'Initiated')
|
||||||
|
|
||||||
def get_payment_url(self):
|
def get_payment_url(self):
|
||||||
@@ -140,10 +155,14 @@ class PaymentRequest(Document):
|
|||||||
})
|
})
|
||||||
|
|
||||||
def set_as_paid(self):
|
def set_as_paid(self):
|
||||||
payment_entry = self.create_payment_entry()
|
if self.payment_channel == "Phone":
|
||||||
self.make_invoice()
|
self.db_set("status", "Paid")
|
||||||
|
|
||||||
return payment_entry
|
else:
|
||||||
|
payment_entry = self.create_payment_entry()
|
||||||
|
self.make_invoice()
|
||||||
|
|
||||||
|
return payment_entry
|
||||||
|
|
||||||
def create_payment_entry(self, submit=True):
|
def create_payment_entry(self, submit=True):
|
||||||
"""create entry"""
|
"""create entry"""
|
||||||
@@ -151,7 +170,7 @@ class PaymentRequest(Document):
|
|||||||
|
|
||||||
ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name)
|
ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name)
|
||||||
|
|
||||||
if self.reference_doctype == "Sales Invoice":
|
if self.reference_doctype in ["Sales Invoice", "POS Invoice"]:
|
||||||
party_account = ref_doc.debit_to
|
party_account = ref_doc.debit_to
|
||||||
elif self.reference_doctype == "Purchase Invoice":
|
elif self.reference_doctype == "Purchase Invoice":
|
||||||
party_account = ref_doc.credit_to
|
party_account = ref_doc.credit_to
|
||||||
@@ -166,8 +185,8 @@ class PaymentRequest(Document):
|
|||||||
else:
|
else:
|
||||||
party_amount = self.grand_total
|
party_amount = self.grand_total
|
||||||
|
|
||||||
payment_entry = get_payment_entry(self.reference_doctype, self.reference_name,
|
payment_entry = get_payment_entry(self.reference_doctype, self.reference_name, party_amount=party_amount,
|
||||||
party_amount=party_amount, bank_account=self.payment_account, bank_amount=bank_amount)
|
bank_account=self.payment_account, bank_amount=bank_amount)
|
||||||
|
|
||||||
payment_entry.update({
|
payment_entry.update({
|
||||||
"reference_no": self.name,
|
"reference_no": self.name,
|
||||||
@@ -255,7 +274,7 @@ class PaymentRequest(Document):
|
|||||||
|
|
||||||
# if shopping cart enabled and in session
|
# if shopping cart enabled and in session
|
||||||
if (shopping_cart_settings.enabled and hasattr(frappe.local, "session")
|
if (shopping_cart_settings.enabled and hasattr(frappe.local, "session")
|
||||||
and frappe.local.session.user != "Guest"):
|
and frappe.local.session.user != "Guest") and self.payment_channel != "Phone":
|
||||||
|
|
||||||
success_url = shopping_cart_settings.payment_success_url
|
success_url = shopping_cart_settings.payment_success_url
|
||||||
if success_url:
|
if success_url:
|
||||||
@@ -280,7 +299,9 @@ def make_payment_request(**args):
|
|||||||
args = frappe._dict(args)
|
args = frappe._dict(args)
|
||||||
|
|
||||||
ref_doc = frappe.get_doc(args.dt, args.dn)
|
ref_doc = frappe.get_doc(args.dt, args.dn)
|
||||||
grand_total = get_amount(ref_doc)
|
gateway_account = get_gateway_details(args) or frappe._dict()
|
||||||
|
|
||||||
|
grand_total = get_amount(ref_doc, gateway_account.get("payment_account"))
|
||||||
if args.loyalty_points and args.dt == "Sales Order":
|
if args.loyalty_points and args.dt == "Sales Order":
|
||||||
from erpnext.accounts.doctype.loyalty_program.loyalty_program import validate_loyalty_points
|
from erpnext.accounts.doctype.loyalty_program.loyalty_program import validate_loyalty_points
|
||||||
loyalty_amount = validate_loyalty_points(ref_doc, int(args.loyalty_points))
|
loyalty_amount = validate_loyalty_points(ref_doc, int(args.loyalty_points))
|
||||||
@@ -288,8 +309,6 @@ def make_payment_request(**args):
|
|||||||
frappe.db.set_value("Sales Order", args.dn, "loyalty_amount", loyalty_amount, update_modified=False)
|
frappe.db.set_value("Sales Order", args.dn, "loyalty_amount", loyalty_amount, update_modified=False)
|
||||||
grand_total = grand_total - loyalty_amount
|
grand_total = grand_total - loyalty_amount
|
||||||
|
|
||||||
gateway_account = get_gateway_details(args) or frappe._dict()
|
|
||||||
|
|
||||||
bank_account = (get_party_bank_account(args.get('party_type'), args.get('party'))
|
bank_account = (get_party_bank_account(args.get('party_type'), args.get('party'))
|
||||||
if args.get('party_type') else '')
|
if args.get('party_type') else '')
|
||||||
|
|
||||||
@@ -314,9 +333,11 @@ def make_payment_request(**args):
|
|||||||
"payment_gateway_account": gateway_account.get("name"),
|
"payment_gateway_account": gateway_account.get("name"),
|
||||||
"payment_gateway": gateway_account.get("payment_gateway"),
|
"payment_gateway": gateway_account.get("payment_gateway"),
|
||||||
"payment_account": gateway_account.get("payment_account"),
|
"payment_account": gateway_account.get("payment_account"),
|
||||||
|
"payment_channel": gateway_account.get("payment_channel"),
|
||||||
"payment_request_type": args.get("payment_request_type"),
|
"payment_request_type": args.get("payment_request_type"),
|
||||||
"currency": ref_doc.currency,
|
"currency": ref_doc.currency,
|
||||||
"grand_total": grand_total,
|
"grand_total": grand_total,
|
||||||
|
"mode_of_payment": args.mode_of_payment,
|
||||||
"email_to": args.recipient_id or ref_doc.owner,
|
"email_to": args.recipient_id or ref_doc.owner,
|
||||||
"subject": _("Payment Request for {0}").format(args.dn),
|
"subject": _("Payment Request for {0}").format(args.dn),
|
||||||
"message": gateway_account.get("message") or get_dummy_message(ref_doc),
|
"message": gateway_account.get("message") or get_dummy_message(ref_doc),
|
||||||
@@ -344,7 +365,7 @@ def make_payment_request(**args):
|
|||||||
|
|
||||||
return pr.as_dict()
|
return pr.as_dict()
|
||||||
|
|
||||||
def get_amount(ref_doc):
|
def get_amount(ref_doc, payment_account=None):
|
||||||
"""get amount based on doctype"""
|
"""get amount based on doctype"""
|
||||||
dt = ref_doc.doctype
|
dt = ref_doc.doctype
|
||||||
if dt in ["Sales Order", "Purchase Order"]:
|
if dt in ["Sales Order", "Purchase Order"]:
|
||||||
@@ -356,6 +377,12 @@ def get_amount(ref_doc):
|
|||||||
else:
|
else:
|
||||||
grand_total = flt(ref_doc.outstanding_amount) / ref_doc.conversion_rate
|
grand_total = flt(ref_doc.outstanding_amount) / ref_doc.conversion_rate
|
||||||
|
|
||||||
|
elif dt == "POS Invoice":
|
||||||
|
for pay in ref_doc.payments:
|
||||||
|
if pay.type == "Phone" and pay.account == payment_account:
|
||||||
|
grand_total = pay.amount
|
||||||
|
break
|
||||||
|
|
||||||
elif dt == "Fees":
|
elif dt == "Fees":
|
||||||
grand_total = ref_doc.outstanding_amount
|
grand_total = ref_doc.outstanding_amount
|
||||||
|
|
||||||
@@ -366,6 +393,10 @@ def get_amount(ref_doc):
|
|||||||
frappe.throw(_("Payment Entry is already created"))
|
frappe.throw(_("Payment Entry is already created"))
|
||||||
|
|
||||||
def get_existing_payment_request_amount(ref_dt, ref_dn):
|
def get_existing_payment_request_amount(ref_dt, ref_dn):
|
||||||
|
"""
|
||||||
|
Get the existing payment request which are unpaid or partially paid for payment channel other than Phone
|
||||||
|
and get the summation of existing paid payment request for Phone payment channel.
|
||||||
|
"""
|
||||||
existing_payment_request_amount = frappe.db.sql("""
|
existing_payment_request_amount = frappe.db.sql("""
|
||||||
select sum(grand_total)
|
select sum(grand_total)
|
||||||
from `tabPayment Request`
|
from `tabPayment Request`
|
||||||
@@ -373,7 +404,9 @@ def get_existing_payment_request_amount(ref_dt, ref_dn):
|
|||||||
reference_doctype = %s
|
reference_doctype = %s
|
||||||
and reference_name = %s
|
and reference_name = %s
|
||||||
and docstatus = 1
|
and docstatus = 1
|
||||||
and status != 'Paid'
|
and (status != 'Paid'
|
||||||
|
or (payment_channel = 'Phone'
|
||||||
|
and status = 'Paid'))
|
||||||
""", (ref_dt, ref_dn))
|
""", (ref_dt, ref_dn))
|
||||||
return flt(existing_payment_request_amount[0][0]) if existing_payment_request_amount else 0
|
return flt(existing_payment_request_amount[0][0]) if existing_payment_request_amount else 0
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,7 @@
|
|||||||
"unique": 0
|
"unique": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"description": "Provide the invoice portion in percent",
|
||||||
"allow_bulk_edit": 0,
|
"allow_bulk_edit": 0,
|
||||||
"allow_on_submit": 0,
|
"allow_on_submit": 0,
|
||||||
"bold": 1,
|
"bold": 1,
|
||||||
@@ -170,6 +171,7 @@
|
|||||||
"unique": 0
|
"unique": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"description": "Give number of days according to prior selection",
|
||||||
"allow_bulk_edit": 0,
|
"allow_bulk_edit": 0,
|
||||||
"allow_on_submit": 0,
|
"allow_on_submit": 0,
|
||||||
"bold": 1,
|
"bold": 1,
|
||||||
@@ -305,7 +307,7 @@
|
|||||||
"issingle": 0,
|
"issingle": 0,
|
||||||
"istable": 0,
|
"istable": 0,
|
||||||
"max_attachments": 0,
|
"max_attachments": 0,
|
||||||
"modified": "2018-03-08 10:47:32.830478",
|
"modified": "2020-10-14 10:47:32.830478",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Payment Term",
|
"name": "Payment Term",
|
||||||
|
|||||||
@@ -201,5 +201,22 @@ frappe.ui.form.on('POS Invoice', {
|
|||||||
}
|
}
|
||||||
frm.set_value("loyalty_amount", loyalty_amount);
|
frm.set_value("loyalty_amount", loyalty_amount);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
request_for_payment: function (frm) {
|
||||||
|
frm.save().then(() => {
|
||||||
|
frappe.dom.freeze();
|
||||||
|
frappe.call({
|
||||||
|
method: 'create_payment_request',
|
||||||
|
doc: frm.doc,
|
||||||
|
})
|
||||||
|
.fail(() => {
|
||||||
|
frappe.dom.unfreeze();
|
||||||
|
frappe.msgprint('Payment request failed');
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
frappe.msgprint('Payment request sent successfully');
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -279,8 +279,7 @@
|
|||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Is Return (Credit Note)",
|
"label": "Is Return (Credit Note)",
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"print_hide": 1,
|
"print_hide": 1
|
||||||
"set_only_once": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "column_break1",
|
"fieldname": "column_break1",
|
||||||
@@ -461,7 +460,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "contact_mobile",
|
"fieldname": "contact_mobile",
|
||||||
"fieldtype": "Small Text",
|
"fieldtype": "Data",
|
||||||
"hidden": 1,
|
"hidden": 1,
|
||||||
"label": "Mobile No",
|
"label": "Mobile No",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
@@ -1579,10 +1578,9 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "fa fa-file-text",
|
"icon": "fa fa-file-text",
|
||||||
"index_web_pages_for_search": 1,
|
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-09-07 12:43:09.138720",
|
"modified": "2020-09-28 16:51:24.641755",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "POS Invoice",
|
"name": "POS Invoice",
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ from erpnext.accounts.doctype.loyalty_program.loyalty_program import \
|
|||||||
|
|
||||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import SalesInvoice, get_bank_cash_account, update_multi_mode_option
|
from erpnext.accounts.doctype.sales_invoice.sales_invoice import SalesInvoice, get_bank_cash_account, update_multi_mode_option
|
||||||
from erpnext.stock.doctype.serial_no.serial_no import get_pos_reserved_serial_nos
|
from erpnext.stock.doctype.serial_no.serial_no import get_pos_reserved_serial_nos
|
||||||
|
from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request
|
||||||
|
|
||||||
from six import iteritems
|
from six import iteritems
|
||||||
|
|
||||||
@@ -57,6 +58,7 @@ class POSInvoice(SalesInvoice):
|
|||||||
against_psi_doc.make_loyalty_point_entry()
|
against_psi_doc.make_loyalty_point_entry()
|
||||||
if self.redeem_loyalty_points and self.loyalty_points:
|
if self.redeem_loyalty_points and self.loyalty_points:
|
||||||
self.apply_loyalty_points()
|
self.apply_loyalty_points()
|
||||||
|
self.check_phone_payments()
|
||||||
self.set_status(update=True)
|
self.set_status(update=True)
|
||||||
|
|
||||||
def on_cancel(self):
|
def on_cancel(self):
|
||||||
@@ -69,6 +71,18 @@ class POSInvoice(SalesInvoice):
|
|||||||
against_psi_doc.delete_loyalty_point_entry()
|
against_psi_doc.delete_loyalty_point_entry()
|
||||||
against_psi_doc.make_loyalty_point_entry()
|
against_psi_doc.make_loyalty_point_entry()
|
||||||
|
|
||||||
|
def check_phone_payments(self):
|
||||||
|
for pay in self.payments:
|
||||||
|
if pay.type == "Phone" and pay.amount >= 0:
|
||||||
|
paid_amt = frappe.db.get_value("Payment Request",
|
||||||
|
filters=dict(
|
||||||
|
reference_doctype="POS Invoice", reference_name=self.name,
|
||||||
|
mode_of_payment=pay.mode_of_payment, status="Paid"),
|
||||||
|
fieldname="grand_total")
|
||||||
|
|
||||||
|
if pay.amount != paid_amt:
|
||||||
|
return frappe.throw(_("Payment related to {0} is not completed").format(pay.mode_of_payment))
|
||||||
|
|
||||||
def validate_stock_availablility(self):
|
def validate_stock_availablility(self):
|
||||||
allow_negative_stock = frappe.db.get_value('Stock Settings', None, 'allow_negative_stock')
|
allow_negative_stock = frappe.db.get_value('Stock Settings', None, 'allow_negative_stock')
|
||||||
|
|
||||||
@@ -92,21 +106,19 @@ class POSInvoice(SalesInvoice):
|
|||||||
|
|
||||||
if len(invalid_serial_nos):
|
if len(invalid_serial_nos):
|
||||||
multiple_nos = 's' if len(invalid_serial_nos) > 1 else ''
|
multiple_nos = 's' if len(invalid_serial_nos) > 1 else ''
|
||||||
frappe.throw(_("Row #{}: Serial No{}. {} has already been transacted into another POS Invoice. \
|
frappe.throw(_("Row #{}: Serial No{}. {} has already been transacted into another POS Invoice. Please select valid serial no.").format(
|
||||||
Please select valid serial no.".format(d.idx, multiple_nos,
|
d.idx, multiple_nos, frappe.bold(', '.join(invalid_serial_nos))), title=_("Not Available"))
|
||||||
frappe.bold(', '.join(invalid_serial_nos)))), title=_("Not Available"))
|
|
||||||
else:
|
else:
|
||||||
if allow_negative_stock:
|
if allow_negative_stock:
|
||||||
return
|
return
|
||||||
|
|
||||||
available_stock = get_stock_availability(d.item_code, d.warehouse)
|
available_stock = get_stock_availability(d.item_code, d.warehouse)
|
||||||
if not (flt(available_stock) > 0):
|
if not (flt(available_stock) > 0):
|
||||||
frappe.throw(_('Row #{}: Item Code: {} is not available under warehouse {}.'
|
frappe.throw(_('Row #{}: Item Code: {} is not available under warehouse {}.').format(
|
||||||
.format(d.idx, frappe.bold(d.item_code), frappe.bold(d.warehouse))), title=_("Not Available"))
|
d.idx, frappe.bold(d.item_code), frappe.bold(d.warehouse)), title=_("Not Available"))
|
||||||
elif flt(available_stock) < flt(d.qty):
|
elif flt(available_stock) < flt(d.qty):
|
||||||
frappe.msgprint(_('Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}. \
|
frappe.msgprint(_('Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}. Available quantity {}.').format(
|
||||||
Available quantity {}.'.format(d.idx, frappe.bold(d.item_code),
|
d.idx, frappe.bold(d.item_code), frappe.bold(d.warehouse), frappe.bold(d.qty)), title=_("Not Available"))
|
||||||
frappe.bold(d.warehouse), frappe.bold(d.qty))), title=_("Not Available"))
|
|
||||||
|
|
||||||
def validate_serialised_or_batched_item(self):
|
def validate_serialised_or_batched_item(self):
|
||||||
for d in self.get("items"):
|
for d in self.get("items"):
|
||||||
@@ -117,14 +129,14 @@ class POSInvoice(SalesInvoice):
|
|||||||
|
|
||||||
|
|
||||||
if serialized and batched and (no_batch_selected or no_serial_selected):
|
if serialized and batched and (no_batch_selected or no_serial_selected):
|
||||||
frappe.throw(_('Row #{}: Please select a serial no and batch against item: {} or remove it to complete transaction.'
|
frappe.throw(_('Row #{}: Please select a serial no and batch against item: {} or remove it to complete transaction.').format(
|
||||||
.format(d.idx, frappe.bold(d.item_code))), title=_("Invalid Item"))
|
d.idx, frappe.bold(d.item_code)), title=_("Invalid Item"))
|
||||||
if serialized and no_serial_selected:
|
if serialized and no_serial_selected:
|
||||||
frappe.throw(_('Row #{}: No serial number selected against item: {}. Please select one or remove it to complete transaction.'
|
frappe.throw(_('Row #{}: No serial number selected against item: {}. Please select one or remove it to complete transaction.').format(
|
||||||
.format(d.idx, frappe.bold(d.item_code))), title=_("Invalid Item"))
|
d.idx, frappe.bold(d.item_code)), title=_("Invalid Item"))
|
||||||
if batched and no_batch_selected:
|
if batched and no_batch_selected:
|
||||||
frappe.throw(_('Row #{}: No batch selected against item: {}. Please select a batch or remove it to complete transaction.'
|
frappe.throw(_('Row #{}: No batch selected against item: {}. Please select a batch or remove it to complete transaction.').format(
|
||||||
.format(d.idx, frappe.bold(d.item_code))), title=_("Invalid Item"))
|
d.idx, frappe.bold(d.item_code)), title=_("Invalid Item"))
|
||||||
|
|
||||||
def validate_return_items(self):
|
def validate_return_items(self):
|
||||||
if not self.get("is_return"): return
|
if not self.get("is_return"): return
|
||||||
@@ -151,7 +163,7 @@ class POSInvoice(SalesInvoice):
|
|||||||
self.base_change_amount = flt(self.base_paid_amount - base_grand_total + flt(self.base_write_off_amount))
|
self.base_change_amount = flt(self.base_paid_amount - base_grand_total + flt(self.base_write_off_amount))
|
||||||
|
|
||||||
if flt(self.change_amount) and not self.account_for_change_amount:
|
if flt(self.change_amount) and not self.account_for_change_amount:
|
||||||
msgprint(_("Please enter Account for Change Amount"), raise_exception=1)
|
frappe.msgprint(_("Please enter Account for Change Amount"), raise_exception=1)
|
||||||
|
|
||||||
def verify_payment_amount(self):
|
def verify_payment_amount(self):
|
||||||
for entry in self.payments:
|
for entry in self.payments:
|
||||||
@@ -167,7 +179,7 @@ class POSInvoice(SalesInvoice):
|
|||||||
total_amount_in_payments += payment.amount
|
total_amount_in_payments += payment.amount
|
||||||
invoice_total = self.rounded_total or self.grand_total
|
invoice_total = self.rounded_total or self.grand_total
|
||||||
if total_amount_in_payments < invoice_total:
|
if total_amount_in_payments < invoice_total:
|
||||||
frappe.throw(_("Total payments amount can't be greater than {}".format(-invoice_total)))
|
frappe.throw(_("Total payments amount can't be greater than {}").format(-invoice_total))
|
||||||
|
|
||||||
def validate_loyalty_transaction(self):
|
def validate_loyalty_transaction(self):
|
||||||
if self.redeem_loyalty_points and (not self.loyalty_redemption_account or not self.loyalty_redemption_cost_center):
|
if self.redeem_loyalty_points and (not self.loyalty_redemption_account or not self.loyalty_redemption_cost_center):
|
||||||
@@ -314,6 +326,32 @@ class POSInvoice(SalesInvoice):
|
|||||||
if not pay.account:
|
if not pay.account:
|
||||||
pay.account = get_bank_cash_account(pay.mode_of_payment, self.company).get("account")
|
pay.account = get_bank_cash_account(pay.mode_of_payment, self.company).get("account")
|
||||||
|
|
||||||
|
def create_payment_request(self):
|
||||||
|
for pay in self.payments:
|
||||||
|
if pay.type == "Phone":
|
||||||
|
if pay.amount <= 0:
|
||||||
|
frappe.throw(_("Payment amount cannot be less than or equal to 0"))
|
||||||
|
|
||||||
|
if not self.contact_mobile:
|
||||||
|
frappe.throw(_("Please enter the phone number first"))
|
||||||
|
|
||||||
|
payment_gateway = frappe.db.get_value("Payment Gateway Account", {
|
||||||
|
"payment_account": pay.account,
|
||||||
|
})
|
||||||
|
record = {
|
||||||
|
"payment_gateway": payment_gateway,
|
||||||
|
"dt": "POS Invoice",
|
||||||
|
"dn": self.name,
|
||||||
|
"payment_request_type": "Inward",
|
||||||
|
"party_type": "Customer",
|
||||||
|
"party": self.customer,
|
||||||
|
"mode_of_payment": pay.mode_of_payment,
|
||||||
|
"recipient_id": self.contact_mobile,
|
||||||
|
"submit_doc": True
|
||||||
|
}
|
||||||
|
|
||||||
|
return make_payment_request(**record)
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_stock_availability(item_code, warehouse):
|
def get_stock_availability(item_code, warehouse):
|
||||||
latest_sle = frappe.db.sql("""select qty_after_transaction
|
latest_sle = frappe.db.sql("""select qty_after_transaction
|
||||||
|
|||||||
@@ -361,6 +361,7 @@
|
|||||||
"fieldname": "bill_date",
|
"fieldname": "bill_date",
|
||||||
"fieldtype": "Date",
|
"fieldtype": "Date",
|
||||||
"label": "Supplier Invoice Date",
|
"label": "Supplier Invoice Date",
|
||||||
|
"no_copy": 1,
|
||||||
"oldfieldname": "bill_date",
|
"oldfieldname": "bill_date",
|
||||||
"oldfieldtype": "Date",
|
"oldfieldtype": "Date",
|
||||||
"print_hide": 1
|
"print_hide": 1
|
||||||
@@ -1333,8 +1334,7 @@
|
|||||||
"icon": "fa fa-file-text",
|
"icon": "fa fa-file-text",
|
||||||
"idx": 204,
|
"idx": 204,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"modified": "2020-09-21 12:22:09.164068",
|
||||||
"modified": "2020-08-03 23:20:04.466153",
|
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Purchase Invoice",
|
"name": "Purchase Invoice",
|
||||||
|
|||||||
@@ -572,7 +572,8 @@ class SalesInvoice(SellingController):
|
|||||||
|
|
||||||
def validate_pos(self):
|
def validate_pos(self):
|
||||||
if self.is_return:
|
if self.is_return:
|
||||||
if flt(self.paid_amount) + flt(self.write_off_amount) - flt(self.grand_total) > \
|
invoice_total = self.rounded_total or self.grand_total
|
||||||
|
if flt(self.paid_amount) + flt(self.write_off_amount) - flt(invoice_total) > \
|
||||||
1.0/(10.0**(self.precision("grand_total") + 1.0)):
|
1.0/(10.0**(self.precision("grand_total") + 1.0)):
|
||||||
frappe.throw(_("Paid amount + Write Off Amount can not be greater than Grand Total"))
|
frappe.throw(_("Paid amount + Write Off Amount can not be greater than Grand Total"))
|
||||||
|
|
||||||
|
|||||||
@@ -171,7 +171,7 @@ def validate_account_for_perpetual_inventory(gl_map):
|
|||||||
frappe.throw(_("Account: {0} can only be updated via Stock Transactions")
|
frappe.throw(_("Account: {0} can only be updated via Stock Transactions")
|
||||||
.format(account), StockAccountInvalidTransaction)
|
.format(account), StockAccountInvalidTransaction)
|
||||||
|
|
||||||
elif account_bal != stock_bal:
|
elif abs(account_bal - stock_bal) > 0.1:
|
||||||
precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"),
|
precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"),
|
||||||
currency=frappe.get_cached_value('Company', gl_map[0].company, "default_currency"))
|
currency=frappe.get_cached_value('Company', gl_map[0].company, "default_currency"))
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"steps": [
|
"steps": [
|
||||||
{
|
{
|
||||||
"step": "Chart Of Accounts"
|
"step": "Chart of Accounts"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"step": "Setup Taxes"
|
"step": "Setup Taxes"
|
||||||
|
|||||||
@@ -10,11 +10,11 @@
|
|||||||
"is_skipped": 0,
|
"is_skipped": 0,
|
||||||
"modified": "2020-05-14 17:40:28.410447",
|
"modified": "2020-05-14 17:40:28.410447",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"name": "Chart Of Accounts",
|
"name": "Chart of Accounts",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"path": "Tree/Account",
|
"path": "Tree/Account",
|
||||||
"reference_document": "Account",
|
"reference_document": "Account",
|
||||||
"show_full_form": 0,
|
"show_full_form": 0,
|
||||||
"title": "Review Chart Of Accounts",
|
"title": "Review Chart of Accounts",
|
||||||
"validate_action": 0
|
"validate_action": 0
|
||||||
}
|
}
|
||||||
@@ -19,8 +19,7 @@ def reconcile(bank_transaction, payment_doctype, payment_name):
|
|||||||
gl_entry = frappe.get_doc("GL Entry", dict(account=account, voucher_type=payment_doctype, voucher_no=payment_name))
|
gl_entry = frappe.get_doc("GL Entry", dict(account=account, voucher_type=payment_doctype, voucher_no=payment_name))
|
||||||
|
|
||||||
if payment_doctype == "Payment Entry" and payment_entry.unallocated_amount > transaction.unallocated_amount:
|
if payment_doctype == "Payment Entry" and payment_entry.unallocated_amount > transaction.unallocated_amount:
|
||||||
frappe.throw(_("The unallocated amount of Payment Entry {0} \
|
frappe.throw(_("The unallocated amount of Payment Entry {0} is greater than the Bank Transaction's unallocated amount").format(payment_name))
|
||||||
is greater than the Bank Transaction's unallocated amount").format(payment_name))
|
|
||||||
|
|
||||||
if transaction.unallocated_amount == 0:
|
if transaction.unallocated_amount == 0:
|
||||||
frappe.throw(_("This bank transaction is already fully reconciled"))
|
frappe.throw(_("This bank transaction is already fully reconciled"))
|
||||||
@@ -83,50 +82,30 @@ def check_matching_amount(bank_account, company, transaction):
|
|||||||
"party", "party_type", "posting_date", "{0}".format(currency_field)], filters=[["paid_amount", "like", "{0}%".format(amount)],
|
"party", "party_type", "posting_date", "{0}".format(currency_field)], filters=[["paid_amount", "like", "{0}%".format(amount)],
|
||||||
["docstatus", "=", "1"], ["payment_type", "=", [payment_type, "Internal Transfer"]], ["ifnull(clearance_date, '')", "=", ""], ["{0}".format(account_from_to), "=", "{0}".format(bank_account)]])
|
["docstatus", "=", "1"], ["payment_type", "=", [payment_type, "Internal Transfer"]], ["ifnull(clearance_date, '')", "=", ""], ["{0}".format(account_from_to), "=", "{0}".format(bank_account)]])
|
||||||
|
|
||||||
if transaction.credit > 0:
|
jea_side = "debit" if transaction.credit > 0 else "credit"
|
||||||
journal_entries = frappe.db.sql("""
|
journal_entries = frappe.db.sql(f"""
|
||||||
SELECT
|
SELECT
|
||||||
'Journal Entry' as doctype, je.name, je.posting_date, je.cheque_no as reference_no,
|
'Journal Entry' as doctype, je.name, je.posting_date, je.cheque_no as reference_no,
|
||||||
je.pay_to_recd_from as party, je.cheque_date as reference_date, jea.debit_in_account_currency as paid_amount
|
jea.account_currency as currency, je.pay_to_recd_from as party, je.cheque_date as reference_date,
|
||||||
FROM
|
jea.{jea_side}_in_account_currency as paid_amount
|
||||||
`tabJournal Entry Account` as jea
|
FROM
|
||||||
JOIN
|
`tabJournal Entry Account` as jea
|
||||||
`tabJournal Entry` as je
|
JOIN
|
||||||
ON
|
`tabJournal Entry` as je
|
||||||
jea.parent = je.name
|
ON
|
||||||
WHERE
|
jea.parent = je.name
|
||||||
(je.clearance_date is null or je.clearance_date='0000-00-00')
|
WHERE
|
||||||
AND
|
(je.clearance_date is null or je.clearance_date='0000-00-00')
|
||||||
jea.account = %s
|
AND
|
||||||
AND
|
jea.account = %(bank_account)s
|
||||||
jea.debit_in_account_currency like %s
|
AND
|
||||||
AND
|
jea.{jea_side}_in_account_currency like %(txt)s
|
||||||
je.docstatus = 1
|
AND
|
||||||
""", (bank_account, amount), as_dict=True)
|
je.docstatus = 1
|
||||||
else:
|
""", {
|
||||||
journal_entries = frappe.db.sql("""
|
'bank_account': bank_account,
|
||||||
SELECT
|
'txt': '%%%s%%' % amount
|
||||||
'Journal Entry' as doctype, je.name, je.posting_date, je.cheque_no as reference_no,
|
}, as_dict=True)
|
||||||
jea.account_currency as currency, je.pay_to_recd_from as party, je.cheque_date as reference_date,
|
|
||||||
jea.credit_in_account_currency as paid_amount
|
|
||||||
FROM
|
|
||||||
`tabJournal Entry Account` as jea
|
|
||||||
JOIN
|
|
||||||
`tabJournal Entry` as je
|
|
||||||
ON
|
|
||||||
jea.parent = je.name
|
|
||||||
WHERE
|
|
||||||
(je.clearance_date is null or je.clearance_date='0000-00-00')
|
|
||||||
AND
|
|
||||||
jea.account = %(bank_account)s
|
|
||||||
AND
|
|
||||||
jea.credit_in_account_currency like %(txt)s
|
|
||||||
AND
|
|
||||||
je.docstatus = 1
|
|
||||||
""", {
|
|
||||||
'bank_account': bank_account,
|
|
||||||
'txt': '%%%s%%' % amount
|
|
||||||
}, as_dict=True)
|
|
||||||
|
|
||||||
if transaction.credit > 0:
|
if transaction.credit > 0:
|
||||||
sales_invoices = frappe.db.sql("""
|
sales_invoices = frappe.db.sql("""
|
||||||
@@ -264,7 +243,11 @@ def check_amount_vs_description(amount_matching, description_matching):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
if "reference_no" in am_match and "reference_no" in des_match:
|
if "reference_no" in am_match and "reference_no" in des_match:
|
||||||
if difflib.SequenceMatcher(lambda x: x == " ", am_match["reference_no"], des_match["reference_no"]).ratio() > 70:
|
# Sequence Matcher does not handle None as input
|
||||||
|
am_reference = am_match["reference_no"] or ""
|
||||||
|
des_reference = des_match["reference_no"] or ""
|
||||||
|
|
||||||
|
if difflib.SequenceMatcher(lambda x: x == " ", am_reference, des_reference).ratio() > 70:
|
||||||
if am_match not in result:
|
if am_match not in result:
|
||||||
result.append(am_match)
|
result.append(am_match)
|
||||||
if result:
|
if result:
|
||||||
|
|||||||
@@ -203,7 +203,7 @@ def set_account_and_due_date(party, account, party_type, company, posting_date,
|
|||||||
return out
|
return out
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_party_account(party_type, party, company):
|
def get_party_account(party_type, party, company=None):
|
||||||
"""Returns the account for the given `party`.
|
"""Returns the account for the given `party`.
|
||||||
Will first search in party (Customer / Supplier) record, if not found,
|
Will first search in party (Customer / Supplier) record, if not found,
|
||||||
will search in group (Customer Group / Supplier Group),
|
will search in group (Customer Group / Supplier Group),
|
||||||
|
|||||||
@@ -3,6 +3,14 @@
|
|||||||
|
|
||||||
frappe.query_reports["Bank Reconciliation Statement"] = {
|
frappe.query_reports["Bank Reconciliation Statement"] = {
|
||||||
"filters": [
|
"filters": [
|
||||||
|
{
|
||||||
|
"fieldname":"company",
|
||||||
|
"label": __("Company"),
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"options": "Company",
|
||||||
|
"reqd": 1,
|
||||||
|
"default": frappe.defaults.get_user_default("Company")
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"fieldname":"account",
|
"fieldname":"account",
|
||||||
"label": __("Bank Account"),
|
"label": __("Bank Account"),
|
||||||
@@ -12,11 +20,14 @@ frappe.query_reports["Bank Reconciliation Statement"] = {
|
|||||||
locals[":Company"][frappe.defaults.get_user_default("Company")]["default_bank_account"]: "",
|
locals[":Company"][frappe.defaults.get_user_default("Company")]["default_bank_account"]: "",
|
||||||
"reqd": 1,
|
"reqd": 1,
|
||||||
"get_query": function() {
|
"get_query": function() {
|
||||||
|
var company = frappe.query_report.get_filter_value('company')
|
||||||
return {
|
return {
|
||||||
"query": "erpnext.controllers.queries.get_account_list",
|
"query": "erpnext.controllers.queries.get_account_list",
|
||||||
"filters": [
|
"filters": [
|
||||||
['Account', 'account_type', 'in', 'Bank, Cash'],
|
['Account', 'account_type', 'in', 'Bank, Cash'],
|
||||||
['Account', 'is_group', '=', 0],
|
['Account', 'is_group', '=', 0],
|
||||||
|
['Account', 'disabled', '=', 0],
|
||||||
|
['Account', 'company', '=', company],
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -796,7 +796,7 @@ def get_children(doctype, parent, company, is_root=False):
|
|||||||
|
|
||||||
return acc
|
return acc
|
||||||
|
|
||||||
def create_payment_gateway_account(gateway):
|
def create_payment_gateway_account(gateway, payment_channel="Email"):
|
||||||
from erpnext.setup.setup_wizard.operations.company_setup import create_bank_account
|
from erpnext.setup.setup_wizard.operations.company_setup import create_bank_account
|
||||||
|
|
||||||
company = frappe.db.get_value("Global Defaults", None, "default_company")
|
company = frappe.db.get_value("Global Defaults", None, "default_company")
|
||||||
@@ -831,7 +831,8 @@ def create_payment_gateway_account(gateway):
|
|||||||
"is_default": 1,
|
"is_default": 1,
|
||||||
"payment_gateway": gateway,
|
"payment_gateway": gateway,
|
||||||
"payment_account": bank_account.name,
|
"payment_account": bank_account.name,
|
||||||
"currency": bank_account.account_currency
|
"currency": bank_account.account_currency,
|
||||||
|
"payment_channel": payment_channel
|
||||||
}).insert(ignore_permissions=True)
|
}).insert(ignore_permissions=True)
|
||||||
|
|
||||||
except frappe.DuplicateEntryError:
|
except frappe.DuplicateEntryError:
|
||||||
|
|||||||
@@ -46,26 +46,26 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "po_required",
|
"fieldname": "po_required",
|
||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"label": "Purchase Order Required for Purchase Invoice & Receipt Creation",
|
"label": "Is Purchase Order Required for Purchase Invoice & Receipt Creation?",
|
||||||
"options": "No\nYes"
|
"options": "No\nYes"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "pr_required",
|
"fieldname": "pr_required",
|
||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"label": "Purchase Receipt Required for Purchase Invoice Creation",
|
"label": "Is Purchase Receipt Required for Purchase Invoice Creation?",
|
||||||
"options": "No\nYes"
|
"options": "No\nYes"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
"fieldname": "maintain_same_rate",
|
"fieldname": "maintain_same_rate",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Maintain same rate throughout purchase cycle"
|
"label": "Maintain Same Rate Throughout the Purchase Cycle"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
"fieldname": "allow_multiple_items",
|
"fieldname": "allow_multiple_items",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Allow Item to be added multiple times in a transaction"
|
"label": "Allow Item To Be Added Multiple Times in a Transaction"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "subcontract",
|
"fieldname": "subcontract",
|
||||||
@@ -93,9 +93,10 @@
|
|||||||
],
|
],
|
||||||
"icon": "fa fa-cog",
|
"icon": "fa fa-cog",
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-05-15 14:49:32.513611",
|
"modified": "2020-10-13 12:00:23.276329",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Buying",
|
"module": "Buying",
|
||||||
"name": "Buying Settings",
|
"name": "Buying Settings",
|
||||||
|
|||||||
@@ -203,9 +203,39 @@ class TestPurchaseOrder(unittest.TestCase):
|
|||||||
frappe.set_user("Administrator")
|
frappe.set_user("Administrator")
|
||||||
|
|
||||||
def test_update_child_with_tax_template(self):
|
def test_update_child_with_tax_template(self):
|
||||||
|
"""
|
||||||
|
Test Action: Create a PO with one item having its tax account head already in the PO.
|
||||||
|
Add the same item + new item with tax template via Update Items.
|
||||||
|
Expected result: First Item's tax row is updated. New tax row is added for second Item.
|
||||||
|
"""
|
||||||
|
if not frappe.db.exists("Item", "Test Item with Tax"):
|
||||||
|
make_item("Test Item with Tax", {
|
||||||
|
'is_stock_item': 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
if not frappe.db.exists("Item Tax Template", {"title": 'Test Update Items Template'}):
|
||||||
|
frappe.get_doc({
|
||||||
|
'doctype': 'Item Tax Template',
|
||||||
|
'title': 'Test Update Items Template',
|
||||||
|
'company': '_Test Company',
|
||||||
|
'taxes': [
|
||||||
|
{
|
||||||
|
'tax_type': "_Test Account Service Tax - _TC",
|
||||||
|
'tax_rate': 10,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}).insert()
|
||||||
|
|
||||||
|
new_item_with_tax = frappe.get_doc("Item", "Test Item with Tax")
|
||||||
|
|
||||||
|
new_item_with_tax.append("taxes", {
|
||||||
|
"item_tax_template": "Test Update Items Template",
|
||||||
|
"valid_from": nowdate()
|
||||||
|
})
|
||||||
|
new_item_with_tax.save()
|
||||||
|
|
||||||
tax_template = "_Test Account Excise Duty @ 10"
|
tax_template = "_Test Account Excise Duty @ 10"
|
||||||
item = "_Test Item Home Desktop 100"
|
item = "_Test Item Home Desktop 100"
|
||||||
|
|
||||||
if not frappe.db.exists("Item Tax", {"parent":item, "item_tax_template":tax_template}):
|
if not frappe.db.exists("Item Tax", {"parent":item, "item_tax_template":tax_template}):
|
||||||
item_doc = frappe.get_doc("Item", item)
|
item_doc = frappe.get_doc("Item", item)
|
||||||
item_doc.append("taxes", {
|
item_doc.append("taxes", {
|
||||||
@@ -237,17 +267,25 @@ class TestPurchaseOrder(unittest.TestCase):
|
|||||||
|
|
||||||
items = json.dumps([
|
items = json.dumps([
|
||||||
{'item_code' : item, 'rate' : 500, 'qty' : 1, 'docname': po.items[0].name},
|
{'item_code' : item, 'rate' : 500, 'qty' : 1, 'docname': po.items[0].name},
|
||||||
{'item_code' : item, 'rate' : 100, 'qty' : 1} # added item
|
{'item_code' : item, 'rate' : 100, 'qty' : 1}, # added item whose tax account head already exists in PO
|
||||||
|
{'item_code' : new_item_with_tax.name, 'rate' : 100, 'qty' : 1} # added item whose tax account head is missing in PO
|
||||||
])
|
])
|
||||||
update_child_qty_rate('Purchase Order', items, po.name)
|
update_child_qty_rate('Purchase Order', items, po.name)
|
||||||
|
|
||||||
po.reload()
|
po.reload()
|
||||||
self.assertEqual(po.taxes[0].tax_amount, 60)
|
self.assertEqual(po.taxes[0].tax_amount, 70)
|
||||||
self.assertEqual(po.taxes[0].total, 660)
|
self.assertEqual(po.taxes[0].total, 770)
|
||||||
|
self.assertEqual(po.taxes[1].account_head, "_Test Account Service Tax - _TC")
|
||||||
|
self.assertEqual(po.taxes[1].tax_amount, 70)
|
||||||
|
self.assertEqual(po.taxes[1].total, 840)
|
||||||
|
|
||||||
|
# teardown
|
||||||
frappe.db.sql("""UPDATE `tabItem Tax` set valid_from = NULL
|
frappe.db.sql("""UPDATE `tabItem Tax` set valid_from = NULL
|
||||||
where parent = %(item)s and item_tax_template = %(tax)s""",
|
where parent = %(item)s and item_tax_template = %(tax)s""", {"item": item, "tax": tax_template})
|
||||||
{"item": item, "tax": tax_template})
|
po.cancel()
|
||||||
|
po.delete()
|
||||||
|
new_item_with_tax.delete()
|
||||||
|
frappe.get_doc("Item Tax Template", "Test Update Items Template").delete()
|
||||||
|
|
||||||
def test_update_child_uom_conv_factor_change(self):
|
def test_update_child_uom_conv_factor_change(self):
|
||||||
po = create_purchase_order(item_code="_Test FG Item", is_subcontracted="Yes")
|
po = create_purchase_order(item_code="_Test FG Item", is_subcontracted="Yes")
|
||||||
|
|||||||
@@ -22,8 +22,6 @@ frappe.ui.form.on("Request for Quotation",{
|
|||||||
},
|
},
|
||||||
|
|
||||||
onload: function(frm) {
|
onload: function(frm) {
|
||||||
frm.add_fetch('email_template', 'response', 'message_for_supplier');
|
|
||||||
|
|
||||||
if(!frm.doc.message_for_supplier) {
|
if(!frm.doc.message_for_supplier) {
|
||||||
frm.set_value("message_for_supplier", __("Please supply the specified items at the best possible rates"))
|
frm.set_value("message_for_supplier", __("Please supply the specified items at the best possible rates"))
|
||||||
}
|
}
|
||||||
@@ -31,14 +29,12 @@ frappe.ui.form.on("Request for Quotation",{
|
|||||||
|
|
||||||
refresh: function(frm, cdt, cdn) {
|
refresh: function(frm, cdt, cdn) {
|
||||||
if (frm.doc.docstatus === 1) {
|
if (frm.doc.docstatus === 1) {
|
||||||
frm.add_custom_button(__('Create'),
|
|
||||||
function(){ frm.trigger("make_suppplier_quotation") }, __("Supplier Quotation"));
|
|
||||||
|
|
||||||
frm.add_custom_button(__("View"),
|
frm.add_custom_button(__('Supplier Quotation'),
|
||||||
function(){ frappe.set_route('List', 'Supplier Quotation',
|
function(){ frm.trigger("make_suppplier_quotation") }, __("Create"));
|
||||||
{'request_for_quotation': frm.doc.name}) }, __("Supplier Quotation"));
|
|
||||||
|
|
||||||
frm.add_custom_button(__("Send Supplier Emails"), function() {
|
|
||||||
|
frm.add_custom_button(__("Send Emails to Suppliers"), function() {
|
||||||
frappe.call({
|
frappe.call({
|
||||||
method: 'erpnext.buying.doctype.request_for_quotation.request_for_quotation.send_supplier_emails',
|
method: 'erpnext.buying.doctype.request_for_quotation.request_for_quotation.send_supplier_emails',
|
||||||
freeze: true,
|
freeze: true,
|
||||||
@@ -49,151 +45,143 @@ frappe.ui.form.on("Request for Quotation",{
|
|||||||
frm.reload_doc();
|
frm.reload_doc();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
}, __("Tools"));
|
||||||
|
|
||||||
|
frm.add_custom_button(__('Download PDF'), () => {
|
||||||
|
var suppliers = [];
|
||||||
|
const fields = [{
|
||||||
|
fieldtype: 'Link',
|
||||||
|
label: __('Select a Supplier'),
|
||||||
|
fieldname: 'supplier',
|
||||||
|
options: 'Supplier',
|
||||||
|
reqd: 1,
|
||||||
|
get_query: () => {
|
||||||
|
return {
|
||||||
|
filters: [
|
||||||
|
["Supplier", "name", "in", frm.doc.suppliers.map((row) => {return row.supplier;})]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
|
||||||
|
frappe.prompt(fields, data => {
|
||||||
|
var child = locals[cdt][cdn]
|
||||||
|
|
||||||
|
var w = window.open(
|
||||||
|
frappe.urllib.get_full_url("/api/method/erpnext.buying.doctype.request_for_quotation.request_for_quotation.get_pdf?"
|
||||||
|
+"doctype="+encodeURIComponent(frm.doc.doctype)
|
||||||
|
+"&name="+encodeURIComponent(frm.doc.name)
|
||||||
|
+"&supplier="+encodeURIComponent(data.supplier)
|
||||||
|
+"&no_letterhead=0"));
|
||||||
|
if(!w) {
|
||||||
|
frappe.msgprint(__("Please enable pop-ups")); return;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'Download PDF for Supplier',
|
||||||
|
'Download');
|
||||||
|
},
|
||||||
|
__("Tools"));
|
||||||
|
|
||||||
|
frm.page.set_inner_btn_group_as_primary(__('Create'));
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
get_suppliers_button: function (frm) {
|
|
||||||
var doc = frm.doc;
|
|
||||||
var dialog = new frappe.ui.Dialog({
|
|
||||||
title: __("Get Suppliers"),
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
"fieldtype": "Select", "label": __("Get Suppliers By"),
|
|
||||||
"fieldname": "search_type",
|
|
||||||
"options": ["Tag","Supplier Group"],
|
|
||||||
"reqd": 1,
|
|
||||||
onchange() {
|
|
||||||
if(dialog.get_value('search_type') == 'Tag'){
|
|
||||||
frappe.call({
|
|
||||||
method: 'erpnext.buying.doctype.request_for_quotation.request_for_quotation.get_supplier_tag',
|
|
||||||
}).then(r => {
|
|
||||||
dialog.set_df_property("tag", "options", r.message)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldtype": "Link", "label": __("Supplier Group"),
|
|
||||||
"fieldname": "supplier_group",
|
|
||||||
"options": "Supplier Group",
|
|
||||||
"reqd": 0,
|
|
||||||
"depends_on": "eval:doc.search_type == 'Supplier Group'"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldtype": "Select", "label": __("Tag"),
|
|
||||||
"fieldname": "tag",
|
|
||||||
"reqd": 0,
|
|
||||||
"depends_on": "eval:doc.search_type == 'Tag'",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldtype": "Button", "label": __("Add All Suppliers"),
|
|
||||||
"fieldname": "add_suppliers"
|
|
||||||
},
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
dialog.fields_dict.add_suppliers.$input.click(function() {
|
|
||||||
var args = dialog.get_values();
|
|
||||||
if(!args) return;
|
|
||||||
dialog.hide();
|
|
||||||
|
|
||||||
//Remove blanks
|
|
||||||
for (var j = 0; j < frm.doc.suppliers.length; j++) {
|
|
||||||
if(!frm.doc.suppliers[j].hasOwnProperty("supplier")) {
|
|
||||||
frm.get_field("suppliers").grid.grid_rows[j].remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function load_suppliers(r) {
|
|
||||||
if(r.message) {
|
|
||||||
for (var i = 0; i < r.message.length; i++) {
|
|
||||||
var exists = false;
|
|
||||||
if (r.message[i].constructor === Array){
|
|
||||||
var supplier = r.message[i][0];
|
|
||||||
} else {
|
|
||||||
var supplier = r.message[i].name;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var j = 0; j < doc.suppliers.length;j++) {
|
|
||||||
if (supplier === doc.suppliers[j].supplier) {
|
|
||||||
exists = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if(!exists) {
|
|
||||||
var d = frm.add_child('suppliers');
|
|
||||||
d.supplier = supplier;
|
|
||||||
frm.script_manager.trigger("supplier", d.doctype, d.name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
frm.refresh_field("suppliers");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (args.search_type === "Tag" && args.tag) {
|
|
||||||
return frappe.call({
|
|
||||||
type: "GET",
|
|
||||||
method: "frappe.desk.doctype.tag.tag.get_tagged_docs",
|
|
||||||
args: {
|
|
||||||
"doctype": "Supplier",
|
|
||||||
"tag": args.tag
|
|
||||||
},
|
|
||||||
callback: load_suppliers
|
|
||||||
});
|
|
||||||
} else if (args.supplier_group) {
|
|
||||||
return frappe.call({
|
|
||||||
method: "frappe.client.get_list",
|
|
||||||
args: {
|
|
||||||
doctype: "Supplier",
|
|
||||||
order_by: "name",
|
|
||||||
fields: ["name"],
|
|
||||||
filters: [["Supplier", "supplier_group", "=", args.supplier_group]]
|
|
||||||
|
|
||||||
},
|
|
||||||
callback: load_suppliers
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
dialog.show();
|
|
||||||
|
|
||||||
},
|
|
||||||
make_suppplier_quotation: function(frm) {
|
make_suppplier_quotation: function(frm) {
|
||||||
var doc = frm.doc;
|
var doc = frm.doc;
|
||||||
var dialog = new frappe.ui.Dialog({
|
var dialog = new frappe.ui.Dialog({
|
||||||
title: __("For Supplier"),
|
title: __("Create Supplier Quotation"),
|
||||||
fields: [
|
fields: [
|
||||||
{ "fieldtype": "Select", "label": __("Supplier"),
|
{ "fieldtype": "Select", "label": __("Supplier"),
|
||||||
"fieldname": "supplier",
|
"fieldname": "supplier",
|
||||||
"options": doc.suppliers.map(d => d.supplier),
|
"options": doc.suppliers.map(d => d.supplier),
|
||||||
"reqd": 1,
|
"reqd": 1,
|
||||||
"default": doc.suppliers.length === 1 ? doc.suppliers[0].supplier_name : "" },
|
"default": doc.suppliers.length === 1 ? doc.suppliers[0].supplier_name : "" },
|
||||||
{ "fieldtype": "Button", "label": __('Create Supplier Quotation'),
|
],
|
||||||
"fieldname": "make_supplier_quotation", "cssClass": "btn-primary" },
|
primary_action_label: __("Create"),
|
||||||
|
primary_action: (args) => {
|
||||||
|
if(!args) return;
|
||||||
|
dialog.hide();
|
||||||
|
|
||||||
|
return frappe.call({
|
||||||
|
type: "GET",
|
||||||
|
method: "erpnext.buying.doctype.request_for_quotation.request_for_quotation.make_supplier_quotation_from_rfq",
|
||||||
|
args: {
|
||||||
|
"source_name": doc.name,
|
||||||
|
"for_supplier": args.supplier
|
||||||
|
},
|
||||||
|
freeze: true,
|
||||||
|
callback: function(r) {
|
||||||
|
if(!r.exc) {
|
||||||
|
var doc = frappe.model.sync(r.message);
|
||||||
|
frappe.set_route("Form", r.message.doctype, r.message.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
dialog.show()
|
||||||
|
},
|
||||||
|
|
||||||
|
preview: (frm) => {
|
||||||
|
let dialog = new frappe.ui.Dialog({
|
||||||
|
title: __('Preview Email'),
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
label: __('Supplier'),
|
||||||
|
fieldtype: 'Select',
|
||||||
|
fieldname: 'supplier',
|
||||||
|
options: frm.doc.suppliers.map(row => row.supplier),
|
||||||
|
reqd: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldtype: 'Column Break',
|
||||||
|
fieldname: 'col_break_1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __('Subject'),
|
||||||
|
fieldtype: 'Data',
|
||||||
|
fieldname: 'subject',
|
||||||
|
read_only: 1,
|
||||||
|
depends_on: 'subject'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldtype: 'Section Break',
|
||||||
|
fieldname: 'sec_break_1',
|
||||||
|
hide_border: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __('Email'),
|
||||||
|
fieldtype: 'HTML',
|
||||||
|
fieldname: 'email_preview'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldtype: 'Section Break',
|
||||||
|
fieldname: 'sec_break_2'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __('Note'),
|
||||||
|
fieldtype: 'HTML',
|
||||||
|
fieldname: 'note'
|
||||||
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
dialog.fields_dict.make_supplier_quotation.$input.click(function() {
|
dialog.fields_dict['supplier'].df.onchange = () => {
|
||||||
var args = dialog.get_values();
|
var supplier = dialog.get_value('supplier');
|
||||||
if(!args) return;
|
frm.call('get_supplier_email_preview', {supplier: supplier}).then(result => {
|
||||||
dialog.hide();
|
dialog.fields_dict.email_preview.$wrapper.empty();
|
||||||
return frappe.call({
|
dialog.fields_dict.email_preview.$wrapper.append(result.message);
|
||||||
type: "GET",
|
|
||||||
method: "erpnext.buying.doctype.request_for_quotation.request_for_quotation.make_supplier_quotation_from_rfq",
|
|
||||||
args: {
|
|
||||||
"source_name": doc.name,
|
|
||||||
"for_supplier": args.supplier
|
|
||||||
},
|
|
||||||
freeze: true,
|
|
||||||
callback: function(r) {
|
|
||||||
if(!r.exc) {
|
|
||||||
var doc = frappe.model.sync(r.message);
|
|
||||||
frappe.set_route("Form", r.message.doctype, r.message.name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
|
||||||
dialog.show()
|
}
|
||||||
|
|
||||||
|
dialog.fields_dict.note.$wrapper.append(`<p class="small text-muted">This is a preview of the email to be sent. A PDF of the document will
|
||||||
|
automatically be attached with the email.</p>`);
|
||||||
|
|
||||||
|
dialog.set_value("subject", frm.doc.subject);
|
||||||
|
dialog.show();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -215,42 +203,6 @@ frappe.ui.form.on("Request for Quotation Supplier",{
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
download_pdf: function(frm, cdt, cdn) {
|
|
||||||
var child = locals[cdt][cdn]
|
|
||||||
|
|
||||||
var w = window.open(
|
|
||||||
frappe.urllib.get_full_url("/api/method/erpnext.buying.doctype.request_for_quotation.request_for_quotation.get_pdf?"
|
|
||||||
+"doctype="+encodeURIComponent(frm.doc.doctype)
|
|
||||||
+"&name="+encodeURIComponent(frm.doc.name)
|
|
||||||
+"&supplier_idx="+encodeURIComponent(child.idx)
|
|
||||||
+"&no_letterhead=0"));
|
|
||||||
if(!w) {
|
|
||||||
frappe.msgprint(__("Please enable pop-ups")); return;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
no_quote: function(frm, cdt, cdn) {
|
|
||||||
var d = locals[cdt][cdn];
|
|
||||||
if (d.no_quote) {
|
|
||||||
if (d.quote_status != __('Received')) {
|
|
||||||
frappe.model.set_value(cdt, cdn, 'quote_status', 'No Quote');
|
|
||||||
} else {
|
|
||||||
frappe.msgprint(__("Cannot set a received RFQ to No Quote"));
|
|
||||||
frappe.model.set_value(cdt, cdn, 'no_quote', 0);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
d.quote_status = __('Pending');
|
|
||||||
frm.call({
|
|
||||||
method:"update_rfq_supplier_status",
|
|
||||||
doc: frm.doc,
|
|
||||||
args: {
|
|
||||||
sup_name: d.supplier
|
|
||||||
},
|
|
||||||
callback: function(r) {
|
|
||||||
frm.refresh_field("suppliers");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
erpnext.buying.RequestforQuotationController = erpnext.buying.BuyingController.extend({
|
erpnext.buying.RequestforQuotationController = erpnext.buying.BuyingController.extend({
|
||||||
@@ -274,9 +226,10 @@ erpnext.buying.RequestforQuotationController = erpnext.buying.BuyingController.e
|
|||||||
per_ordered: ["<", 99.99]
|
per_ordered: ["<", 99.99]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}, __("Get items from"));
|
}, __("Get Items From"));
|
||||||
|
|
||||||
// Get items from Opportunity
|
// Get items from Opportunity
|
||||||
this.frm.add_custom_button(__('Opportunity'),
|
this.frm.add_custom_button(__('Opportunity'),
|
||||||
function() {
|
function() {
|
||||||
erpnext.utils.map_current_doc({
|
erpnext.utils.map_current_doc({
|
||||||
method: "erpnext.crm.doctype.opportunity.opportunity.make_request_for_quotation",
|
method: "erpnext.crm.doctype.opportunity.opportunity.make_request_for_quotation",
|
||||||
@@ -286,7 +239,8 @@ erpnext.buying.RequestforQuotationController = erpnext.buying.BuyingController.e
|
|||||||
company: me.frm.doc.company
|
company: me.frm.doc.company
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}, __("Get items from"));
|
}, __("Get Items From"));
|
||||||
|
|
||||||
// Get items from open Material Requests based on supplier
|
// Get items from open Material Requests based on supplier
|
||||||
this.frm.add_custom_button(__('Possible Supplier'), function() {
|
this.frm.add_custom_button(__('Possible Supplier'), function() {
|
||||||
// Create a dialog window for the user to pick their supplier
|
// Create a dialog window for the user to pick their supplier
|
||||||
@@ -324,8 +278,13 @@ erpnext.buying.RequestforQuotationController = erpnext.buying.BuyingController.e
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
d.show();
|
d.show();
|
||||||
}, __("Get items from"));
|
}, __("Get Items From"));
|
||||||
|
|
||||||
|
// Get Suppliers
|
||||||
|
this.frm.add_custom_button(__('Get Suppliers'),
|
||||||
|
function() {
|
||||||
|
me.get_suppliers_button(me.frm);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -335,9 +294,108 @@ erpnext.buying.RequestforQuotationController = erpnext.buying.BuyingController.e
|
|||||||
|
|
||||||
tc_name: function() {
|
tc_name: function() {
|
||||||
this.get_terms();
|
this.get_terms();
|
||||||
}
|
},
|
||||||
});
|
|
||||||
|
|
||||||
|
get_suppliers_button: function (frm) {
|
||||||
|
var doc = frm.doc;
|
||||||
|
var dialog = new frappe.ui.Dialog({
|
||||||
|
title: __("Get Suppliers"),
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
"fieldtype": "Select", "label": __("Get Suppliers By"),
|
||||||
|
"fieldname": "search_type",
|
||||||
|
"options": ["Tag","Supplier Group"],
|
||||||
|
"reqd": 1,
|
||||||
|
onchange() {
|
||||||
|
if(dialog.get_value('search_type') == 'Tag'){
|
||||||
|
frappe.call({
|
||||||
|
method: 'erpnext.buying.doctype.request_for_quotation.request_for_quotation.get_supplier_tag',
|
||||||
|
}).then(r => {
|
||||||
|
dialog.set_df_property("tag", "options", r.message)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldtype": "Link", "label": __("Supplier Group"),
|
||||||
|
"fieldname": "supplier_group",
|
||||||
|
"options": "Supplier Group",
|
||||||
|
"reqd": 0,
|
||||||
|
"depends_on": "eval:doc.search_type == 'Supplier Group'"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldtype": "Select", "label": __("Tag"),
|
||||||
|
"fieldname": "tag",
|
||||||
|
"reqd": 0,
|
||||||
|
"depends_on": "eval:doc.search_type == 'Tag'",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
primary_action_label: __("Add Suppliers"),
|
||||||
|
primary_action : (args) => {
|
||||||
|
if(!args) return;
|
||||||
|
dialog.hide();
|
||||||
|
|
||||||
|
//Remove blanks
|
||||||
|
for (var j = 0; j < frm.doc.suppliers.length; j++) {
|
||||||
|
if(!frm.doc.suppliers[j].hasOwnProperty("supplier")) {
|
||||||
|
frm.get_field("suppliers").grid.grid_rows[j].remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function load_suppliers(r) {
|
||||||
|
if(r.message) {
|
||||||
|
for (var i = 0; i < r.message.length; i++) {
|
||||||
|
var exists = false;
|
||||||
|
if (r.message[i].constructor === Array){
|
||||||
|
var supplier = r.message[i][0];
|
||||||
|
} else {
|
||||||
|
var supplier = r.message[i].name;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var j = 0; j < doc.suppliers.length;j++) {
|
||||||
|
if (supplier === doc.suppliers[j].supplier) {
|
||||||
|
exists = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(!exists) {
|
||||||
|
var d = frm.add_child('suppliers');
|
||||||
|
d.supplier = supplier;
|
||||||
|
frm.script_manager.trigger("supplier", d.doctype, d.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
frm.refresh_field("suppliers");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.search_type === "Tag" && args.tag) {
|
||||||
|
return frappe.call({
|
||||||
|
type: "GET",
|
||||||
|
method: "frappe.desk.doctype.tag.tag.get_tagged_docs",
|
||||||
|
args: {
|
||||||
|
"doctype": "Supplier",
|
||||||
|
"tag": args.tag
|
||||||
|
},
|
||||||
|
callback: load_suppliers
|
||||||
|
});
|
||||||
|
} else if (args.supplier_group) {
|
||||||
|
return frappe.call({
|
||||||
|
method: "frappe.client.get_list",
|
||||||
|
args: {
|
||||||
|
doctype: "Supplier",
|
||||||
|
order_by: "name",
|
||||||
|
fields: ["name"],
|
||||||
|
filters: [["Supplier", "supplier_group", "=", args.supplier_group]]
|
||||||
|
|
||||||
|
},
|
||||||
|
callback: load_suppliers
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
dialog.show();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// for backward compatibility: combine new and previous states
|
// for backward compatibility: combine new and previous states
|
||||||
$.extend(cur_frm.cscript, new erpnext.buying.RequestforQuotationController({frm: cur_frm}));
|
$.extend(cur_frm.cscript, new erpnext.buying.RequestforQuotationController({frm: cur_frm}));
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"actions": "",
|
"actions": [],
|
||||||
"allow_import": 1,
|
"allow_import": 1,
|
||||||
"autoname": "naming_series:",
|
"autoname": "naming_series:",
|
||||||
"creation": "2016-02-25 01:24:07.224790",
|
"creation": "2016-02-25 01:24:07.224790",
|
||||||
@@ -12,25 +12,27 @@
|
|||||||
"vendor",
|
"vendor",
|
||||||
"column_break1",
|
"column_break1",
|
||||||
"transaction_date",
|
"transaction_date",
|
||||||
|
"status",
|
||||||
|
"amended_from",
|
||||||
"suppliers_section",
|
"suppliers_section",
|
||||||
"suppliers",
|
"suppliers",
|
||||||
"get_suppliers_button",
|
|
||||||
"items_section",
|
"items_section",
|
||||||
"items",
|
"items",
|
||||||
"link_to_mrs",
|
"link_to_mrs",
|
||||||
"supplier_response_section",
|
"supplier_response_section",
|
||||||
|
"salutation",
|
||||||
"email_template",
|
"email_template",
|
||||||
|
"col_break_email_1",
|
||||||
|
"subject",
|
||||||
|
"preview",
|
||||||
|
"sec_break_email_2",
|
||||||
"message_for_supplier",
|
"message_for_supplier",
|
||||||
"terms_section_break",
|
"terms_section_break",
|
||||||
"tc_name",
|
"tc_name",
|
||||||
"terms",
|
"terms",
|
||||||
"printing_settings",
|
"printing_settings",
|
||||||
"select_print_heading",
|
"select_print_heading",
|
||||||
"letter_head",
|
"letter_head"
|
||||||
"more_info",
|
|
||||||
"status",
|
|
||||||
"column_break3",
|
|
||||||
"amended_from"
|
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@@ -78,6 +80,7 @@
|
|||||||
"width": "50%"
|
"width": "50%"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"default": "Today",
|
||||||
"fieldname": "transaction_date",
|
"fieldname": "transaction_date",
|
||||||
"fieldtype": "Date",
|
"fieldtype": "Date",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
@@ -94,16 +97,11 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "suppliers",
|
"fieldname": "suppliers",
|
||||||
"fieldtype": "Table",
|
"fieldtype": "Table",
|
||||||
"label": "Supplier Detail",
|
"label": "Suppliers",
|
||||||
"options": "Request for Quotation Supplier",
|
"options": "Request for Quotation Supplier",
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"reqd": 1
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "get_suppliers_button",
|
|
||||||
"fieldtype": "Button",
|
|
||||||
"label": "Get Suppliers"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldname": "items_section",
|
"fieldname": "items_section",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
@@ -126,8 +124,10 @@
|
|||||||
"label": "Link to Material Requests"
|
"label": "Link to Material Requests"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"depends_on": "eval:!doc.__islocal",
|
||||||
"fieldname": "supplier_response_section",
|
"fieldname": "supplier_response_section",
|
||||||
"fieldtype": "Section Break"
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Email Details"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "email_template",
|
"fieldname": "email_template",
|
||||||
@@ -137,6 +137,9 @@
|
|||||||
"print_hide": 1
|
"print_hide": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"allow_on_submit": 1,
|
||||||
|
"fetch_from": "email_template.response",
|
||||||
|
"fetch_if_empty": 1,
|
||||||
"fieldname": "message_for_supplier",
|
"fieldname": "message_for_supplier",
|
||||||
"fieldtype": "Text Editor",
|
"fieldtype": "Text Editor",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
@@ -197,14 +200,6 @@
|
|||||||
"options": "Letter Head",
|
"options": "Letter Head",
|
||||||
"print_hide": 1
|
"print_hide": 1
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"collapsible": 1,
|
|
||||||
"fieldname": "more_info",
|
|
||||||
"fieldtype": "Section Break",
|
|
||||||
"label": "More Information",
|
|
||||||
"oldfieldtype": "Section Break",
|
|
||||||
"options": "fa fa-file-text"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldname": "status",
|
"fieldname": "status",
|
||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
@@ -218,10 +213,6 @@
|
|||||||
"reqd": 1,
|
"reqd": 1,
|
||||||
"search_index": 1
|
"search_index": 1
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "column_break3",
|
|
||||||
"fieldtype": "Column Break"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldname": "amended_from",
|
"fieldname": "amended_from",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
@@ -230,12 +221,46 @@
|
|||||||
"options": "Request for Quotation",
|
"options": "Request for Quotation",
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fetch_from": "email_template.subject",
|
||||||
|
"fetch_if_empty": 1,
|
||||||
|
"fieldname": "subject",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Subject",
|
||||||
|
"print_hide": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Select a greeting for the receiver. E.g. Mr., Ms., etc.",
|
||||||
|
"fieldname": "salutation",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Salutation",
|
||||||
|
"no_copy": 1,
|
||||||
|
"options": "Salutation",
|
||||||
|
"print_hide": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "col_break_email_1",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "eval:!doc.docstatus==1",
|
||||||
|
"fieldname": "preview",
|
||||||
|
"fieldtype": "Button",
|
||||||
|
"label": "Preview Email"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "eval:!doc.__islocal",
|
||||||
|
"fieldname": "sec_break_email_2",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"hide_border": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "fa fa-shopping-cart",
|
"icon": "fa fa-shopping-cart",
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-06-25 14:37:21.140194",
|
"modified": "2020-10-16 17:49:09.561929",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Buying",
|
"module": "Buying",
|
||||||
"name": "Request for Quotation",
|
"name": "Request for Quotation",
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ class RequestforQuotation(BuyingController):
|
|||||||
super(RequestforQuotation, self).set_qty_as_per_stock_uom()
|
super(RequestforQuotation, self).set_qty_as_per_stock_uom()
|
||||||
self.update_email_id()
|
self.update_email_id()
|
||||||
|
|
||||||
|
if self.docstatus < 1:
|
||||||
|
# after amend and save, status still shows as cancelled, until submit
|
||||||
|
frappe.db.set(self, 'status', 'Draft')
|
||||||
|
|
||||||
def validate_duplicate_supplier(self):
|
def validate_duplicate_supplier(self):
|
||||||
supplier_list = [d.supplier for d in self.suppliers]
|
supplier_list = [d.supplier for d in self.suppliers]
|
||||||
if len(supplier_list) != len(set(supplier_list)):
|
if len(supplier_list) != len(set(supplier_list)):
|
||||||
@@ -51,7 +55,7 @@ class RequestforQuotation(BuyingController):
|
|||||||
|
|
||||||
def validate_email_id(self, args):
|
def validate_email_id(self, args):
|
||||||
if not args.email_id:
|
if not args.email_id:
|
||||||
frappe.throw(_("Row {0}: For Supplier {0}, Email Address is Required to Send Email").format(args.idx, args.supplier))
|
frappe.throw(_("Row {0}: For Supplier {1}, Email Address is Required to send an email").format(args.idx, frappe.bold(args.supplier)))
|
||||||
|
|
||||||
def on_submit(self):
|
def on_submit(self):
|
||||||
frappe.db.set(self, 'status', 'Submitted')
|
frappe.db.set(self, 'status', 'Submitted')
|
||||||
@@ -62,43 +66,58 @@ class RequestforQuotation(BuyingController):
|
|||||||
def on_cancel(self):
|
def on_cancel(self):
|
||||||
frappe.db.set(self, 'status', 'Cancelled')
|
frappe.db.set(self, 'status', 'Cancelled')
|
||||||
|
|
||||||
|
def get_supplier_email_preview(self, supplier):
|
||||||
|
"""Returns formatted email preview as string."""
|
||||||
|
rfq_suppliers = list(filter(lambda row: row.supplier == supplier, self.suppliers))
|
||||||
|
rfq_supplier = rfq_suppliers[0]
|
||||||
|
|
||||||
|
self.validate_email_id(rfq_supplier)
|
||||||
|
|
||||||
|
message = self.supplier_rfq_mail(rfq_supplier, '', self.get_link(), True)
|
||||||
|
|
||||||
|
return message
|
||||||
|
|
||||||
def send_to_supplier(self):
|
def send_to_supplier(self):
|
||||||
|
"""Sends RFQ mail to involved suppliers."""
|
||||||
for rfq_supplier in self.suppliers:
|
for rfq_supplier in self.suppliers:
|
||||||
if rfq_supplier.send_email:
|
if rfq_supplier.send_email:
|
||||||
self.validate_email_id(rfq_supplier)
|
self.validate_email_id(rfq_supplier)
|
||||||
|
|
||||||
# make new user if required
|
# make new user if required
|
||||||
update_password_link = self.update_supplier_contact(rfq_supplier, self.get_link())
|
update_password_link, contact = self.update_supplier_contact(rfq_supplier, self.get_link())
|
||||||
|
|
||||||
self.update_supplier_part_no(rfq_supplier)
|
self.update_supplier_part_no(rfq_supplier.supplier)
|
||||||
self.supplier_rfq_mail(rfq_supplier, update_password_link, self.get_link())
|
self.supplier_rfq_mail(rfq_supplier, update_password_link, self.get_link())
|
||||||
rfq_supplier.email_sent = 1
|
rfq_supplier.email_sent = 1
|
||||||
|
if not rfq_supplier.contact:
|
||||||
|
rfq_supplier.contact = contact
|
||||||
rfq_supplier.save()
|
rfq_supplier.save()
|
||||||
|
|
||||||
def get_link(self):
|
def get_link(self):
|
||||||
# RFQ link for supplier portal
|
# RFQ link for supplier portal
|
||||||
return get_url("/rfq/" + self.name)
|
return get_url("/rfq/" + self.name)
|
||||||
|
|
||||||
def update_supplier_part_no(self, args):
|
def update_supplier_part_no(self, supplier):
|
||||||
self.vendor = args.supplier
|
self.vendor = supplier
|
||||||
for item in self.items:
|
for item in self.items:
|
||||||
item.supplier_part_no = frappe.db.get_value('Item Supplier',
|
item.supplier_part_no = frappe.db.get_value('Item Supplier',
|
||||||
{'parent': item.item_code, 'supplier': args.supplier}, 'supplier_part_no')
|
{'parent': item.item_code, 'supplier': supplier}, 'supplier_part_no')
|
||||||
|
|
||||||
def update_supplier_contact(self, rfq_supplier, link):
|
def update_supplier_contact(self, rfq_supplier, link):
|
||||||
'''Create a new user for the supplier if not set in contact'''
|
'''Create a new user for the supplier if not set in contact'''
|
||||||
update_password_link = ''
|
update_password_link, contact = '', ''
|
||||||
|
|
||||||
if frappe.db.exists("User", rfq_supplier.email_id):
|
if frappe.db.exists("User", rfq_supplier.email_id):
|
||||||
user = frappe.get_doc("User", rfq_supplier.email_id)
|
user = frappe.get_doc("User", rfq_supplier.email_id)
|
||||||
else:
|
else:
|
||||||
user, update_password_link = self.create_user(rfq_supplier, link)
|
user, update_password_link = self.create_user(rfq_supplier, link)
|
||||||
|
|
||||||
self.update_contact_of_supplier(rfq_supplier, user)
|
contact = self.link_supplier_contact(rfq_supplier, user)
|
||||||
|
|
||||||
return update_password_link
|
return update_password_link, contact
|
||||||
|
|
||||||
def update_contact_of_supplier(self, rfq_supplier, user):
|
def link_supplier_contact(self, rfq_supplier, user):
|
||||||
|
"""If no Contact, create a new contact against Supplier. If Contact exists, check if email and user id set."""
|
||||||
if rfq_supplier.contact:
|
if rfq_supplier.contact:
|
||||||
contact = frappe.get_doc("Contact", rfq_supplier.contact)
|
contact = frappe.get_doc("Contact", rfq_supplier.contact)
|
||||||
else:
|
else:
|
||||||
@@ -115,6 +134,10 @@ class RequestforQuotation(BuyingController):
|
|||||||
|
|
||||||
contact.save(ignore_permissions=True)
|
contact.save(ignore_permissions=True)
|
||||||
|
|
||||||
|
if not rfq_supplier.contact:
|
||||||
|
# return contact to later update, RFQ supplier row's contact
|
||||||
|
return contact.name
|
||||||
|
|
||||||
def create_user(self, rfq_supplier, link):
|
def create_user(self, rfq_supplier, link):
|
||||||
user = frappe.get_doc({
|
user = frappe.get_doc({
|
||||||
'doctype': 'User',
|
'doctype': 'User',
|
||||||
@@ -129,22 +152,36 @@ class RequestforQuotation(BuyingController):
|
|||||||
|
|
||||||
return user, update_password_link
|
return user, update_password_link
|
||||||
|
|
||||||
def supplier_rfq_mail(self, data, update_password_link, rfq_link):
|
def supplier_rfq_mail(self, data, update_password_link, rfq_link, preview=False):
|
||||||
full_name = get_user_fullname(frappe.session['user'])
|
full_name = get_user_fullname(frappe.session['user'])
|
||||||
if full_name == "Guest":
|
if full_name == "Guest":
|
||||||
full_name = "Administrator"
|
full_name = "Administrator"
|
||||||
|
|
||||||
|
# send document dict and some important data from suppliers row
|
||||||
|
# to render message_for_supplier from any template
|
||||||
|
doc_args = self.as_dict()
|
||||||
|
doc_args.update({
|
||||||
|
'supplier': data.get('supplier'),
|
||||||
|
'supplier_name': data.get('supplier_name')
|
||||||
|
})
|
||||||
|
|
||||||
args = {
|
args = {
|
||||||
'update_password_link': update_password_link,
|
'update_password_link': update_password_link,
|
||||||
'message': frappe.render_template(self.message_for_supplier, data.as_dict()),
|
'message': frappe.render_template(self.message_for_supplier, doc_args),
|
||||||
'rfq_link': rfq_link,
|
'rfq_link': rfq_link,
|
||||||
'user_fullname': full_name
|
'user_fullname': full_name,
|
||||||
|
'supplier_name' : data.get('supplier_name'),
|
||||||
|
'supplier_salutation' : self.salutation or 'Dear Mx.',
|
||||||
}
|
}
|
||||||
|
|
||||||
subject = _("Request for Quotation")
|
subject = self.subject or _("Request for Quotation")
|
||||||
template = "templates/emails/request_for_quotation.html"
|
template = "templates/emails/request_for_quotation.html"
|
||||||
sender = frappe.session.user not in STANDARD_USERS and frappe.session.user or None
|
sender = frappe.session.user not in STANDARD_USERS and frappe.session.user or None
|
||||||
message = frappe.get_template(template).render(args)
|
message = frappe.get_template(template).render(args)
|
||||||
|
|
||||||
|
if preview:
|
||||||
|
return message
|
||||||
|
|
||||||
attachments = self.get_attachments()
|
attachments = self.get_attachments()
|
||||||
|
|
||||||
self.send_email(data, sender, subject, message, attachments)
|
self.send_email(data, sender, subject, message, attachments)
|
||||||
@@ -164,23 +201,22 @@ class RequestforQuotation(BuyingController):
|
|||||||
def update_rfq_supplier_status(self, sup_name=None):
|
def update_rfq_supplier_status(self, sup_name=None):
|
||||||
for supplier in self.suppliers:
|
for supplier in self.suppliers:
|
||||||
if sup_name == None or supplier.supplier == sup_name:
|
if sup_name == None or supplier.supplier == sup_name:
|
||||||
if supplier.quote_status != _('No Quote'):
|
quote_status = _('Received')
|
||||||
quote_status = _('Received')
|
for item in self.items:
|
||||||
for item in self.items:
|
sqi_count = frappe.db.sql("""
|
||||||
sqi_count = frappe.db.sql("""
|
SELECT
|
||||||
SELECT
|
COUNT(sqi.name) as count
|
||||||
COUNT(sqi.name) as count
|
FROM
|
||||||
FROM
|
`tabSupplier Quotation Item` as sqi,
|
||||||
`tabSupplier Quotation Item` as sqi,
|
`tabSupplier Quotation` as sq
|
||||||
`tabSupplier Quotation` as sq
|
WHERE sq.supplier = %(supplier)s
|
||||||
WHERE sq.supplier = %(supplier)s
|
AND sqi.docstatus = 1
|
||||||
AND sqi.docstatus = 1
|
AND sqi.request_for_quotation_item = %(rqi)s
|
||||||
AND sqi.request_for_quotation_item = %(rqi)s
|
AND sqi.parent = sq.name""",
|
||||||
AND sqi.parent = sq.name""",
|
{"supplier": supplier.supplier, "rqi": item.name}, as_dict=1)[0]
|
||||||
{"supplier": supplier.supplier, "rqi": item.name}, as_dict=1)[0]
|
if (sqi_count.count) == 0:
|
||||||
if (sqi_count.count) == 0:
|
quote_status = _('Pending')
|
||||||
quote_status = _('Pending')
|
supplier.quote_status = quote_status
|
||||||
supplier.quote_status = quote_status
|
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
@@ -289,16 +325,15 @@ def create_rfq_items(sq_doc, supplier, data):
|
|||||||
})
|
})
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_pdf(doctype, name, supplier_idx):
|
def get_pdf(doctype, name, supplier):
|
||||||
doc = get_rfq_doc(doctype, name, supplier_idx)
|
doc = get_rfq_doc(doctype, name, supplier)
|
||||||
if doc:
|
if doc:
|
||||||
download_pdf(doctype, name, doc=doc)
|
download_pdf(doctype, name, doc=doc)
|
||||||
|
|
||||||
def get_rfq_doc(doctype, name, supplier_idx):
|
def get_rfq_doc(doctype, name, supplier):
|
||||||
if cint(supplier_idx):
|
if supplier:
|
||||||
doc = frappe.get_doc(doctype, name)
|
doc = frappe.get_doc(doctype, name)
|
||||||
args = doc.get('suppliers')[cint(supplier_idx) - 1]
|
doc.update_supplier_part_no(supplier)
|
||||||
doc.update_supplier_part_no(args)
|
|
||||||
return doc
|
return doc
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
|
|||||||
@@ -25,14 +25,10 @@ class TestRequestforQuotation(unittest.TestCase):
|
|||||||
sq = make_supplier_quotation_from_rfq(rfq.name, for_supplier=rfq.get('suppliers')[0].supplier)
|
sq = make_supplier_quotation_from_rfq(rfq.name, for_supplier=rfq.get('suppliers')[0].supplier)
|
||||||
sq.submit()
|
sq.submit()
|
||||||
|
|
||||||
# No Quote first supplier quotation
|
|
||||||
rfq.get('suppliers')[1].no_quote = 1
|
|
||||||
rfq.get('suppliers')[1].quote_status = 'No Quote'
|
|
||||||
|
|
||||||
rfq.update_rfq_supplier_status() #rfq.get('suppliers')[1].supplier)
|
rfq.update_rfq_supplier_status() #rfq.get('suppliers')[1].supplier)
|
||||||
|
|
||||||
self.assertEqual(rfq.get('suppliers')[0].quote_status, 'Received')
|
self.assertEqual(rfq.get('suppliers')[0].quote_status, 'Received')
|
||||||
self.assertEqual(rfq.get('suppliers')[1].quote_status, 'No Quote')
|
self.assertEqual(rfq.get('suppliers')[1].quote_status, 'Pending')
|
||||||
|
|
||||||
def test_make_supplier_quotation(self):
|
def test_make_supplier_quotation(self):
|
||||||
rfq = make_request_for_quotation()
|
rfq = make_request_for_quotation()
|
||||||
|
|||||||
@@ -84,9 +84,6 @@ QUnit.test("Test: Request for Quotation", function (assert) {
|
|||||||
cur_frm.fields_dict.suppliers.grid.grid_rows[0].toggle_view();
|
cur_frm.fields_dict.suppliers.grid.grid_rows[0].toggle_view();
|
||||||
},
|
},
|
||||||
() => frappe.timeout(1),
|
() => frappe.timeout(1),
|
||||||
() => {
|
|
||||||
frappe.click_check('No Quote');
|
|
||||||
},
|
|
||||||
() => frappe.timeout(1),
|
() => frappe.timeout(1),
|
||||||
() => {
|
() => {
|
||||||
cur_frm.cur_grid.toggle_view();
|
cur_frm.cur_grid.toggle_view();
|
||||||
@@ -125,7 +122,6 @@ QUnit.test("Test: Request for Quotation", function (assert) {
|
|||||||
() => frappe.timeout(1),
|
() => frappe.timeout(1),
|
||||||
() => {
|
() => {
|
||||||
assert.ok(cur_frm.fields_dict.suppliers.grid.grid_rows[1].doc.quote_status == "Received");
|
assert.ok(cur_frm.fields_dict.suppliers.grid.grid_rows[1].doc.quote_status == "Received");
|
||||||
assert.ok(cur_frm.fields_dict.suppliers.grid.grid_rows[0].doc.no_quote == 1);
|
|
||||||
},
|
},
|
||||||
() => done()
|
() => done()
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -27,10 +27,11 @@
|
|||||||
"stock_qty",
|
"stock_qty",
|
||||||
"warehouse_and_reference",
|
"warehouse_and_reference",
|
||||||
"warehouse",
|
"warehouse",
|
||||||
"project_name",
|
|
||||||
"col_break4",
|
"col_break4",
|
||||||
"material_request",
|
"material_request",
|
||||||
"material_request_item",
|
"material_request_item",
|
||||||
|
"section_break_24",
|
||||||
|
"project_name",
|
||||||
"section_break_23",
|
"section_break_23",
|
||||||
"page_break"
|
"page_break"
|
||||||
],
|
],
|
||||||
@@ -161,7 +162,7 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "project_name",
|
"fieldname": "project_name",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Project Name",
|
"label": "Project",
|
||||||
"options": "Project",
|
"options": "Project",
|
||||||
"print_hide": 1
|
"print_hide": 1
|
||||||
},
|
},
|
||||||
@@ -249,11 +250,18 @@
|
|||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"collapsible": 1,
|
||||||
|
"fieldname": "section_break_24",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Accounting Dimensions"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-06-12 19:10:36.333441",
|
"modified": "2020-09-24 17:26:46.276934",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Buying",
|
"module": "Buying",
|
||||||
"name": "Request for Quotation Item",
|
"name": "Request for Quotation Item",
|
||||||
|
|||||||
@@ -1,362 +1,99 @@
|
|||||||
{
|
{
|
||||||
"allow_copy": 0,
|
"actions": [],
|
||||||
"allow_guest_to_view": 0,
|
|
||||||
"allow_import": 0,
|
|
||||||
"allow_rename": 0,
|
|
||||||
"beta": 0,
|
|
||||||
"creation": "2016-03-29 05:59:11.896885",
|
"creation": "2016-03-29 05:59:11.896885",
|
||||||
"custom": 0,
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
"document_type": "",
|
|
||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"send_email",
|
||||||
|
"email_sent",
|
||||||
|
"supplier",
|
||||||
|
"contact",
|
||||||
|
"quote_status",
|
||||||
|
"column_break_3",
|
||||||
|
"supplier_name",
|
||||||
|
"email_id"
|
||||||
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_on_submit": 1,
|
"allow_on_submit": 1,
|
||||||
"bold": 0,
|
"columns": 2,
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"default": "1",
|
"default": "1",
|
||||||
"fieldname": "send_email",
|
"fieldname": "send_email",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"hidden": 0,
|
"in_list_view": 1,
|
||||||
"ignore_user_permissions": 0,
|
"label": "Send Email"
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Send Email",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_on_submit": 1,
|
"allow_on_submit": 1,
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"default": "0",
|
"default": "0",
|
||||||
"depends_on": "eval:doc.docstatus >= 1",
|
"depends_on": "eval:doc.docstatus >= 1",
|
||||||
"fieldname": "email_sent",
|
"fieldname": "email_sent",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Email Sent",
|
"label": "Email Sent",
|
||||||
"length": 0,
|
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"permlevel": 0,
|
"read_only": 1
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 1,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
"columns": 2,
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 4,
|
|
||||||
"fieldname": "supplier",
|
"fieldname": "supplier",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Supplier",
|
"label": "Supplier",
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"options": "Supplier",
|
"options": "Supplier",
|
||||||
"permlevel": 0,
|
"reqd": 1
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 1,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
"allow_on_submit": 1,
|
||||||
"allow_on_submit": 0,
|
"columns": 2,
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 3,
|
|
||||||
"fieldname": "contact",
|
"fieldname": "contact",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Contact",
|
"label": "Contact",
|
||||||
"length": 0,
|
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"options": "Contact",
|
"options": "Contact"
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_on_submit": 1,
|
"allow_on_submit": 1,
|
||||||
"bold": 0,
|
"depends_on": "eval:doc.docstatus >= 1",
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"depends_on": "eval:doc.docstatus >= 1 && doc.quote_status != 'Received'",
|
|
||||||
"fieldname": "no_quote",
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "No Quote",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_on_submit": 1,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"depends_on": "eval:doc.docstatus >= 1 && !doc.no_quote",
|
|
||||||
"fieldname": "quote_status",
|
"fieldname": "quote_status",
|
||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Quote Status",
|
"label": "Quote Status",
|
||||||
"length": 0,
|
"options": "Pending\nReceived",
|
||||||
"no_copy": 0,
|
"read_only": 1
|
||||||
"options": "Pending\nReceived\nNo Quote",
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 1,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "column_break_3",
|
"fieldname": "column_break_3",
|
||||||
"fieldtype": "Column Break",
|
"fieldtype": "Column Break"
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 1,
|
"bold": 1,
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fetch_from": "supplier.supplier_name",
|
"fetch_from": "supplier.supplier_name",
|
||||||
"fieldname": "supplier_name",
|
"fieldname": "supplier_name",
|
||||||
"fieldtype": "Read Only",
|
"fieldtype": "Read Only",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 1,
|
"in_global_search": 1,
|
||||||
"in_list_view": 0,
|
"label": "Supplier Name"
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Supplier Name",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"options": "",
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 3,
|
"columns": 3,
|
||||||
"fetch_from": "contact.email_id",
|
"fetch_from": "contact.email_id",
|
||||||
"fieldname": "email_id",
|
"fieldname": "email_id",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Email Id",
|
"label": "Email Id",
|
||||||
"length": 0,
|
"no_copy": 1
|
||||||
"no_copy": 1,
|
|
||||||
"options": "",
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_on_submit": 1,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "download_pdf",
|
|
||||||
"fieldtype": "Button",
|
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Download PDF",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"has_web_view": 0,
|
"index_web_pages_for_search": 1,
|
||||||
"hide_heading": 0,
|
|
||||||
"hide_toolbar": 0,
|
|
||||||
"idx": 0,
|
|
||||||
"image_view": 0,
|
|
||||||
"in_create": 0,
|
|
||||||
"is_submittable": 0,
|
|
||||||
"issingle": 0,
|
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"max_attachments": 0,
|
"links": [],
|
||||||
"modified": "2018-05-16 22:43:30.212408",
|
"modified": "2020-10-16 12:23:41.769820",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Buying",
|
"module": "Buying",
|
||||||
"name": "Request for Quotation Supplier",
|
"name": "Request for Quotation Supplier",
|
||||||
"name_case": "",
|
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [],
|
"permissions": [],
|
||||||
"quick_entry": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"read_only_onload": 0,
|
|
||||||
"show_name_in_global_search": 0,
|
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"track_changes": 1,
|
"track_changes": 1
|
||||||
"track_seen": 0
|
|
||||||
}
|
}
|
||||||
@@ -91,12 +91,7 @@ class SupplierQuotation(BuyingController):
|
|||||||
for my_item in self.items) if include_me else 0
|
for my_item in self.items) if include_me else 0
|
||||||
if (sqi_count.count + self_count) == 0:
|
if (sqi_count.count + self_count) == 0:
|
||||||
quote_status = _('Pending')
|
quote_status = _('Pending')
|
||||||
if quote_status == _('Received') and doc_sup.quote_status == _('No Quote'):
|
|
||||||
frappe.msgprint(_("{0} indicates that {1} will not provide a quotation, but all items \
|
|
||||||
have been quoted. Updating the RFQ quote status.").format(doc.name, self.supplier))
|
|
||||||
frappe.db.set_value('Request for Quotation Supplier', doc_sup.name, 'quote_status', quote_status)
|
|
||||||
frappe.db.set_value('Request for Quotation Supplier', doc_sup.name, 'no_quote', 0)
|
|
||||||
elif doc_sup.quote_status != _('No Quote'):
|
|
||||||
frappe.db.set_value('Request for Quotation Supplier', doc_sup.name, 'quote_status', quote_status)
|
frappe.db.set_value('Request for Quotation Supplier', doc_sup.name, 'quote_status', quote_status)
|
||||||
|
|
||||||
def get_list_context(context=None):
|
def get_list_context(context=None):
|
||||||
|
|||||||
@@ -241,7 +241,7 @@
|
|||||||
"fieldname": "rate",
|
"fieldname": "rate",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Rate ",
|
"label": "Rate",
|
||||||
"oldfieldname": "import_rate",
|
"oldfieldname": "import_rate",
|
||||||
"oldfieldtype": "Currency",
|
"oldfieldtype": "Currency",
|
||||||
"options": "currency"
|
"options": "currency"
|
||||||
@@ -560,7 +560,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-10-01 16:34:39.703033",
|
"modified": "2020-10-19 12:36:26.913211",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Buying",
|
"module": "Buying",
|
||||||
"name": "Supplier Quotation Item",
|
"name": "Supplier Quotation Item",
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ def get_conditions(filters):
|
|||||||
conditions = ""
|
conditions = ""
|
||||||
|
|
||||||
if filters.get("company"):
|
if filters.get("company"):
|
||||||
conditions += " AND par.company=%s" % frappe.db.escape(filters.get('company'))
|
conditions += " AND parent.company=%s" % frappe.db.escape(filters.get('company'))
|
||||||
|
|
||||||
if filters.get("cost_center") or filters.get("project"):
|
if filters.get("cost_center") or filters.get("project"):
|
||||||
conditions += """
|
conditions += """
|
||||||
@@ -151,10 +151,10 @@ def get_conditions(filters):
|
|||||||
""" % (frappe.db.escape(filters.get('cost_center')), frappe.db.escape(filters.get('project')))
|
""" % (frappe.db.escape(filters.get('cost_center')), frappe.db.escape(filters.get('project')))
|
||||||
|
|
||||||
if filters.get("from_date"):
|
if filters.get("from_date"):
|
||||||
conditions += " AND par.transaction_date>='%s'" % filters.get('from_date')
|
conditions += " AND parent.transaction_date>='%s'" % filters.get('from_date')
|
||||||
|
|
||||||
if filters.get("to_date"):
|
if filters.get("to_date"):
|
||||||
conditions += " AND par.transaction_date<='%s'" % filters.get('to_date')
|
conditions += " AND parent.transaction_date<='%s'" % filters.get('to_date')
|
||||||
return conditions
|
return conditions
|
||||||
|
|
||||||
def get_data(filters):
|
def get_data(filters):
|
||||||
@@ -198,21 +198,23 @@ def get_mapped_mr_details(conditions):
|
|||||||
mr_records = {}
|
mr_records = {}
|
||||||
mr_details = frappe.db.sql("""
|
mr_details = frappe.db.sql("""
|
||||||
SELECT
|
SELECT
|
||||||
par.transaction_date,
|
parent.transaction_date,
|
||||||
par.per_ordered,
|
parent.per_ordered,
|
||||||
par.owner,
|
parent.owner,
|
||||||
child.name,
|
child.name,
|
||||||
child.parent,
|
child.parent,
|
||||||
child.amount,
|
child.amount,
|
||||||
child.qty,
|
child.qty,
|
||||||
child.item_code,
|
child.item_code,
|
||||||
child.uom,
|
child.uom,
|
||||||
par.status
|
parent.status,
|
||||||
FROM `tabMaterial Request` par, `tabMaterial Request Item` child
|
child.project,
|
||||||
|
child.cost_center
|
||||||
|
FROM `tabMaterial Request` parent, `tabMaterial Request Item` child
|
||||||
WHERE
|
WHERE
|
||||||
par.per_ordered>=0
|
parent.per_ordered>=0
|
||||||
AND par.name=child.parent
|
AND parent.name=child.parent
|
||||||
AND par.docstatus=1
|
AND parent.docstatus=1
|
||||||
{conditions}
|
{conditions}
|
||||||
""".format(conditions=conditions), as_dict=1) #nosec
|
""".format(conditions=conditions), as_dict=1) #nosec
|
||||||
|
|
||||||
@@ -232,7 +234,9 @@ def get_mapped_mr_details(conditions):
|
|||||||
status=record.status,
|
status=record.status,
|
||||||
actual_cost=0,
|
actual_cost=0,
|
||||||
purchase_order_amt=0,
|
purchase_order_amt=0,
|
||||||
purchase_order_amt_in_company_currency=0
|
purchase_order_amt_in_company_currency=0,
|
||||||
|
project = record.project,
|
||||||
|
cost_center = record.cost_center
|
||||||
)
|
)
|
||||||
procurement_record_against_mr.append(procurement_record_details)
|
procurement_record_against_mr.append(procurement_record_details)
|
||||||
return mr_records, procurement_record_against_mr
|
return mr_records, procurement_record_against_mr
|
||||||
@@ -280,16 +284,16 @@ def get_po_entries(conditions):
|
|||||||
child.amount,
|
child.amount,
|
||||||
child.base_amount,
|
child.base_amount,
|
||||||
child.schedule_date,
|
child.schedule_date,
|
||||||
par.transaction_date,
|
parent.transaction_date,
|
||||||
par.supplier,
|
parent.supplier,
|
||||||
par.status,
|
parent.status,
|
||||||
par.owner
|
parent.owner
|
||||||
FROM `tabPurchase Order` par, `tabPurchase Order Item` child
|
FROM `tabPurchase Order` parent, `tabPurchase Order Item` child
|
||||||
WHERE
|
WHERE
|
||||||
par.docstatus = 1
|
parent.docstatus = 1
|
||||||
AND par.name = child.parent
|
AND parent.name = child.parent
|
||||||
AND par.status not in ("Closed","Completed","Cancelled")
|
AND parent.status not in ("Closed","Completed","Cancelled")
|
||||||
{conditions}
|
{conditions}
|
||||||
GROUP BY
|
GROUP BY
|
||||||
par.name, child.item_code
|
parent.name, child.item_code
|
||||||
""".format(conditions=conditions), as_dict=1) #nosec
|
""".format(conditions=conditions), as_dict=1) #nosec
|
||||||
@@ -15,9 +15,9 @@ class CallLog(Document):
|
|||||||
number = strip_number(self.get('from'))
|
number = strip_number(self.get('from'))
|
||||||
self.contact = get_contact_with_phone_number(number)
|
self.contact = get_contact_with_phone_number(number)
|
||||||
self.lead = get_lead_with_phone_number(number)
|
self.lead = get_lead_with_phone_number(number)
|
||||||
|
if self.contact:
|
||||||
contact = frappe.get_doc("Contact", self.contact)
|
contact = frappe.get_doc("Contact", self.contact)
|
||||||
self.customer = contact.get_link_for("Customer")
|
self.customer = contact.get_link_for("Customer")
|
||||||
|
|
||||||
def after_insert(self):
|
def after_insert(self):
|
||||||
self.trigger_call_popup()
|
self.trigger_call_popup()
|
||||||
|
|||||||
@@ -63,8 +63,8 @@ def get_data():
|
|||||||
{
|
{
|
||||||
"type": "doctype",
|
"type": "doctype",
|
||||||
"name": "Chart of Accounts Importer",
|
"name": "Chart of Accounts Importer",
|
||||||
"labe": _("Chart Of Accounts Importer"),
|
"label": _("Chart of Accounts Importer"),
|
||||||
"description": _("Import Chart Of Accounts from CSV / Excel files"),
|
"description": _("Import Chart of Accounts from CSV / Excel files"),
|
||||||
"onboard": 1
|
"onboard": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -605,8 +605,6 @@ class AccountsController(TransactionBase):
|
|||||||
from erpnext.accounts.utils import unlink_ref_doc_from_payment_entries
|
from erpnext.accounts.utils import unlink_ref_doc_from_payment_entries
|
||||||
|
|
||||||
if self.doctype in ["Sales Invoice", "Purchase Invoice"]:
|
if self.doctype in ["Sales Invoice", "Purchase Invoice"]:
|
||||||
if self.is_return: return
|
|
||||||
|
|
||||||
if frappe.db.get_single_value('Accounts Settings', 'unlink_payment_on_cancellation_of_invoice'):
|
if frappe.db.get_single_value('Accounts Settings', 'unlink_payment_on_cancellation_of_invoice'):
|
||||||
unlink_ref_doc_from_payment_entries(self)
|
unlink_ref_doc_from_payment_entries(self)
|
||||||
|
|
||||||
@@ -1170,6 +1168,31 @@ def set_child_tax_template_and_map(item, child_item, parent_doc):
|
|||||||
if child_item.get("item_tax_template"):
|
if child_item.get("item_tax_template"):
|
||||||
child_item.item_tax_rate = get_item_tax_map(parent_doc.get('company'), child_item.item_tax_template, as_json=True)
|
child_item.item_tax_rate = get_item_tax_map(parent_doc.get('company'), child_item.item_tax_template, as_json=True)
|
||||||
|
|
||||||
|
def add_taxes_from_tax_template(child_item, parent_doc):
|
||||||
|
add_taxes_from_item_tax_template = frappe.db.get_single_value("Accounts Settings", "add_taxes_from_item_tax_template")
|
||||||
|
|
||||||
|
if child_item.get("item_tax_rate") and add_taxes_from_item_tax_template:
|
||||||
|
tax_map = json.loads(child_item.get("item_tax_rate"))
|
||||||
|
for tax_type in tax_map:
|
||||||
|
tax_rate = flt(tax_map[tax_type])
|
||||||
|
taxes = parent_doc.get('taxes') or []
|
||||||
|
# add new row for tax head only if missing
|
||||||
|
found = any(tax.account_head == tax_type for tax in taxes)
|
||||||
|
if not found:
|
||||||
|
tax_row = parent_doc.append("taxes", {})
|
||||||
|
tax_row.update({
|
||||||
|
"description" : str(tax_type).split(' - ')[0],
|
||||||
|
"charge_type" : "On Net Total",
|
||||||
|
"account_head" : tax_type,
|
||||||
|
"rate" : tax_rate
|
||||||
|
})
|
||||||
|
if parent_doc.doctype == "Purchase Order":
|
||||||
|
tax_row.update({
|
||||||
|
"category" : "Total",
|
||||||
|
"add_deduct_tax" : "Add"
|
||||||
|
})
|
||||||
|
tax_row.db_insert()
|
||||||
|
|
||||||
def set_sales_order_defaults(parent_doctype, parent_doctype_name, child_docname, trans_item):
|
def set_sales_order_defaults(parent_doctype, parent_doctype_name, child_docname, trans_item):
|
||||||
"""
|
"""
|
||||||
Returns a Sales Order Item child item containing the default values
|
Returns a Sales Order Item child item containing the default values
|
||||||
@@ -1185,6 +1208,7 @@ def set_sales_order_defaults(parent_doctype, parent_doctype_name, child_docname,
|
|||||||
conversion_factor = flt(get_conversion_factor(item.item_code, child_item.uom).get("conversion_factor"))
|
conversion_factor = flt(get_conversion_factor(item.item_code, child_item.uom).get("conversion_factor"))
|
||||||
child_item.conversion_factor = flt(trans_item.get('conversion_factor')) or conversion_factor
|
child_item.conversion_factor = flt(trans_item.get('conversion_factor')) or conversion_factor
|
||||||
set_child_tax_template_and_map(item, child_item, p_doc)
|
set_child_tax_template_and_map(item, child_item, p_doc)
|
||||||
|
add_taxes_from_tax_template(child_item, p_doc)
|
||||||
child_item.warehouse = get_item_warehouse(item, p_doc, overwrite_warehouse=True)
|
child_item.warehouse = get_item_warehouse(item, p_doc, overwrite_warehouse=True)
|
||||||
if not child_item.warehouse:
|
if not child_item.warehouse:
|
||||||
frappe.throw(_("Cannot find {} for item {}. Please set the same in Item Master or Stock Settings.")
|
frappe.throw(_("Cannot find {} for item {}. Please set the same in Item Master or Stock Settings.")
|
||||||
@@ -1209,6 +1233,7 @@ def set_purchase_order_defaults(parent_doctype, parent_doctype_name, child_docna
|
|||||||
child_item.base_rate = 1 # Initiallize value will update in parent validation
|
child_item.base_rate = 1 # Initiallize value will update in parent validation
|
||||||
child_item.base_amount = 1 # Initiallize value will update in parent validation
|
child_item.base_amount = 1 # Initiallize value will update in parent validation
|
||||||
set_child_tax_template_and_map(item, child_item, p_doc)
|
set_child_tax_template_and_map(item, child_item, p_doc)
|
||||||
|
add_taxes_from_tax_template(child_item, p_doc)
|
||||||
return child_item
|
return child_item
|
||||||
|
|
||||||
def validate_and_delete_children(parent, data):
|
def validate_and_delete_children(parent, data):
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ class StockController(AccountsController):
|
|||||||
frappe.throw(_("Row #{0}: Serial No {1} does not belong to Batch {2}")
|
frappe.throw(_("Row #{0}: Serial No {1} does not belong to Batch {2}")
|
||||||
.format(d.idx, serial_no_data.name, d.batch_no))
|
.format(d.idx, serial_no_data.name, d.batch_no))
|
||||||
|
|
||||||
if d.qty > 0 and d.get("batch_no") and self.get("posting_date") and self.docstatus < 2:
|
if flt(d.qty) > 0.0 and d.get("batch_no") and self.get("posting_date") and self.docstatus < 2:
|
||||||
expiry_date = frappe.get_cached_value("Batch", d.get("batch_no"), "expiry_date")
|
expiry_date = frappe.get_cached_value("Batch", d.get("batch_no"), "expiry_date")
|
||||||
|
|
||||||
if expiry_date and getdate(expiry_date) < getdate(self.posting_date):
|
if expiry_date and getdate(expiry_date) < getdate(self.posting_date):
|
||||||
|
|||||||
@@ -241,6 +241,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "eval: doc.__islocal",
|
"depends_on": "eval: doc.__islocal",
|
||||||
|
"description": "Home, Work, etc.",
|
||||||
"fieldname": "address_title",
|
"fieldname": "address_title",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"label": "Address Title"
|
"label": "Address Title"
|
||||||
@@ -249,7 +250,8 @@
|
|||||||
"depends_on": "eval: doc.__islocal",
|
"depends_on": "eval: doc.__islocal",
|
||||||
"fieldname": "address_line1",
|
"fieldname": "address_line1",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"label": "Address Line 1"
|
"label": "Address Line 1",
|
||||||
|
"mandatory_depends_on": "eval: doc.address_title && doc.address_type"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "eval: doc.__islocal",
|
"depends_on": "eval: doc.__islocal",
|
||||||
@@ -261,7 +263,8 @@
|
|||||||
"depends_on": "eval: doc.__islocal",
|
"depends_on": "eval: doc.__islocal",
|
||||||
"fieldname": "city",
|
"fieldname": "city",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"label": "City/Town"
|
"label": "City/Town",
|
||||||
|
"mandatory_depends_on": "eval: doc.address_title && doc.address_type"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "eval: doc.__islocal",
|
"depends_on": "eval: doc.__islocal",
|
||||||
@@ -280,6 +283,7 @@
|
|||||||
"fieldname": "country",
|
"fieldname": "country",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Country",
|
"label": "Country",
|
||||||
|
"mandatory_depends_on": "eval: doc.address_title && doc.address_type",
|
||||||
"options": "Country"
|
"options": "Country"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -449,7 +453,7 @@
|
|||||||
"idx": 5,
|
"idx": 5,
|
||||||
"image_field": "image",
|
"image_field": "image",
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-06-18 14:39:41.835416",
|
"modified": "2020-10-13 15:24:00.094811",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "CRM",
|
"module": "CRM",
|
||||||
"name": "Lead",
|
"name": "Lead",
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ class Lead(SellingController):
|
|||||||
load_address_and_contact(self)
|
load_address_and_contact(self)
|
||||||
|
|
||||||
def before_insert(self):
|
def before_insert(self):
|
||||||
self.address_doc = self.create_address()
|
if self.address_title and self.address_type:
|
||||||
|
self.address_doc = self.create_address()
|
||||||
self.contact_doc = self.create_contact()
|
self.contact_doc = self.create_contact()
|
||||||
|
|
||||||
def after_insert(self):
|
def after_insert(self):
|
||||||
@@ -133,15 +134,6 @@ class Lead(SellingController):
|
|||||||
# skipping country since the system auto-sets it from system defaults
|
# skipping country since the system auto-sets it from system defaults
|
||||||
address = frappe.new_doc("Address")
|
address = frappe.new_doc("Address")
|
||||||
|
|
||||||
mandatory_fields = [ df.fieldname for df in address.meta.fields if df.reqd ]
|
|
||||||
|
|
||||||
if not all([self.get(field) for field in mandatory_fields]):
|
|
||||||
frappe.msgprint(_('Missing mandatory fields in address. \
|
|
||||||
{0} to create address' ).format("<a href='desk#Form/Address/New Address 1' \
|
|
||||||
> Click here </a>"),
|
|
||||||
alert=True, indicator='yellow')
|
|
||||||
return
|
|
||||||
|
|
||||||
address.update({addr_field: self.get(addr_field) for addr_field in address_fields})
|
address.update({addr_field: self.get(addr_field) for addr_field in address_fields})
|
||||||
address.update({info_field: self.get(info_field) for info_field in info_fields})
|
address.update({info_field: self.get(info_field) for info_field in info_fields})
|
||||||
address.insert()
|
address.insert()
|
||||||
@@ -190,7 +182,7 @@ class Lead(SellingController):
|
|||||||
|
|
||||||
def update_links(self):
|
def update_links(self):
|
||||||
# update address links
|
# update address links
|
||||||
if self.address_doc:
|
if hasattr(self, 'address_doc'):
|
||||||
self.address_doc.append("links", {
|
self.address_doc.append("links", {
|
||||||
"link_doctype": "Lead",
|
"link_doctype": "Lead",
|
||||||
"link_name": self.name,
|
"link_name": self.name,
|
||||||
|
|||||||
@@ -49,6 +49,22 @@ data = {
|
|||||||
'fieldname': 'reference_dn', 'label': 'Reference Name', 'fieldtype': 'Dynamic Link', 'options': 'reference_dt',
|
'fieldname': 'reference_dn', 'label': 'Reference Name', 'fieldtype': 'Dynamic Link', 'options': 'reference_dt',
|
||||||
'insert_after': 'reference_dt'
|
'insert_after': 'reference_dt'
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
'Stock Entry': [
|
||||||
|
{
|
||||||
|
'fieldname': 'inpatient_medication_entry', 'label': 'Inpatient Medication Entry', 'fieldtype': 'Link', 'options': 'Inpatient Medication Entry',
|
||||||
|
'insert_after': 'credit_note', 'read_only': True
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'Stock Entry Detail': [
|
||||||
|
{
|
||||||
|
'fieldname': 'patient', 'label': 'Patient', 'fieldtype': 'Link', 'options': 'Patient',
|
||||||
|
'insert_after': 'po_detail', 'read_only': True
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'fieldname': 'inpatient_medication_entry_child', 'label': 'Inpatient Medication Entry Child', 'fieldtype': 'Data',
|
||||||
|
'insert_after': 'patient', 'read_only': True
|
||||||
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
'on_setup': 'erpnext.healthcare.setup.setup_healthcare'
|
'on_setup': 'erpnext.healthcare.setup.setup_healthcare'
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import frappe
|
|||||||
import json
|
import json
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.model.mapper import get_mapped_doc
|
from frappe.model.mapper import get_mapped_doc
|
||||||
from frappe.utils import flt, cstr
|
from frappe.utils import flt, cstr, getdate
|
||||||
from frappe.email.doctype.email_group.email_group import add_subscribers
|
from frappe.email.doctype.email_group.email_group import add_subscribers
|
||||||
|
|
||||||
def get_course(program):
|
def get_course(program):
|
||||||
@@ -67,6 +67,13 @@ def mark_attendance(students_present, students_absent, course_schedule=None, stu
|
|||||||
:param date: Date.
|
:param date: Date.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
if student_group:
|
||||||
|
academic_year = frappe.db.get_value('Student Group', student_group, 'academic_year')
|
||||||
|
if academic_year:
|
||||||
|
year_start_date, year_end_date = frappe.db.get_value('Academic Year', academic_year, ['year_start_date', 'year_end_date'])
|
||||||
|
if getdate(date) < getdate(year_start_date) or getdate(date) > getdate(year_end_date):
|
||||||
|
frappe.throw(_('Attendance cannot be marked outside of Academic Year {0}').format(academic_year))
|
||||||
|
|
||||||
present = json.loads(students_present)
|
present = json.loads(students_present)
|
||||||
absent = json.loads(students_absent)
|
absent = json.loads(students_absent)
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,23 @@ frappe.ui.form.on('Assessment Plan', {
|
|||||||
frappe.set_route('Form', 'Assessment Result Tool');
|
frappe.set_route('Form', 'Assessment Result Tool');
|
||||||
}, __('Tools'));
|
}, __('Tools'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
frm.set_query('course', function() {
|
||||||
|
return {
|
||||||
|
query: 'erpnext.education.doctype.program_enrollment.program_enrollment.get_program_courses',
|
||||||
|
filters: {
|
||||||
|
'program': frm.doc.program
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
frm.set_query('academic_term', function() {
|
||||||
|
return {
|
||||||
|
filters: {
|
||||||
|
'academic_year': frm.doc.academic_year
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
course: function(frm) {
|
course: function(frm) {
|
||||||
|
|||||||
@@ -12,8 +12,8 @@
|
|||||||
"assessment_group",
|
"assessment_group",
|
||||||
"grading_scale",
|
"grading_scale",
|
||||||
"column_break_2",
|
"column_break_2",
|
||||||
"course",
|
|
||||||
"program",
|
"program",
|
||||||
|
"course",
|
||||||
"academic_year",
|
"academic_year",
|
||||||
"academic_term",
|
"academic_term",
|
||||||
"section_break_5",
|
"section_break_5",
|
||||||
@@ -198,7 +198,7 @@
|
|||||||
],
|
],
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-05-09 14:56:26.746988",
|
"modified": "2020-10-23 15:55:35.076251",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Education",
|
"module": "Education",
|
||||||
"name": "Assessment Plan",
|
"name": "Assessment Plan",
|
||||||
|
|||||||
@@ -7,6 +7,23 @@ frappe.ui.form.on('Assessment Result', {
|
|||||||
frm.trigger('setup_chart');
|
frm.trigger('setup_chart');
|
||||||
}
|
}
|
||||||
frm.set_df_property('details', 'read_only', 1);
|
frm.set_df_property('details', 'read_only', 1);
|
||||||
|
|
||||||
|
frm.set_query('course', function() {
|
||||||
|
return {
|
||||||
|
query: 'erpnext.education.doctype.program_enrollment.program_enrollment.get_program_courses',
|
||||||
|
filters: {
|
||||||
|
'program': frm.doc.program
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
frm.set_query('academic_term', function() {
|
||||||
|
return {
|
||||||
|
filters: {
|
||||||
|
'academic_year': frm.doc.academic_year
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
onload: function(frm) {
|
onload: function(frm) {
|
||||||
|
|||||||
@@ -6,9 +6,13 @@ from __future__ import unicode_literals
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
|
from frappe.utils import get_link_to_form
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
|
|
||||||
class CourseEnrollment(Document):
|
class CourseEnrollment(Document):
|
||||||
|
def validate(self):
|
||||||
|
self.validate_duplication()
|
||||||
|
|
||||||
def get_progress(self, student):
|
def get_progress(self, student):
|
||||||
"""
|
"""
|
||||||
Returns Progress of given student for a particular course enrollment
|
Returns Progress of given student for a particular course enrollment
|
||||||
@@ -27,13 +31,15 @@ class CourseEnrollment(Document):
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
def validate_duplication(self):
|
def validate_duplication(self):
|
||||||
enrollment = frappe.get_all("Course Enrollment", filters={
|
enrollment = frappe.db.exists("Course Enrollment", {
|
||||||
"student": self.student,
|
"student": self.student,
|
||||||
"course": self.course,
|
"course": self.course,
|
||||||
"program_enrollment": self.program_enrollment
|
"program_enrollment": self.program_enrollment,
|
||||||
|
"name": ("!=", self.name)
|
||||||
})
|
})
|
||||||
if enrollment:
|
if enrollment:
|
||||||
frappe.throw(_("Student is already enrolled."))
|
frappe.throw(_("Student is already enrolled via Course Enrollment {0}").format(
|
||||||
|
get_link_to_form("Course Enrollment", enrollment)), title=_('Duplicate Entry'))
|
||||||
|
|
||||||
def add_quiz_activity(self, quiz_name, quiz_response, answers, score, status):
|
def add_quiz_activity(self, quiz_name, quiz_response, answers, score, status):
|
||||||
result = {k: ('Correct' if v else 'Wrong') for k,v in answers.items()}
|
result = {k: ('Correct' if v else 'Wrong') for k,v in answers.items()}
|
||||||
|
|||||||
@@ -17,8 +17,9 @@ class TestCourseEnrollment(unittest.TestCase):
|
|||||||
setup_program()
|
setup_program()
|
||||||
student = create_student({"first_name": "_Test First", "last_name": "_Test Last", "email": "_test_student_1@example.com"})
|
student = create_student({"first_name": "_Test First", "last_name": "_Test Last", "email": "_test_student_1@example.com"})
|
||||||
program_enrollment = student.enroll_in_program("_Test Program")
|
program_enrollment = student.enroll_in_program("_Test Program")
|
||||||
course_enrollment = student.enroll_in_course("_Test Course 1", program_enrollment.name)
|
course_enrollment = frappe.db.get_value("Course Enrollment",
|
||||||
make_course_activity(course_enrollment.name, "Article", "_Test Article 1-1")
|
{"course": "_Test Course 1", "student": student.name, "program_enrollment": program_enrollment.name}, 'name')
|
||||||
|
make_course_activity(course_enrollment, "Article", "_Test Article 1-1")
|
||||||
|
|
||||||
def test_get_progress(self):
|
def test_get_progress(self):
|
||||||
student = get_student("_test_student_1@example.com")
|
student = get_student("_test_student_1@example.com")
|
||||||
@@ -30,5 +31,14 @@ class TestCourseEnrollment(unittest.TestCase):
|
|||||||
self.assertTrue(finished in progress)
|
self.assertTrue(finished in progress)
|
||||||
frappe.db.rollback()
|
frappe.db.rollback()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
for entry in frappe.db.get_all("Course Enrollment"):
|
||||||
|
frappe.delete_doc("Course Enrollment", entry.name)
|
||||||
|
|
||||||
|
for entry in frappe.db.get_all("Program Enrollment"):
|
||||||
|
doc = frappe.get_doc("Program Enrollment", entry.name)
|
||||||
|
doc.cancel()
|
||||||
|
doc.delete()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -41,5 +41,24 @@ frappe.ui.form.on("Instructor", {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
frm.set_query("academic_term", "instructor_log", function(_doc, cdt, cdn) {
|
||||||
|
let d = locals[cdt][cdn];
|
||||||
|
return {
|
||||||
|
filters: {
|
||||||
|
"academic_year": d.academic_year
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
frm.set_query("course", "instructor_log", function(_doc, cdt, cdn) {
|
||||||
|
let d = locals[cdt][cdn];
|
||||||
|
return {
|
||||||
|
query: "erpnext.education.doctype.program_enrollment.program_enrollment.get_program_courses",
|
||||||
|
filters: {
|
||||||
|
"program": d.program
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -1,336 +1,88 @@
|
|||||||
{
|
{
|
||||||
"allow_copy": 0,
|
"actions": [],
|
||||||
"allow_events_in_timeline": 0,
|
|
||||||
"allow_guest_to_view": 0,
|
|
||||||
"allow_import": 0,
|
|
||||||
"allow_rename": 0,
|
|
||||||
"beta": 0,
|
|
||||||
"creation": "2017-12-27 08:55:52.680284",
|
"creation": "2017-12-27 08:55:52.680284",
|
||||||
"custom": 0,
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
"document_type": "",
|
|
||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"academic_year",
|
||||||
|
"academic_term",
|
||||||
|
"department",
|
||||||
|
"column_break_3",
|
||||||
|
"program",
|
||||||
|
"course",
|
||||||
|
"student_group",
|
||||||
|
"section_break_8",
|
||||||
|
"other_details"
|
||||||
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "academic_year",
|
"fieldname": "academic_year",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Academic Year",
|
"label": "Academic Year",
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"options": "Academic Year",
|
"options": "Academic Year",
|
||||||
"permlevel": 0,
|
"reqd": 1
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "academic_term",
|
"fieldname": "academic_term",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Academic Term",
|
"label": "Academic Term",
|
||||||
"length": 0,
|
"options": "Academic Term"
|
||||||
"no_copy": 0,
|
|
||||||
"options": "Academic Term",
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "department",
|
"fieldname": "department",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Department",
|
"label": "Department",
|
||||||
"length": 0,
|
"options": "Department"
|
||||||
"no_copy": 0,
|
|
||||||
"options": "Department",
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "column_break_3",
|
"fieldname": "column_break_3",
|
||||||
"fieldtype": "Column Break",
|
"fieldtype": "Column Break"
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "program",
|
"fieldname": "program",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Program",
|
"label": "Program",
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"options": "Program",
|
"options": "Program",
|
||||||
"permlevel": 0,
|
"reqd": 1
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "course",
|
"fieldname": "course",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Course",
|
"label": "Course",
|
||||||
"length": 0,
|
"options": "Course"
|
||||||
"no_copy": 0,
|
|
||||||
"options": "Course",
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "student_group",
|
"fieldname": "student_group",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Student Group",
|
"label": "Student Group",
|
||||||
"length": 0,
|
"options": "Student Group"
|
||||||
"no_copy": 0,
|
|
||||||
"options": "Student Group",
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "section_break_8",
|
"fieldname": "section_break_8",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break"
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "other_details",
|
"fieldname": "other_details",
|
||||||
"fieldtype": "Small Text",
|
"fieldtype": "Small Text",
|
||||||
"hidden": 0,
|
"label": "Other details"
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Other details",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"has_web_view": 0,
|
|
||||||
"hide_heading": 0,
|
|
||||||
"hide_toolbar": 0,
|
|
||||||
"idx": 0,
|
|
||||||
"image_view": 0,
|
|
||||||
"in_create": 0,
|
|
||||||
"is_submittable": 0,
|
|
||||||
"issingle": 0,
|
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"max_attachments": 0,
|
"links": [],
|
||||||
"modified": "2018-11-04 03:38:30.902942",
|
"modified": "2020-10-23 15:15:50.759657",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Education",
|
"module": "Education",
|
||||||
"name": "Instructor Log",
|
"name": "Instructor Log",
|
||||||
"name_case": "",
|
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [],
|
"permissions": [],
|
||||||
"quick_entry": 1,
|
"quick_entry": 1,
|
||||||
"read_only": 0,
|
|
||||||
"read_only_onload": 0,
|
|
||||||
"restrict_to_domain": "Education",
|
"restrict_to_domain": "Education",
|
||||||
"show_name_in_global_search": 0,
|
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"track_changes": 1,
|
"track_changes": 1
|
||||||
"track_seen": 0,
|
|
||||||
"track_views": 0
|
|
||||||
}
|
}
|
||||||
@@ -49,6 +49,11 @@ class TestProgram(unittest.TestCase):
|
|||||||
self.assertEqual(course[1].name, "_Test Course 2")
|
self.assertEqual(course[1].name, "_Test Course 2")
|
||||||
frappe.db.rollback()
|
frappe.db.rollback()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
for dt in ["Program", "Course", "Topic", "Article"]:
|
||||||
|
for entry in frappe.get_all(dt):
|
||||||
|
frappe.delete_doc(dt, entry.program)
|
||||||
|
|
||||||
def make_program(name):
|
def make_program(name):
|
||||||
program = frappe.get_doc({
|
program = frappe.get_doc({
|
||||||
"doctype": "Program",
|
"doctype": "Program",
|
||||||
@@ -68,7 +73,7 @@ def make_program_and_linked_courses(program_name, course_name_list):
|
|||||||
program = frappe.get_doc("Program", program_name)
|
program = frappe.get_doc("Program", program_name)
|
||||||
course_list = [make_course(course_name) for course_name in course_name_list]
|
course_list = [make_course(course_name) for course_name in course_name_list]
|
||||||
for course in course_list:
|
for course in course_list:
|
||||||
program.append("courses", {"course": course})
|
program.append("courses", {"course": course, "required": 1})
|
||||||
program.save()
|
program.save()
|
||||||
return program
|
return program
|
||||||
|
|
||||||
|
|||||||
@@ -17,9 +17,7 @@
|
|||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Course",
|
"label": "Course",
|
||||||
"options": "Course",
|
"options": "Course",
|
||||||
"reqd": 1,
|
"reqd": 1
|
||||||
"show_days": 1,
|
|
||||||
"show_seconds": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fetch_from": "course.course_name",
|
"fetch_from": "course.course_name",
|
||||||
@@ -27,23 +25,19 @@
|
|||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Course Name",
|
"label": "Course Name",
|
||||||
"read_only": 1,
|
"read_only": 1
|
||||||
"show_days": 1,
|
|
||||||
"show_seconds": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "1",
|
||||||
"fieldname": "required",
|
"fieldname": "required",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Mandatory",
|
"label": "Mandatory"
|
||||||
"show_days": 1,
|
|
||||||
"show_seconds": 1
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-06-09 18:56:10.213241",
|
"modified": "2020-09-15 18:14:22.816795",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Education",
|
"module": "Education",
|
||||||
"name": "Program Course",
|
"name": "Program Course",
|
||||||
|
|||||||
@@ -2,16 +2,24 @@
|
|||||||
// For license information, please see license.txt
|
// For license information, please see license.txt
|
||||||
|
|
||||||
|
|
||||||
frappe.ui.form.on("Program Enrollment", {
|
frappe.ui.form.on('Program Enrollment', {
|
||||||
setup: function(frm) {
|
setup: function(frm) {
|
||||||
frm.add_fetch('fee_structure', 'total_amount', 'amount');
|
frm.add_fetch('fee_structure', 'total_amount', 'amount');
|
||||||
},
|
},
|
||||||
|
|
||||||
onload: function(frm, cdt, cdn){
|
onload: function(frm) {
|
||||||
frm.set_query("academic_term", "fees", function(){
|
frm.set_query('academic_term', function() {
|
||||||
return{
|
return {
|
||||||
"filters":{
|
'filters':{
|
||||||
"academic_year": (frm.doc.academic_year)
|
'academic_year': frm.doc.academic_year
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
frm.set_query('academic_term', 'fees', function() {
|
||||||
|
return {
|
||||||
|
'filters':{
|
||||||
|
'academic_year': frm.doc.academic_year
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -24,9 +32,9 @@ frappe.ui.form.on("Program Enrollment", {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (frm.doc.program) {
|
if (frm.doc.program) {
|
||||||
frm.set_query("course", "courses", function(doc, cdt, cdn) {
|
frm.set_query('course', 'courses', function() {
|
||||||
return{
|
return {
|
||||||
query: "erpnext.education.doctype.program_enrollment.program_enrollment.get_program_courses",
|
query: 'erpnext.education.doctype.program_enrollment.program_enrollment.get_program_courses',
|
||||||
filters: {
|
filters: {
|
||||||
'program': frm.doc.program
|
'program': frm.doc.program
|
||||||
}
|
}
|
||||||
@@ -34,9 +42,9 @@ frappe.ui.form.on("Program Enrollment", {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
frm.set_query("student", function() {
|
frm.set_query('student', function() {
|
||||||
return{
|
return{
|
||||||
query: "erpnext.education.doctype.program_enrollment.program_enrollment.get_students",
|
query: 'erpnext.education.doctype.program_enrollment.program_enrollment.get_students',
|
||||||
filters: {
|
filters: {
|
||||||
'academic_year': frm.doc.academic_year,
|
'academic_year': frm.doc.academic_year,
|
||||||
'academic_term': frm.doc.academic_term
|
'academic_term': frm.doc.academic_term
|
||||||
@@ -49,14 +57,14 @@ frappe.ui.form.on("Program Enrollment", {
|
|||||||
frm.events.get_courses(frm);
|
frm.events.get_courses(frm);
|
||||||
if (frm.doc.program) {
|
if (frm.doc.program) {
|
||||||
frappe.call({
|
frappe.call({
|
||||||
method: "erpnext.education.api.get_fee_schedule",
|
method: 'erpnext.education.api.get_fee_schedule',
|
||||||
args: {
|
args: {
|
||||||
"program": frm.doc.program,
|
'program': frm.doc.program,
|
||||||
"student_category": frm.doc.student_category
|
'student_category': frm.doc.student_category
|
||||||
},
|
},
|
||||||
callback: function(r) {
|
callback: function(r) {
|
||||||
if(r.message) {
|
if (r.message) {
|
||||||
frm.set_value("fees" ,r.message);
|
frm.set_value('fees' ,r.message);
|
||||||
frm.events.get_courses(frm);
|
frm.events.get_courses(frm);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -65,17 +73,17 @@ frappe.ui.form.on("Program Enrollment", {
|
|||||||
},
|
},
|
||||||
|
|
||||||
student_category: function() {
|
student_category: function() {
|
||||||
frappe.ui.form.trigger("Program Enrollment", "program");
|
frappe.ui.form.trigger('Program Enrollment', 'program');
|
||||||
},
|
},
|
||||||
|
|
||||||
get_courses: function(frm) {
|
get_courses: function(frm) {
|
||||||
frm.set_value("courses",[]);
|
frm.set_value('courses',[]);
|
||||||
frappe.call({
|
frappe.call({
|
||||||
method: "get_courses",
|
method: 'get_courses',
|
||||||
doc:frm.doc,
|
doc:frm.doc,
|
||||||
callback: function(r) {
|
callback: function(r) {
|
||||||
if(r.message) {
|
if (r.message) {
|
||||||
frm.set_value("courses", r.message);
|
frm.set_value('courses', r.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -84,10 +92,10 @@ frappe.ui.form.on("Program Enrollment", {
|
|||||||
|
|
||||||
frappe.ui.form.on('Program Enrollment Course', {
|
frappe.ui.form.on('Program Enrollment Course', {
|
||||||
courses_add: function(frm){
|
courses_add: function(frm){
|
||||||
frm.fields_dict['courses'].grid.get_field('course').get_query = function(doc){
|
frm.fields_dict['courses'].grid.get_field('course').get_query = function(doc) {
|
||||||
var course_list = [];
|
var course_list = [];
|
||||||
if(!doc.__islocal) course_list.push(doc.name);
|
if(!doc.__islocal) course_list.push(doc.name);
|
||||||
$.each(doc.courses, function(idx, val){
|
$.each(doc.courses, function(_idx, val) {
|
||||||
if (val.course) course_list.push(val.course);
|
if (val.course) course_list.push(val.course);
|
||||||
});
|
});
|
||||||
return { filters: [['Course', 'name', 'not in', course_list]] };
|
return { filters: [['Course', 'name', 'not in', course_list]] };
|
||||||
|
|||||||
@@ -1,725 +1,185 @@
|
|||||||
{
|
{
|
||||||
"allow_copy": 0,
|
"actions": [],
|
||||||
"allow_events_in_timeline": 0,
|
|
||||||
"allow_guest_to_view": 0,
|
|
||||||
"allow_import": 1,
|
"allow_import": 1,
|
||||||
"allow_rename": 0,
|
|
||||||
"autoname": "EDU-ENR-.YYYY.-.#####",
|
"autoname": "EDU-ENR-.YYYY.-.#####",
|
||||||
"beta": 0,
|
|
||||||
"creation": "2015-12-02 12:58:32.916080",
|
"creation": "2015-12-02 12:58:32.916080",
|
||||||
"custom": 0,
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
"document_type": "Document",
|
"document_type": "Document",
|
||||||
"editable_grid": 0,
|
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"student",
|
||||||
|
"student_name",
|
||||||
|
"student_category",
|
||||||
|
"student_batch_name",
|
||||||
|
"school_house",
|
||||||
|
"column_break_4",
|
||||||
|
"program",
|
||||||
|
"academic_year",
|
||||||
|
"academic_term",
|
||||||
|
"enrollment_date",
|
||||||
|
"boarding_student",
|
||||||
|
"enrolled_courses",
|
||||||
|
"courses",
|
||||||
|
"transportation",
|
||||||
|
"mode_of_transportation",
|
||||||
|
"column_break_13",
|
||||||
|
"vehicle_no",
|
||||||
|
"section_break_7",
|
||||||
|
"fees",
|
||||||
|
"amended_from",
|
||||||
|
"image"
|
||||||
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "student",
|
"fieldname": "student",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 1,
|
"in_global_search": 1,
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Student",
|
"label": "Student",
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"options": "Student",
|
"options": "Student",
|
||||||
"permlevel": 0,
|
"reqd": 1
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 1,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fetch_from": "student.title",
|
"fetch_from": "student.title",
|
||||||
"fieldname": "student_name",
|
"fieldname": "student_name",
|
||||||
"fieldtype": "Read Only",
|
"fieldtype": "Read Only",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 1,
|
"in_global_search": 1,
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Student Name",
|
"label": "Student Name",
|
||||||
"length": 0,
|
"read_only": 1
|
||||||
"no_copy": 0,
|
|
||||||
"options": "",
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 1,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "student_category",
|
"fieldname": "student_category",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Student Category",
|
"label": "Student Category",
|
||||||
"length": 0,
|
"options": "Student Category"
|
||||||
"no_copy": 0,
|
|
||||||
"options": "Student Category",
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 1,
|
"allow_on_submit": 1,
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "student_batch_name",
|
"fieldname": "student_batch_name",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 1,
|
"in_global_search": 1,
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Student Batch",
|
"label": "Student Batch",
|
||||||
"length": 0,
|
"options": "Student Batch Name"
|
||||||
"no_copy": 0,
|
|
||||||
"options": "Student Batch Name",
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 1,
|
"allow_on_submit": 1,
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "school_house",
|
"fieldname": "school_house",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "School House",
|
"label": "School House",
|
||||||
"length": 0,
|
"options": "School House"
|
||||||
"no_copy": 0,
|
|
||||||
"options": "School House",
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "column_break_4",
|
"fieldname": "column_break_4",
|
||||||
"fieldtype": "Column Break",
|
"fieldtype": "Column Break"
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "program",
|
"fieldname": "program",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 1,
|
"in_standard_filter": 1,
|
||||||
"label": "Program",
|
"label": "Program",
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"options": "Program",
|
"options": "Program",
|
||||||
"permlevel": 0,
|
"reqd": 1
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 1,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "academic_year",
|
"fieldname": "academic_year",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 1,
|
"in_standard_filter": 1,
|
||||||
"label": "Academic Year",
|
"label": "Academic Year",
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"options": "Academic Year",
|
"options": "Academic Year",
|
||||||
"permlevel": 0,
|
"reqd": 1
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 1,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "academic_term",
|
"fieldname": "academic_term",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Academic Term",
|
"label": "Academic Term",
|
||||||
"length": 0,
|
"options": "Academic Term"
|
||||||
"no_copy": 0,
|
|
||||||
"options": "Academic Term",
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"default": "Today",
|
"default": "Today",
|
||||||
"fieldname": "enrollment_date",
|
"fieldname": "enrollment_date",
|
||||||
"fieldtype": "Date",
|
"fieldtype": "Date",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Enrollment Date",
|
"label": "Enrollment Date",
|
||||||
"length": 0,
|
"reqd": 1
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 1,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"default": "0",
|
"default": "0",
|
||||||
"description": "Check this if the Student is residing at the Institute's Hostel.",
|
"description": "Check this if the Student is residing at the Institute's Hostel.",
|
||||||
"fieldname": "boarding_student",
|
"fieldname": "boarding_student",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"hidden": 0,
|
"label": "Boarding Student"
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Boarding Student",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"options": "",
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 1,
|
"collapsible": 1,
|
||||||
"collapsible_depends_on": "vehicle_no",
|
"collapsible_depends_on": "vehicle_no",
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "transportation",
|
"fieldname": "transportation",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"hidden": 0,
|
"label": "Transportation"
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Transportation",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 1,
|
"allow_on_submit": 1,
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "mode_of_transportation",
|
"fieldname": "mode_of_transportation",
|
||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Mode of Transportation",
|
"label": "Mode of Transportation",
|
||||||
"length": 0,
|
"options": "\nWalking\nInstitute's Bus\nPublic Transport\nSelf-Driving Vehicle\nPick/Drop by Guardian"
|
||||||
"no_copy": 0,
|
|
||||||
"options": "\nWalking\nInstitute's Bus\nPublic Transport\nSelf-Driving Vehicle\nPick/Drop by Guardian",
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "column_break_13",
|
"fieldname": "column_break_13",
|
||||||
"fieldtype": "Column Break",
|
"fieldtype": "Column Break"
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 1,
|
"allow_on_submit": 1,
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "vehicle_no",
|
"fieldname": "vehicle_no",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"hidden": 0,
|
"label": "Vehicle/Bus Number"
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Vehicle/Bus Number",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 1,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "enrolled_courses",
|
"fieldname": "enrolled_courses",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"hidden": 0,
|
"label": "Enrolled courses"
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Enrolled courses",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 1,
|
"allow_on_submit": 1,
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "courses",
|
"fieldname": "courses",
|
||||||
"fieldtype": "Table",
|
"fieldtype": "Table",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Courses",
|
"label": "Courses",
|
||||||
"length": 0,
|
"options": "Program Enrollment Course"
|
||||||
"no_copy": 0,
|
|
||||||
"options": "Program Enrollment Course",
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 1,
|
"collapsible": 1,
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "section_break_7",
|
"fieldname": "section_break_7",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"hidden": 0,
|
"label": "Fees"
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Fees",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "fees",
|
"fieldname": "fees",
|
||||||
"fieldtype": "Table",
|
"fieldtype": "Table",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Fees",
|
"label": "Fees",
|
||||||
"length": 0,
|
"options": "Program Fee"
|
||||||
"no_copy": 0,
|
|
||||||
"options": "Program Fee",
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0,
|
|
||||||
"width": ""
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "amended_from",
|
"fieldname": "amended_from",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Amended From",
|
"label": "Amended From",
|
||||||
"length": 0,
|
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"options": "Program Enrollment",
|
"options": "Program Enrollment",
|
||||||
"permlevel": 0,
|
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"print_hide_if_no_value": 0,
|
"read_only": 1
|
||||||
"read_only": 1,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "image",
|
"fieldname": "image",
|
||||||
"fieldtype": "Attach Image",
|
"fieldtype": "Attach Image",
|
||||||
"hidden": 1,
|
"hidden": 1,
|
||||||
"ignore_user_permissions": 0,
|
"label": "Image"
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Image",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"has_web_view": 0,
|
|
||||||
"hide_heading": 0,
|
|
||||||
"hide_toolbar": 0,
|
|
||||||
"idx": 0,
|
|
||||||
"image_field": "image",
|
"image_field": "image",
|
||||||
"image_view": 0,
|
|
||||||
"in_create": 0,
|
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"issingle": 0,
|
"links": [],
|
||||||
"istable": 0,
|
"modified": "2020-09-15 18:12:11.988565",
|
||||||
"max_attachments": 0,
|
|
||||||
"menu_index": 0,
|
|
||||||
"modified": "2018-11-07 21:13:06.502279",
|
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Education",
|
"module": "Education",
|
||||||
"name": "Program Enrollment",
|
"name": "Program Enrollment",
|
||||||
"name_case": "",
|
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
{
|
{
|
||||||
@@ -729,47 +189,30 @@
|
|||||||
"delete": 1,
|
"delete": 1,
|
||||||
"email": 1,
|
"email": 1,
|
||||||
"export": 1,
|
"export": 1,
|
||||||
"if_owner": 0,
|
|
||||||
"import": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"print": 1,
|
"print": 1,
|
||||||
"read": 1,
|
"read": 1,
|
||||||
"report": 1,
|
"report": 1,
|
||||||
"role": "Academics User",
|
"role": "Academics User",
|
||||||
"set_user_permissions": 0,
|
|
||||||
"share": 1,
|
"share": 1,
|
||||||
"submit": 1,
|
"submit": 1,
|
||||||
"write": 1
|
"write": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"amend": 0,
|
|
||||||
"cancel": 0,
|
|
||||||
"create": 1,
|
"create": 1,
|
||||||
"delete": 0,
|
|
||||||
"email": 1,
|
"email": 1,
|
||||||
"export": 1,
|
"export": 1,
|
||||||
"if_owner": 0,
|
|
||||||
"import": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"print": 1,
|
"print": 1,
|
||||||
"read": 1,
|
"read": 1,
|
||||||
"report": 1,
|
"report": 1,
|
||||||
"role": "LMS User",
|
"role": "LMS User",
|
||||||
"set_user_permissions": 0,
|
|
||||||
"share": 1,
|
"share": 1,
|
||||||
"submit": 1,
|
"submit": 1,
|
||||||
"write": 1
|
"write": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"quick_entry": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"read_only_onload": 0,
|
|
||||||
"restrict_to_domain": "Education",
|
"restrict_to_domain": "Education",
|
||||||
"show_name_in_global_search": 1,
|
"show_name_in_global_search": 1,
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"title_field": "student_name",
|
"title_field": "student_name"
|
||||||
"track_changes": 0,
|
|
||||||
"track_seen": 0,
|
|
||||||
"track_views": 0
|
|
||||||
}
|
}
|
||||||
@@ -7,12 +7,15 @@ import frappe
|
|||||||
from frappe import msgprint, _
|
from frappe import msgprint, _
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.desk.reportview import get_match_cond, get_filters_cond
|
from frappe.desk.reportview import get_match_cond, get_filters_cond
|
||||||
from frappe.utils import comma_and
|
from frappe.utils import comma_and, get_link_to_form, getdate
|
||||||
import erpnext.www.lms as lms
|
import erpnext.www.lms as lms
|
||||||
|
|
||||||
class ProgramEnrollment(Document):
|
class ProgramEnrollment(Document):
|
||||||
def validate(self):
|
def validate(self):
|
||||||
self.validate_duplication()
|
self.validate_duplication()
|
||||||
|
self.validate_academic_year()
|
||||||
|
if self.academic_term:
|
||||||
|
self.validate_academic_term()
|
||||||
if not self.student_name:
|
if not self.student_name:
|
||||||
self.student_name = frappe.db.get_value("Student", self.student, "title")
|
self.student_name = frappe.db.get_value("Student", self.student, "title")
|
||||||
if not self.courses:
|
if not self.courses:
|
||||||
@@ -23,11 +26,34 @@ class ProgramEnrollment(Document):
|
|||||||
self.make_fee_records()
|
self.make_fee_records()
|
||||||
self.create_course_enrollments()
|
self.create_course_enrollments()
|
||||||
|
|
||||||
|
def validate_academic_year(self):
|
||||||
|
start_date, end_date = frappe.db.get_value("Academic Year", self.academic_year, ["year_start_date", "year_end_date"])
|
||||||
|
if self.enrollment_date:
|
||||||
|
if start_date and getdate(self.enrollment_date) < getdate(start_date):
|
||||||
|
frappe.throw(_("Enrollment Date cannot be before the Start Date of the Academic Year {0}").format(
|
||||||
|
get_link_to_form("Academic Year", self.academic_year)))
|
||||||
|
|
||||||
|
if end_date and getdate(self.enrollment_date) > getdate(end_date):
|
||||||
|
frappe.throw(_("Enrollment Date cannot be after the End Date of the Academic Term {0}").format(
|
||||||
|
get_link_to_form("Academic Year", self.academic_year)))
|
||||||
|
|
||||||
|
def validate_academic_term(self):
|
||||||
|
start_date, end_date = frappe.db.get_value("Academic Term", self.academic_term, ["term_start_date", "term_end_date"])
|
||||||
|
if self.enrollment_date:
|
||||||
|
if start_date and getdate(self.enrollment_date) < getdate(start_date):
|
||||||
|
frappe.throw(_("Enrollment Date cannot be before the Start Date of the Academic Term {0}").format(
|
||||||
|
get_link_to_form("Academic Term", self.academic_term)))
|
||||||
|
|
||||||
|
if end_date and getdate(self.enrollment_date) > getdate(end_date):
|
||||||
|
frappe.throw(_("Enrollment Date cannot be after the End Date of the Academic Term {0}").format(
|
||||||
|
get_link_to_form("Academic Term", self.academic_term)))
|
||||||
|
|
||||||
def validate_duplication(self):
|
def validate_duplication(self):
|
||||||
enrollment = frappe.get_all("Program Enrollment", filters={
|
enrollment = frappe.get_all("Program Enrollment", filters={
|
||||||
"student": self.student,
|
"student": self.student,
|
||||||
"program": self.program,
|
"program": self.program,
|
||||||
"academic_year": self.academic_year,
|
"academic_year": self.academic_year,
|
||||||
|
"academic_term": self.academic_term,
|
||||||
"docstatus": ("<", 2),
|
"docstatus": ("<", 2),
|
||||||
"name": ("!=", self.name)
|
"name": ("!=", self.name)
|
||||||
})
|
})
|
||||||
@@ -70,10 +96,9 @@ class ProgramEnrollment(Document):
|
|||||||
|
|
||||||
def create_course_enrollments(self):
|
def create_course_enrollments(self):
|
||||||
student = frappe.get_doc("Student", self.student)
|
student = frappe.get_doc("Student", self.student)
|
||||||
program = frappe.get_doc("Program", self.program)
|
course_list = [course.course for course in self.courses]
|
||||||
course_list = [course.course for course in program.courses]
|
|
||||||
for course_name in course_list:
|
for course_name in course_list:
|
||||||
student.enroll_in_course(course_name=course_name, program_enrollment=self.name)
|
student.enroll_in_course(course_name=course_name, program_enrollment=self.name, enrollment_date=self.enrollment_date)
|
||||||
|
|
||||||
def get_all_course_enrollments(self):
|
def get_all_course_enrollments(self):
|
||||||
course_enrollment_names = frappe.get_list("Course Enrollment", filters={'program_enrollment': self.name})
|
course_enrollment_names = frappe.get_list("Course Enrollment", filters={'program_enrollment': self.name})
|
||||||
|
|||||||
@@ -24,3 +24,12 @@ class TestProgramEnrollment(unittest.TestCase):
|
|||||||
self.assertTrue("_Test Course 1" in course_enrollments.keys())
|
self.assertTrue("_Test Course 1" in course_enrollments.keys())
|
||||||
self.assertTrue("_Test Course 2" in course_enrollments.keys())
|
self.assertTrue("_Test Course 2" in course_enrollments.keys())
|
||||||
frappe.db.rollback()
|
frappe.db.rollback()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
for entry in frappe.db.get_all("Course Enrollment"):
|
||||||
|
frappe.delete_doc("Course Enrollment", entry.name)
|
||||||
|
|
||||||
|
for entry in frappe.db.get_all("Program Enrollment"):
|
||||||
|
doc = frappe.get_doc("Program Enrollment", entry.name)
|
||||||
|
doc.cancel()
|
||||||
|
doc.delete()
|
||||||
@@ -147,7 +147,7 @@ class Student(Document):
|
|||||||
enrollment.save(ignore_permissions=True)
|
enrollment.save(ignore_permissions=True)
|
||||||
except frappe.exceptions.ValidationError:
|
except frappe.exceptions.ValidationError:
|
||||||
enrollment_name = frappe.get_list("Course Enrollment", filters={"student": self.name, "course": course_name, "program_enrollment": program_enrollment})[0].name
|
enrollment_name = frappe.get_list("Course Enrollment", filters={"student": self.name, "course": course_name, "program_enrollment": program_enrollment})[0].name
|
||||||
return frappe.get_doc("Program Enrollment", enrollment_name)
|
return frappe.get_doc("Course Enrollment", enrollment_name)
|
||||||
else:
|
else:
|
||||||
return enrollment
|
return enrollment
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,16 @@ class TestStudent(unittest.TestCase):
|
|||||||
self.assertTrue("_Test Course 2" in course_enrollments.keys())
|
self.assertTrue("_Test Course 2" in course_enrollments.keys())
|
||||||
frappe.db.rollback()
|
frappe.db.rollback()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
for entry in frappe.db.get_all("Course Enrollment"):
|
||||||
|
frappe.delete_doc("Course Enrollment", entry.name)
|
||||||
|
|
||||||
|
for entry in frappe.db.get_all("Program Enrollment"):
|
||||||
|
doc = frappe.get_doc("Program Enrollment", entry.name)
|
||||||
|
doc.cancel()
|
||||||
|
doc.delete()
|
||||||
|
|
||||||
|
|
||||||
def create_student(student_dict):
|
def create_student(student_dict):
|
||||||
student = get_student(student_dict['email'])
|
student = get_student(student_dict['email'])
|
||||||
if not student:
|
if not student:
|
||||||
|
|||||||
@@ -51,12 +51,14 @@
|
|||||||
"fieldname": "admission_start_date",
|
"fieldname": "admission_start_date",
|
||||||
"fieldtype": "Date",
|
"fieldtype": "Date",
|
||||||
"label": "Admission Start Date",
|
"label": "Admission Start Date",
|
||||||
|
"mandatory_depends_on": "enable_admission_application",
|
||||||
"no_copy": 1
|
"no_copy": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "admission_end_date",
|
"fieldname": "admission_end_date",
|
||||||
"fieldtype": "Date",
|
"fieldtype": "Date",
|
||||||
"label": "Admission End Date",
|
"label": "Admission End Date",
|
||||||
|
"mandatory_depends_on": "enable_admission_application",
|
||||||
"no_copy": 1
|
"no_copy": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -83,6 +85,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
|
"depends_on": "published",
|
||||||
"fieldname": "enable_admission_application",
|
"fieldname": "enable_admission_application",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Enable Admission Application"
|
"label": "Enable Admission Application"
|
||||||
@@ -91,7 +94,7 @@
|
|||||||
"has_web_view": 1,
|
"has_web_view": 1,
|
||||||
"is_published_field": "published",
|
"is_published_field": "published",
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-06-15 20:18:38.591626",
|
"modified": "2020-09-18 00:14:54.615321",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Education",
|
"module": "Education",
|
||||||
"name": "Student Admission",
|
"name": "Student Admission",
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ class StudentAdmission(WebsiteGenerator):
|
|||||||
if not self.route: #pylint: disable=E0203
|
if not self.route: #pylint: disable=E0203
|
||||||
self.route = "admissions/" + "-".join(self.title.split(" "))
|
self.route = "admissions/" + "-".join(self.title.split(" "))
|
||||||
|
|
||||||
|
if self.enable_admission_application and not self.program_details:
|
||||||
|
frappe.throw(_("Please add programs to enable admission application."))
|
||||||
|
|
||||||
def get_context(self, context):
|
def get_context(self, context):
|
||||||
context.no_cache = 1
|
context.no_cache = 1
|
||||||
context.show_sidebar = True
|
context.show_sidebar = True
|
||||||
|
|||||||
@@ -43,31 +43,35 @@
|
|||||||
<thead>
|
<thead>
|
||||||
<tr class="active">
|
<tr class="active">
|
||||||
<th style="width: 90px">Program/Std.</th>
|
<th style="width: 90px">Program/Std.</th>
|
||||||
<th style="width: 170px">Minumum Age</th>
|
<th style="width: 180px">Description</th>
|
||||||
<th style="width: 170px">Maximum Age</th>
|
<th style="width: 100px">Minumum Age</th>
|
||||||
|
<th style="width: 100px">Maximum Age</th>
|
||||||
<th style="width: 100px">Application Fee</th>
|
<th style="width: 100px">Application Fee</th>
|
||||||
|
{%- if doc.enable_admission_application and frappe.utils.getdate(doc.admission_start_date) <= today -%}
|
||||||
|
<th style="width: 120px"></th>
|
||||||
|
{% endif %}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for row in program_details %}
|
{% for row in program_details %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ row.program }}</td>
|
<td>{{ row.program }}</td>
|
||||||
|
<td><div class="text-muted">{{ row.description if row.description else '' }}</div></td>
|
||||||
<td>{{ row.min_age }}</td>
|
<td>{{ row.min_age }}</td>
|
||||||
<td>{{ row.max_age }}</td>
|
<td>{{ row.max_age }}</td>
|
||||||
<td>{{ row.application_fee }}</td>
|
<td>{{ row.application_fee }}</td>
|
||||||
|
{%- if doc.enable_admission_application and frappe.utils.getdate(doc.admission_start_date) <= today -%}
|
||||||
|
<td>
|
||||||
|
<a class='btn btn-sm btn-primary' href='/student-applicant?new=1&student_admission={{doc.name}}&program={{row.program}}&academic_year={{academic_year}}'>
|
||||||
|
{{ _("Apply Now") }}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
{% endif %}
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{%- if doc.enable_admission_application -%}
|
|
||||||
<br>
|
|
||||||
<p>
|
|
||||||
<a class='btn btn-primary'
|
|
||||||
href='/student-applicant?new=1&student_admission={{doc.name}}'>
|
|
||||||
{{ _("Apply Now") }}</a>
|
|
||||||
</p>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<div class="web-list-item">
|
<div class="web-list-item transaction-list-item">
|
||||||
{% set today = frappe.utils.getdate(frappe.utils.nowdate()) %}
|
{% set today = frappe.utils.getdate(frappe.utils.nowdate()) %}
|
||||||
<a href = "{{ doc.route }}/">
|
<a href = "{{ doc.route }}/" class="no-underline">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-6 text-left small bold" style="margin-top: -3px;"">
|
<div class="col-sm-4 bold">
|
||||||
<span class="indicator
|
<span class="indicator
|
||||||
{% if frappe.utils.getdate(doc.admission_end_date) == today %}
|
{% if frappe.utils.getdate(doc.admission_end_date) == today %}
|
||||||
red
|
red
|
||||||
@@ -15,6 +15,14 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
">{{ doc.title }}</span>
|
">{{ doc.title }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-sm-2 small">
|
||||||
|
<span class="text-muted">
|
||||||
|
Academic Year
|
||||||
|
</span>
|
||||||
|
<div class="text-muted bold">
|
||||||
|
{{ doc.academic_year }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="col-sm-3 small">
|
<div class="col-sm-3 small">
|
||||||
<span class="text-muted">
|
<span class="text-muted">
|
||||||
Starts on
|
Starts on
|
||||||
@@ -27,7 +35,7 @@
|
|||||||
<span class="text-muted">
|
<span class="text-muted">
|
||||||
Ends on
|
Ends on
|
||||||
</span>
|
</span>
|
||||||
<div class="bold">
|
<div class=" text-muted bold">
|
||||||
{{ frappe.format_date(doc.admission_end_date) }}
|
{{ frappe.format_date(doc.admission_end_date) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
"program",
|
"program",
|
||||||
"min_age",
|
"min_age",
|
||||||
"max_age",
|
"max_age",
|
||||||
|
"description",
|
||||||
"column_break_4",
|
"column_break_4",
|
||||||
"application_fee",
|
"application_fee",
|
||||||
"applicant_naming_series"
|
"applicant_naming_series"
|
||||||
@@ -18,52 +19,47 @@
|
|||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Program",
|
"label": "Program",
|
||||||
"options": "Program",
|
"options": "Program"
|
||||||
"show_days": 1,
|
|
||||||
"show_seconds": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "column_break_4",
|
"fieldname": "column_break_4",
|
||||||
"fieldtype": "Column Break",
|
"fieldtype": "Column Break"
|
||||||
"show_days": 1,
|
|
||||||
"show_seconds": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "application_fee",
|
"fieldname": "application_fee",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Application Fee",
|
"label": "Application Fee"
|
||||||
"show_days": 1,
|
|
||||||
"show_seconds": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "applicant_naming_series",
|
"fieldname": "applicant_naming_series",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Naming Series (for Student Applicant)",
|
"label": "Naming Series (for Student Applicant)"
|
||||||
"show_days": 1,
|
|
||||||
"show_seconds": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "min_age",
|
"fieldname": "min_age",
|
||||||
"fieldtype": "Int",
|
"fieldtype": "Int",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Minimum Age",
|
"label": "Minimum Age"
|
||||||
"show_days": 1,
|
|
||||||
"show_seconds": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "max_age",
|
"fieldname": "max_age",
|
||||||
"fieldtype": "Int",
|
"fieldtype": "Int",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Maximum Age",
|
"label": "Maximum Age"
|
||||||
"show_days": 1,
|
},
|
||||||
"show_seconds": 1
|
{
|
||||||
|
"fetch_from": "program.description",
|
||||||
|
"fetch_if_empty": 1,
|
||||||
|
"fieldname": "description",
|
||||||
|
"fieldtype": "Small Text",
|
||||||
|
"label": "Description"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-06-10 23:06:30.037404",
|
"modified": "2020-10-05 13:03:42.005985",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Education",
|
"module": "Education",
|
||||||
"name": "Student Admission Program",
|
"name": "Student Admission Program",
|
||||||
|
|||||||
@@ -168,6 +168,7 @@
|
|||||||
"fieldname": "student_email_id",
|
"fieldname": "student_email_id",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"label": "Student Email Address",
|
"label": "Student Email Address",
|
||||||
|
"options": "Email",
|
||||||
"unique": 1
|
"unique": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -261,7 +262,7 @@
|
|||||||
"image_field": "image",
|
"image_field": "image",
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-09-07 19:31:30.063563",
|
"modified": "2020-10-05 13:59:45.631647",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Education",
|
"module": "Education",
|
||||||
"name": "Student Applicant",
|
"name": "Student Applicant",
|
||||||
|
|||||||
@@ -6,13 +6,13 @@ from __future__ import unicode_literals
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.utils import get_link_to_form
|
from frappe.utils import get_link_to_form, getdate
|
||||||
from erpnext.education.api import get_student_group_students
|
from erpnext.education.api import get_student_group_students
|
||||||
|
|
||||||
|
|
||||||
class StudentAttendance(Document):
|
class StudentAttendance(Document):
|
||||||
def validate(self):
|
def validate(self):
|
||||||
self.validate_mandatory()
|
self.validate_mandatory()
|
||||||
|
self.validate_date()
|
||||||
self.set_date()
|
self.set_date()
|
||||||
self.set_student_group()
|
self.set_student_group()
|
||||||
self.validate_student()
|
self.validate_student()
|
||||||
@@ -27,6 +27,18 @@ class StudentAttendance(Document):
|
|||||||
frappe.throw(_('{0} or {1} is mandatory').format(frappe.bold('Student Group'),
|
frappe.throw(_('{0} or {1} is mandatory').format(frappe.bold('Student Group'),
|
||||||
frappe.bold('Course Schedule')), title=_('Mandatory Fields'))
|
frappe.bold('Course Schedule')), title=_('Mandatory Fields'))
|
||||||
|
|
||||||
|
def validate_date(self):
|
||||||
|
if not self.leave_application and getdate(self.date) > getdate():
|
||||||
|
frappe.throw(_('Attendance cannot be marked for future dates.'))
|
||||||
|
|
||||||
|
if self.student_group:
|
||||||
|
academic_year = frappe.db.get_value('Student Group', self.student_group, 'academic_year')
|
||||||
|
if academic_year:
|
||||||
|
year_start_date, year_end_date = frappe.db.get_value('Academic Year', academic_year, ['year_start_date', 'year_end_date'])
|
||||||
|
if year_start_date and year_end_date:
|
||||||
|
if getdate(self.date) < getdate(year_start_date) or getdate(self.date) > getdate(year_end_date):
|
||||||
|
frappe.throw(_('Attendance cannot be marked outside of Academic Year {0}').format(academic_year))
|
||||||
|
|
||||||
def set_student_group(self):
|
def set_student_group(self):
|
||||||
if self.course_schedule:
|
if self.course_schedule:
|
||||||
self.student_group = frappe.db.get_value('Course Schedule', self.course_schedule, 'student_group')
|
self.student_group = frappe.db.get_value('Course Schedule', self.course_schedule, 'student_group')
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ frappe.ui.form.on('Student Attendance Tool', {
|
|||||||
},
|
},
|
||||||
|
|
||||||
date: function(frm) {
|
date: function(frm) {
|
||||||
|
if (frm.doc.date > frappe.datetime.get_today())
|
||||||
|
frappe.throw(__("Cannot mark attendance for future dates."));
|
||||||
frm.trigger("student_group");
|
frm.trigger("student_group");
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -133,8 +135,8 @@ education.StudentsEditor = Class.extend({
|
|||||||
return !stud.disabled && !stud.checked;
|
return !stud.disabled && !stud.checked;
|
||||||
});
|
});
|
||||||
|
|
||||||
frappe.confirm(__("Do you want to update attendance?<br>Present: {0}\
|
frappe.confirm(__("Do you want to update attendance? <br> Present: {0} <br> Absent: {1}",
|
||||||
<br>Absent: {1}", [students_present.length, students_absent.length]),
|
[students_present.length, students_absent.length]),
|
||||||
function() { //ifyes
|
function() { //ifyes
|
||||||
if(!frappe.request.ajax_count) {
|
if(!frappe.request.ajax_count) {
|
||||||
frappe.call({
|
frappe.call({
|
||||||
|
|||||||
@@ -1,333 +1,118 @@
|
|||||||
{
|
{
|
||||||
|
"actions": [],
|
||||||
"allow_copy": 1,
|
"allow_copy": 1,
|
||||||
"allow_guest_to_view": 0,
|
|
||||||
"allow_import": 0,
|
|
||||||
"allow_rename": 0,
|
|
||||||
"beta": 0,
|
|
||||||
"creation": "2016-11-16 17:12:46.437539",
|
"creation": "2016-11-16 17:12:46.437539",
|
||||||
"custom": 0,
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
"document_type": "",
|
|
||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"based_on",
|
||||||
|
"group_based_on",
|
||||||
|
"column_break_2",
|
||||||
|
"student_group",
|
||||||
|
"academic_year",
|
||||||
|
"academic_term",
|
||||||
|
"course_schedule",
|
||||||
|
"date",
|
||||||
|
"attendance",
|
||||||
|
"students_html"
|
||||||
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"default": "",
|
|
||||||
"fieldname": "based_on",
|
"fieldname": "based_on",
|
||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Based On",
|
"label": "Based On",
|
||||||
"length": 0,
|
"options": "Student Group\nCourse Schedule"
|
||||||
"no_copy": 0,
|
|
||||||
"options": "Student Group\nCourse Schedule",
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"default": "Batch",
|
"default": "Batch",
|
||||||
"depends_on": "eval:doc.based_on == \"Student Group\"",
|
"depends_on": "eval:doc.based_on == \"Student Group\"",
|
||||||
"fieldname": "group_based_on",
|
"fieldname": "group_based_on",
|
||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Group Based On",
|
"label": "Group Based On",
|
||||||
"length": 0,
|
"options": "Batch\nCourse\nActivity"
|
||||||
"no_copy": 0,
|
|
||||||
"options": "Batch\nCourse\nActivity",
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "column_break_2",
|
"fieldname": "column_break_2",
|
||||||
"fieldtype": "Column Break",
|
"fieldtype": "Column Break"
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"depends_on": "eval:doc.based_on ==\"Student Group\"",
|
"depends_on": "eval:doc.based_on ==\"Student Group\"",
|
||||||
"fieldname": "student_group",
|
"fieldname": "student_group",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Student Group",
|
"label": "Student Group",
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"options": "Student Group",
|
"options": "Student Group",
|
||||||
"permlevel": 0,
|
"reqd": 1
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 1,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"depends_on": "eval:doc.based_on ==\"Course Schedule\"",
|
"depends_on": "eval:doc.based_on ==\"Course Schedule\"",
|
||||||
"fieldname": "course_schedule",
|
"fieldname": "course_schedule",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Course Schedule",
|
"label": "Course Schedule",
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"options": "Course Schedule",
|
"options": "Course Schedule",
|
||||||
"permlevel": 0,
|
"reqd": 1
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 1,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"depends_on": "eval:doc.based_on ==\"Student Group\"",
|
"depends_on": "eval:doc.based_on ==\"Student Group\"",
|
||||||
"fieldname": "date",
|
"fieldname": "date",
|
||||||
"fieldtype": "Date",
|
"fieldtype": "Date",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Date",
|
"label": "Date",
|
||||||
"length": 0,
|
"reqd": 1
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 1,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"depends_on": "eval: (doc.course_schedule \n|| (doc.student_group && doc.date))",
|
"depends_on": "eval: (doc.course_schedule \n|| (doc.student_group && doc.date))",
|
||||||
"fieldname": "attendance",
|
"fieldname": "attendance",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"hidden": 0,
|
"label": "Attendance"
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Attendance",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "students_html",
|
"fieldname": "students_html",
|
||||||
"fieldtype": "HTML",
|
"fieldtype": "HTML",
|
||||||
"hidden": 0,
|
"label": "Students HTML"
|
||||||
"ignore_user_permissions": 0,
|
},
|
||||||
"ignore_xss_filter": 0,
|
{
|
||||||
"in_filter": 0,
|
"fetch_from": "student_group.academic_year",
|
||||||
"in_global_search": 0,
|
"fieldname": "academic_year",
|
||||||
"in_list_view": 0,
|
"fieldtype": "Link",
|
||||||
"in_standard_filter": 0,
|
"label": "Academic Year",
|
||||||
"label": "Students HTML",
|
"options": "Academic Year",
|
||||||
"length": 0,
|
"read_only": 1
|
||||||
"no_copy": 0,
|
},
|
||||||
"permlevel": 0,
|
{
|
||||||
"precision": "",
|
"fetch_from": "student_group.academic_term",
|
||||||
"print_hide": 0,
|
"fieldname": "academic_term",
|
||||||
"print_hide_if_no_value": 0,
|
"fieldtype": "Link",
|
||||||
"read_only": 0,
|
"label": "Academic Term",
|
||||||
"remember_last_selected_value": 0,
|
"options": "Academic Term",
|
||||||
"report_hide": 0,
|
"read_only": 1
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"unique": 0
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"has_web_view": 0,
|
|
||||||
"hide_heading": 1,
|
|
||||||
"hide_toolbar": 1,
|
"hide_toolbar": 1,
|
||||||
"idx": 0,
|
|
||||||
"image_view": 0,
|
|
||||||
"in_create": 0,
|
|
||||||
"is_submittable": 0,
|
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"istable": 0,
|
"links": [],
|
||||||
"max_attachments": 0,
|
"modified": "2020-10-23 17:52:28.078971",
|
||||||
"modified": "2017-11-10 18:55:36.168044",
|
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Education",
|
"module": "Education",
|
||||||
"name": "Student Attendance Tool",
|
"name": "Student Attendance Tool",
|
||||||
"name_case": "",
|
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
{
|
{
|
||||||
"amend": 0,
|
|
||||||
"apply_user_permissions": 0,
|
|
||||||
"cancel": 0,
|
|
||||||
"create": 1,
|
"create": 1,
|
||||||
"delete": 0,
|
|
||||||
"email": 0,
|
|
||||||
"export": 0,
|
|
||||||
"if_owner": 0,
|
|
||||||
"import": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"print": 0,
|
|
||||||
"read": 1,
|
"read": 1,
|
||||||
"report": 0,
|
|
||||||
"role": "Instructor",
|
"role": "Instructor",
|
||||||
"set_user_permissions": 0,
|
|
||||||
"share": 0,
|
|
||||||
"submit": 0,
|
|
||||||
"write": 1
|
"write": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"amend": 0,
|
|
||||||
"apply_user_permissions": 0,
|
|
||||||
"cancel": 0,
|
|
||||||
"create": 1,
|
"create": 1,
|
||||||
"delete": 0,
|
|
||||||
"email": 0,
|
|
||||||
"export": 0,
|
|
||||||
"if_owner": 0,
|
|
||||||
"import": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"print": 0,
|
|
||||||
"read": 1,
|
"read": 1,
|
||||||
"report": 0,
|
|
||||||
"role": "Academics User",
|
"role": "Academics User",
|
||||||
"set_user_permissions": 0,
|
|
||||||
"share": 0,
|
|
||||||
"submit": 0,
|
|
||||||
"write": 1
|
"write": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"quick_entry": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"read_only_onload": 0,
|
|
||||||
"restrict_to_domain": "Education",
|
"restrict_to_domain": "Education",
|
||||||
"show_name_in_global_search": 0,
|
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC"
|
||||||
"track_changes": 0,
|
|
||||||
"track_seen": 0
|
|
||||||
}
|
}
|
||||||
@@ -70,19 +70,7 @@
|
|||||||
"show_in_filter": 0
|
"show_in_filter": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_read_on_all_link_options": 0,
|
"allow_read_on_all_link_options": 1,
|
||||||
"fieldname": "image",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"hidden": 0,
|
|
||||||
"label": "Image",
|
|
||||||
"max_length": 0,
|
|
||||||
"max_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"show_in_filter": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"allow_read_on_all_link_options": 0,
|
|
||||||
"fieldname": "program",
|
"fieldname": "program",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"hidden": 0,
|
"hidden": 0,
|
||||||
@@ -95,7 +83,7 @@
|
|||||||
"show_in_filter": 0
|
"show_in_filter": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_read_on_all_link_options": 0,
|
"allow_read_on_all_link_options": 1,
|
||||||
"fieldname": "academic_year",
|
"fieldname": "academic_year",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"hidden": 0,
|
"hidden": 0,
|
||||||
@@ -107,6 +95,19 @@
|
|||||||
"reqd": 0,
|
"reqd": 0,
|
||||||
"show_in_filter": 0
|
"show_in_filter": 0
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"allow_read_on_all_link_options": 1,
|
||||||
|
"fieldname": "academic_term",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"hidden": 0,
|
||||||
|
"label": "Academic Term",
|
||||||
|
"max_length": 0,
|
||||||
|
"max_value": 0,
|
||||||
|
"options": "Academic Term",
|
||||||
|
"read_only": 0,
|
||||||
|
"reqd": 0,
|
||||||
|
"show_in_filter": 0
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"allow_read_on_all_link_options": 0,
|
"allow_read_on_all_link_options": 0,
|
||||||
"fieldname": "date_of_birth",
|
"fieldname": "date_of_birth",
|
||||||
@@ -119,6 +120,19 @@
|
|||||||
"reqd": 0,
|
"reqd": 0,
|
||||||
"show_in_filter": 0
|
"show_in_filter": 0
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"allow_read_on_all_link_options": 1,
|
||||||
|
"fieldname": "gender",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"hidden": 0,
|
||||||
|
"label": "Gender",
|
||||||
|
"max_length": 0,
|
||||||
|
"max_value": 0,
|
||||||
|
"options": "Gender",
|
||||||
|
"read_only": 0,
|
||||||
|
"reqd": 0,
|
||||||
|
"show_in_filter": 0
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"allow_read_on_all_link_options": 0,
|
"allow_read_on_all_link_options": 0,
|
||||||
"fieldname": "blood_group",
|
"fieldname": "blood_group",
|
||||||
@@ -141,7 +155,7 @@
|
|||||||
"max_length": 0,
|
"max_length": 0,
|
||||||
"max_value": 0,
|
"max_value": 0,
|
||||||
"read_only": 0,
|
"read_only": 0,
|
||||||
"reqd": 0,
|
"reqd": 1,
|
||||||
"show_in_filter": 0
|
"show_in_filter": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -206,19 +220,6 @@
|
|||||||
"reqd": 0,
|
"reqd": 0,
|
||||||
"show_in_filter": 0
|
"show_in_filter": 0
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"allow_read_on_all_link_options": 0,
|
|
||||||
"fieldname": "guardians",
|
|
||||||
"fieldtype": "Table",
|
|
||||||
"hidden": 0,
|
|
||||||
"label": "Guardians",
|
|
||||||
"max_length": 0,
|
|
||||||
"max_value": 0,
|
|
||||||
"options": "Student Guardian",
|
|
||||||
"read_only": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"show_in_filter": 0
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"allow_read_on_all_link_options": 0,
|
"allow_read_on_all_link_options": 0,
|
||||||
"fieldname": "siblings",
|
"fieldname": "siblings",
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"cards": [
|
||||||
|
{
|
||||||
|
"hidden": 0,
|
||||||
|
"label": "Integrations Settings",
|
||||||
|
"links": "[\n\t{\n\t \"type\": \"doctype\",\n\t\t\"name\": \"Woocommerce Settings\"\n\t},\n\t{\n\t \"type\": \"doctype\",\n\t\t\"name\": \"Shopify Settings\",\n\t\t\"description\": \"Connect Shopify with ERPNext\"\n\t},\n\t{\n\t \"type\": \"doctype\",\n\t\t\"name\": \"Amazon MWS Settings\",\n\t\t\"description\": \"Connect Amazon with ERPNext\"\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Plaid Settings\",\n\t\t\"description\": \"Connect your bank accounts to ERPNext\"\n\t},\n {\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Exotel Settings\",\n\t\t\"description\": \"Connect your Exotel Account to ERPNext and track call logs\"\n }\n]"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"category": "Modules",
|
||||||
|
"charts": [],
|
||||||
|
"creation": "2020-07-31 10:38:54.021237",
|
||||||
|
"developer_mode_only": 0,
|
||||||
|
"disable_user_customization": 0,
|
||||||
|
"docstatus": 0,
|
||||||
|
"doctype": "Desk Page",
|
||||||
|
"extends": "Settings",
|
||||||
|
"extends_another_page": 1,
|
||||||
|
"hide_custom": 0,
|
||||||
|
"idx": 0,
|
||||||
|
"is_standard": 1,
|
||||||
|
"label": "ERPNext Integrations Settings",
|
||||||
|
"modified": "2020-07-31 10:44:39.374297",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "ERPNext Integrations",
|
||||||
|
"name": "ERPNext Integrations Settings",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"pin_to_bottom": 0,
|
||||||
|
"pin_to_top": 0,
|
||||||
|
"shortcuts": []
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
|
||||||
|
{% if not jQuery.isEmptyObject(data) %}
|
||||||
|
<h5 style="margin-top: 20px;"> {{ __("Balance Details") }} </h5>
|
||||||
|
<table class="table table-bordered small">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style="width: 20%">{{ __("Account Type") }}</th>
|
||||||
|
<th style="width: 20%" class="text-right">{{ __("Current Balance") }}</th>
|
||||||
|
<th style="width: 20%" class="text-right">{{ __("Available Balance") }}</th>
|
||||||
|
<th style="width: 20%" class="text-right">{{ __("Reserved Balance") }}</th>
|
||||||
|
<th style="width: 20%" class="text-right">{{ __("Uncleared Balance") }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for(const [key, value] of Object.entries(data)) { %}
|
||||||
|
<tr>
|
||||||
|
<td> {%= key %} </td>
|
||||||
|
<td class="text-right"> {%= value["current_balance"] %} </td>
|
||||||
|
<td class="text-right"> {%= value["available_balance"] %} </td>
|
||||||
|
<td class="text-right"> {%= value["reserved_balance"] %} </td>
|
||||||
|
<td class="text-right"> {%= value["uncleared_balance"] %} </td>
|
||||||
|
</tr>
|
||||||
|
{% } %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% else %}
|
||||||
|
<p style="margin-top: 30px;"> Account Balance Information Not Available. </p>
|
||||||
|
{% endif %}
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
import base64
|
||||||
|
import requests
|
||||||
|
from requests.auth import HTTPBasicAuth
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
class MpesaConnector():
|
||||||
|
def __init__(self, env="sandbox", app_key=None, app_secret=None, sandbox_url="https://sandbox.safaricom.co.ke",
|
||||||
|
live_url="https://safaricom.co.ke"):
|
||||||
|
"""Setup configuration for Mpesa connector and generate new access token."""
|
||||||
|
self.env = env
|
||||||
|
self.app_key = app_key
|
||||||
|
self.app_secret = app_secret
|
||||||
|
if env == "sandbox":
|
||||||
|
self.base_url = sandbox_url
|
||||||
|
else:
|
||||||
|
self.base_url = live_url
|
||||||
|
self.authenticate()
|
||||||
|
|
||||||
|
def authenticate(self):
|
||||||
|
"""
|
||||||
|
This method is used to fetch the access token required by Mpesa.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
access_token (str): This token is to be used with the Bearer header for further API calls to Mpesa.
|
||||||
|
"""
|
||||||
|
authenticate_uri = "/oauth/v1/generate?grant_type=client_credentials"
|
||||||
|
authenticate_url = "{0}{1}".format(self.base_url, authenticate_uri)
|
||||||
|
r = requests.get(
|
||||||
|
authenticate_url,
|
||||||
|
auth=HTTPBasicAuth(self.app_key, self.app_secret)
|
||||||
|
)
|
||||||
|
self.authentication_token = r.json()['access_token']
|
||||||
|
return r.json()['access_token']
|
||||||
|
|
||||||
|
def get_balance(self, initiator=None, security_credential=None, party_a=None, identifier_type=None,
|
||||||
|
remarks=None, queue_timeout_url=None,result_url=None):
|
||||||
|
"""
|
||||||
|
This method uses Mpesa's Account Balance API to to enquire the balance on a M-Pesa BuyGoods (Till Number).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
initiator (str): Username used to authenticate the transaction.
|
||||||
|
security_credential (str): Generate from developer portal.
|
||||||
|
command_id (str): AccountBalance.
|
||||||
|
party_a (int): Till number being queried.
|
||||||
|
identifier_type (int): Type of organization receiving the transaction. (MSISDN/Till Number/Organization short code)
|
||||||
|
remarks (str): Comments that are sent along with the transaction(maximum 100 characters).
|
||||||
|
queue_timeout_url (str): The url that handles information of timed out transactions.
|
||||||
|
result_url (str): The url that receives results from M-Pesa api call.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
OriginatorConverstionID (str): The unique request ID for tracking a transaction.
|
||||||
|
ConversationID (str): The unique request ID returned by mpesa for each request made
|
||||||
|
ResponseDescription (str): Response Description message
|
||||||
|
"""
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"Initiator": initiator,
|
||||||
|
"SecurityCredential": security_credential,
|
||||||
|
"CommandID": "AccountBalance",
|
||||||
|
"PartyA": party_a,
|
||||||
|
"IdentifierType": identifier_type,
|
||||||
|
"Remarks": remarks,
|
||||||
|
"QueueTimeOutURL": queue_timeout_url,
|
||||||
|
"ResultURL": result_url
|
||||||
|
}
|
||||||
|
headers = {'Authorization': 'Bearer {0}'.format(self.authentication_token), 'Content-Type': "application/json"}
|
||||||
|
saf_url = "{0}{1}".format(self.base_url, "/mpesa/accountbalance/v1/query")
|
||||||
|
r = requests.post(saf_url, headers=headers, json=payload)
|
||||||
|
return r.json()
|
||||||
|
|
||||||
|
def stk_push(self, business_shortcode=None, passcode=None, amount=None, callback_url=None, reference_code=None,
|
||||||
|
phone_number=None, description=None):
|
||||||
|
"""
|
||||||
|
This method uses Mpesa's Express API to initiate online payment on behalf of a customer.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
business_shortcode (int): The short code of the organization.
|
||||||
|
passcode (str): Get from developer portal
|
||||||
|
amount (int): The amount being transacted
|
||||||
|
callback_url (str): A CallBack URL is a valid secure URL that is used to receive notifications from M-Pesa API.
|
||||||
|
reference_code(str): Account Reference: This is an Alpha-Numeric parameter that is defined by your system as an Identifier of the transaction for CustomerPayBillOnline transaction type.
|
||||||
|
phone_number(int): The Mobile Number to receive the STK Pin Prompt.
|
||||||
|
description(str): This is any additional information/comment that can be sent along with the request from your system. MAX 13 characters
|
||||||
|
|
||||||
|
Success Response:
|
||||||
|
CustomerMessage(str): Messages that customers can understand.
|
||||||
|
CheckoutRequestID(str): This is a global unique identifier of the processed checkout transaction request.
|
||||||
|
ResponseDescription(str): Describes Success or failure
|
||||||
|
MerchantRequestID(str): This is a global unique Identifier for any submitted payment request.
|
||||||
|
ResponseCode(int): 0 means success all others are error codes. e.g.404.001.03
|
||||||
|
|
||||||
|
Error Reponse:
|
||||||
|
requestId(str): This is a unique requestID for the payment request
|
||||||
|
errorCode(str): This is a predefined code that indicates the reason for request failure.
|
||||||
|
errorMessage(str): This is a predefined code that indicates the reason for request failure.
|
||||||
|
"""
|
||||||
|
|
||||||
|
time = str(datetime.datetime.now()).split(".")[0].replace("-", "").replace(" ", "").replace(":", "")
|
||||||
|
password = "{0}{1}{2}".format(str(business_shortcode), str(passcode), time)
|
||||||
|
encoded = base64.b64encode(bytes(password, encoding='utf8'))
|
||||||
|
payload = {
|
||||||
|
"BusinessShortCode": business_shortcode,
|
||||||
|
"Password": encoded.decode("utf-8"),
|
||||||
|
"Timestamp": time,
|
||||||
|
"TransactionType": "CustomerPayBillOnline",
|
||||||
|
"Amount": amount,
|
||||||
|
"PartyA": int(phone_number),
|
||||||
|
"PartyB": business_shortcode,
|
||||||
|
"PhoneNumber": int(phone_number),
|
||||||
|
"CallBackURL": callback_url,
|
||||||
|
"AccountReference": reference_code,
|
||||||
|
"TransactionDesc": description
|
||||||
|
}
|
||||||
|
headers = {'Authorization': 'Bearer {0}'.format(self.authentication_token), 'Content-Type': "application/json"}
|
||||||
|
|
||||||
|
saf_url = "{0}{1}".format(self.base_url, "/mpesa/stkpush/v1/processrequest")
|
||||||
|
r = requests.post(saf_url, headers=headers, json=payload)
|
||||||
|
return r.json()
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import frappe
|
||||||
|
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
|
||||||
|
|
||||||
|
def create_custom_pos_fields():
|
||||||
|
"""Create custom fields corresponding to POS Settings and POS Invoice."""
|
||||||
|
pos_field = {
|
||||||
|
"POS Invoice": [
|
||||||
|
{
|
||||||
|
"fieldname": "request_for_payment",
|
||||||
|
"label": "Request for Payment",
|
||||||
|
"fieldtype": "Button",
|
||||||
|
"hidden": 1,
|
||||||
|
"insert_after": "contact_email"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "mpesa_receipt_number",
|
||||||
|
"label": "Mpesa Receipt Number",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"read_only": 1,
|
||||||
|
"insert_after": "company"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
if not frappe.get_meta("POS Invoice").has_field("request_for_payment"):
|
||||||
|
create_custom_fields(pos_field)
|
||||||
|
|
||||||
|
record_dict = [{
|
||||||
|
"doctype": "POS Field",
|
||||||
|
"fieldname": "contact_mobile",
|
||||||
|
"label": "Mobile No",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"options": "Phone",
|
||||||
|
"parenttype": "POS Settings",
|
||||||
|
"parent": "POS Settings",
|
||||||
|
"parentfield": "invoice_fields"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"doctype": "POS Field",
|
||||||
|
"fieldname": "request_for_payment",
|
||||||
|
"label": "Request for Payment",
|
||||||
|
"fieldtype": "Button",
|
||||||
|
"parenttype": "POS Settings",
|
||||||
|
"parent": "POS Settings",
|
||||||
|
"parentfield": "invoice_fields"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
create_pos_settings(record_dict)
|
||||||
|
|
||||||
|
def create_pos_settings(record_dict):
|
||||||
|
for record in record_dict:
|
||||||
|
if frappe.db.exists("POS Field", {"fieldname": record.get("fieldname")}):
|
||||||
|
continue
|
||||||
|
frappe.get_doc(record).insert()
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
// For license information, please see license.txt
|
||||||
|
|
||||||
|
frappe.ui.form.on('Mpesa Settings', {
|
||||||
|
onload_post_render: function(frm) {
|
||||||
|
frm.events.setup_account_balance_html(frm);
|
||||||
|
},
|
||||||
|
|
||||||
|
refresh: function(frm) {
|
||||||
|
frappe.realtime.on("refresh_mpesa_dashboard", function(){
|
||||||
|
frm.reload_doc();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
get_account_balance: function(frm) {
|
||||||
|
if (!frm.initiator_name && !frm.security_credentials) {
|
||||||
|
frappe.throw(__("Please set the initiator name and the security credential"));
|
||||||
|
}
|
||||||
|
frappe.call({
|
||||||
|
method: "get_account_balance_info",
|
||||||
|
doc: frm.doc
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
setup_account_balance_html: function(frm) {
|
||||||
|
if (!frm.doc.account_balance) return;
|
||||||
|
$("div").remove(".form-dashboard-section.custom");
|
||||||
|
frm.dashboard.add_section(
|
||||||
|
frappe.render_template('account_balance', {
|
||||||
|
data: JSON.parse(frm.doc.account_balance)
|
||||||
|
})
|
||||||
|
);
|
||||||
|
frm.dashboard.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"autoname": "field:payment_gateway_name",
|
||||||
|
"creation": "2020-09-10 13:21:27.398088",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"payment_gateway_name",
|
||||||
|
"consumer_key",
|
||||||
|
"consumer_secret",
|
||||||
|
"initiator_name",
|
||||||
|
"till_number",
|
||||||
|
"sandbox",
|
||||||
|
"column_break_4",
|
||||||
|
"online_passkey",
|
||||||
|
"security_credential",
|
||||||
|
"get_account_balance",
|
||||||
|
"account_balance"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "payment_gateway_name",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Payment Gateway Name",
|
||||||
|
"reqd": 1,
|
||||||
|
"unique": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "consumer_key",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Consumer Key",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "consumer_secret",
|
||||||
|
"fieldtype": "Password",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Consumer Secret",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "till_number",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Till Number",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "sandbox",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Sandbox"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_4",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "online_passkey",
|
||||||
|
"fieldtype": "Password",
|
||||||
|
"label": " Online PassKey",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "initiator_name",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Initiator Name"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "security_credential",
|
||||||
|
"fieldtype": "Small Text",
|
||||||
|
"label": "Security Credential"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "account_balance",
|
||||||
|
"fieldtype": "Long Text",
|
||||||
|
"hidden": 1,
|
||||||
|
"label": "Account Balance",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "get_account_balance",
|
||||||
|
"fieldtype": "Button",
|
||||||
|
"label": "Get Account Balance"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"links": [],
|
||||||
|
"modified": "2020-09-25 20:21:38.215494",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "ERPNext Integrations",
|
||||||
|
"name": "Mpesa Settings",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "System Manager",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Accounts Manager",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Accounts User",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC"
|
||||||
|
}
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (c) 2020, Frappe Technologies and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
from json import loads, dumps
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
from frappe.model.document import Document
|
||||||
|
from frappe import _
|
||||||
|
from frappe.utils import call_hook_method, fmt_money
|
||||||
|
from frappe.integrations.utils import create_request_log, create_payment_gateway
|
||||||
|
from frappe.utils import get_request_site_address
|
||||||
|
from erpnext.erpnext_integrations.utils import create_mode_of_payment
|
||||||
|
from erpnext.erpnext_integrations.doctype.mpesa_settings.mpesa_connector import MpesaConnector
|
||||||
|
from erpnext.erpnext_integrations.doctype.mpesa_settings.mpesa_custom_fields import create_custom_pos_fields
|
||||||
|
|
||||||
|
class MpesaSettings(Document):
|
||||||
|
supported_currencies = ["KES"]
|
||||||
|
|
||||||
|
def validate_transaction_currency(self, currency):
|
||||||
|
if currency not in self.supported_currencies:
|
||||||
|
frappe.throw(_("Please select another payment method. Mpesa does not support transactions in currency '{0}'").format(currency))
|
||||||
|
|
||||||
|
def on_update(self):
|
||||||
|
create_custom_pos_fields()
|
||||||
|
create_payment_gateway('Mpesa-' + self.payment_gateway_name, settings='Mpesa Settings', controller=self.payment_gateway_name)
|
||||||
|
call_hook_method('payment_gateway_enabled', gateway='Mpesa-' + self.payment_gateway_name, payment_channel="Phone")
|
||||||
|
|
||||||
|
# required to fetch the bank account details from the payment gateway account
|
||||||
|
frappe.db.commit()
|
||||||
|
create_mode_of_payment('Mpesa-' + self.payment_gateway_name, payment_type="Phone")
|
||||||
|
|
||||||
|
def request_for_payment(self, **kwargs):
|
||||||
|
if frappe.flags.in_test:
|
||||||
|
from erpnext.erpnext_integrations.doctype.mpesa_settings.test_mpesa_settings import get_payment_request_response_payload
|
||||||
|
response = frappe._dict(get_payment_request_response_payload())
|
||||||
|
else:
|
||||||
|
response = frappe._dict(generate_stk_push(**kwargs))
|
||||||
|
|
||||||
|
self.handle_api_response("CheckoutRequestID", kwargs, response)
|
||||||
|
|
||||||
|
def get_account_balance_info(self):
|
||||||
|
payload = dict(
|
||||||
|
reference_doctype="Mpesa Settings",
|
||||||
|
reference_docname=self.name,
|
||||||
|
doc_details=vars(self)
|
||||||
|
)
|
||||||
|
|
||||||
|
if frappe.flags.in_test:
|
||||||
|
from erpnext.erpnext_integrations.doctype.mpesa_settings.test_mpesa_settings import get_test_account_balance_response
|
||||||
|
response = frappe._dict(get_test_account_balance_response())
|
||||||
|
else:
|
||||||
|
response = frappe._dict(get_account_balance(payload))
|
||||||
|
|
||||||
|
self.handle_api_response("ConversationID", payload, response)
|
||||||
|
|
||||||
|
def handle_api_response(self, global_id, request_dict, response):
|
||||||
|
"""Response received from API calls returns a global identifier for each transaction, this code is returned during the callback."""
|
||||||
|
# check error response
|
||||||
|
if getattr(response, "requestId"):
|
||||||
|
req_name = getattr(response, "requestId")
|
||||||
|
error = response
|
||||||
|
else:
|
||||||
|
# global checkout id used as request name
|
||||||
|
req_name = getattr(response, global_id)
|
||||||
|
error = None
|
||||||
|
|
||||||
|
create_request_log(request_dict, "Host", "Mpesa", req_name, error)
|
||||||
|
|
||||||
|
if error:
|
||||||
|
frappe.throw(_(getattr(response, "errorMessage")), title=_("Transaction Error"))
|
||||||
|
|
||||||
|
def generate_stk_push(**kwargs):
|
||||||
|
"""Generate stk push by making a API call to the stk push API."""
|
||||||
|
args = frappe._dict(kwargs)
|
||||||
|
try:
|
||||||
|
callback_url = get_request_site_address(True) + "/api/method/erpnext.erpnext_integrations.doctype.mpesa_settings.mpesa_settings.verify_transaction"
|
||||||
|
|
||||||
|
mpesa_settings = frappe.get_doc("Mpesa Settings", args.payment_gateway[6:])
|
||||||
|
env = "production" if not mpesa_settings.sandbox else "sandbox"
|
||||||
|
|
||||||
|
connector = MpesaConnector(env=env,
|
||||||
|
app_key=mpesa_settings.consumer_key,
|
||||||
|
app_secret=mpesa_settings.get_password("consumer_secret"))
|
||||||
|
|
||||||
|
mobile_number = sanitize_mobile_number(args.sender)
|
||||||
|
|
||||||
|
response = connector.stk_push(business_shortcode=mpesa_settings.till_number,
|
||||||
|
passcode=mpesa_settings.get_password("online_passkey"), amount=args.grand_total,
|
||||||
|
callback_url=callback_url, reference_code=mpesa_settings.till_number,
|
||||||
|
phone_number=mobile_number, description="POS Payment")
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
frappe.log_error(title=_("Mpesa Express Transaction Error"))
|
||||||
|
frappe.throw(_("Issue detected with Mpesa configuration, check the error logs for more details"), title=_("Mpesa Express Error"))
|
||||||
|
|
||||||
|
def sanitize_mobile_number(number):
|
||||||
|
"""Add country code and strip leading zeroes from the phone number."""
|
||||||
|
return "254" + str(number).lstrip("0")
|
||||||
|
|
||||||
|
@frappe.whitelist(allow_guest=True)
|
||||||
|
def verify_transaction(**kwargs):
|
||||||
|
"""Verify the transaction result received via callback from stk."""
|
||||||
|
transaction_response = frappe._dict(kwargs["Body"]["stkCallback"])
|
||||||
|
|
||||||
|
checkout_id = getattr(transaction_response, "CheckoutRequestID", "")
|
||||||
|
request = frappe.get_doc("Integration Request", checkout_id)
|
||||||
|
transaction_data = frappe._dict(loads(request.data))
|
||||||
|
|
||||||
|
if transaction_response['ResultCode'] == 0:
|
||||||
|
if request.reference_doctype and request.reference_docname:
|
||||||
|
try:
|
||||||
|
doc = frappe.get_doc(request.reference_doctype,
|
||||||
|
request.reference_docname)
|
||||||
|
doc.run_method("on_payment_authorized", 'Completed')
|
||||||
|
|
||||||
|
item_response = transaction_response["CallbackMetadata"]["Item"]
|
||||||
|
mpesa_receipt = fetch_param_value(item_response, "MpesaReceiptNumber", "Name")
|
||||||
|
frappe.db.set_value("POS Invoice", doc.reference_name, "mpesa_receipt_number", mpesa_receipt)
|
||||||
|
request.handle_success(transaction_response)
|
||||||
|
except Exception:
|
||||||
|
request.handle_failure(transaction_response)
|
||||||
|
frappe.log_error(frappe.get_traceback())
|
||||||
|
|
||||||
|
else:
|
||||||
|
request.handle_failure(transaction_response)
|
||||||
|
|
||||||
|
frappe.publish_realtime('process_phone_payment', doctype="POS Invoice",
|
||||||
|
docname=transaction_data.payment_reference, user=request.owner, message=transaction_response)
|
||||||
|
|
||||||
|
def get_account_balance(request_payload):
|
||||||
|
"""Call account balance API to send the request to the Mpesa Servers."""
|
||||||
|
try:
|
||||||
|
mpesa_settings = frappe.get_doc("Mpesa Settings", request_payload.get("reference_docname"))
|
||||||
|
env = "production" if not mpesa_settings.sandbox else "sandbox"
|
||||||
|
connector = MpesaConnector(env=env,
|
||||||
|
app_key=mpesa_settings.consumer_key,
|
||||||
|
app_secret=mpesa_settings.get_password("consumer_secret"))
|
||||||
|
|
||||||
|
callback_url = get_request_site_address(True) + "/api/method/erpnext.erpnext_integrations.doctype.mpesa_settings.mpesa_settings.process_balance_info"
|
||||||
|
|
||||||
|
response = connector.get_balance(mpesa_settings.initiator_name, mpesa_settings.security_credential, mpesa_settings.till_number, 4, mpesa_settings.name, callback_url, callback_url)
|
||||||
|
return response
|
||||||
|
except Exception:
|
||||||
|
frappe.log_error(title=_("Account Balance Processing Error"))
|
||||||
|
frappe.throw(title=_("Error"), message=_("Please check your configuration and try again"))
|
||||||
|
|
||||||
|
@frappe.whitelist(allow_guest=True)
|
||||||
|
def process_balance_info(**kwargs):
|
||||||
|
"""Process and store account balance information received via callback from the account balance API call."""
|
||||||
|
account_balance_response = frappe._dict(kwargs["Result"])
|
||||||
|
|
||||||
|
conversation_id = getattr(account_balance_response, "ConversationID", "")
|
||||||
|
request = frappe.get_doc("Integration Request", conversation_id)
|
||||||
|
|
||||||
|
if request.status == "Completed":
|
||||||
|
return
|
||||||
|
|
||||||
|
transaction_data = frappe._dict(loads(request.data))
|
||||||
|
|
||||||
|
if account_balance_response["ResultCode"] == 0:
|
||||||
|
try:
|
||||||
|
result_params = account_balance_response["ResultParameters"]["ResultParameter"]
|
||||||
|
|
||||||
|
balance_info = fetch_param_value(result_params, "AccountBalance", "Key")
|
||||||
|
balance_info = format_string_to_json(balance_info)
|
||||||
|
|
||||||
|
ref_doc = frappe.get_doc(transaction_data.reference_doctype, transaction_data.reference_docname)
|
||||||
|
ref_doc.db_set("account_balance", balance_info)
|
||||||
|
|
||||||
|
request.handle_success(account_balance_response)
|
||||||
|
frappe.publish_realtime("refresh_mpesa_dashboard")
|
||||||
|
except Exception:
|
||||||
|
request.handle_failure(account_balance_response)
|
||||||
|
frappe.log_error(title=_("Mpesa Account Balance Processing Error"), message=account_balance_response)
|
||||||
|
else:
|
||||||
|
request.handle_failure(account_balance_response)
|
||||||
|
|
||||||
|
def format_string_to_json(balance_info):
|
||||||
|
"""
|
||||||
|
Format string to json.
|
||||||
|
|
||||||
|
e.g: '''Working Account|KES|481000.00|481000.00|0.00|0.00'''
|
||||||
|
=> {'Working Account': {'current_balance': '481000.00',
|
||||||
|
'available_balance': '481000.00',
|
||||||
|
'reserved_balance': '0.00',
|
||||||
|
'uncleared_balance': '0.00'}}
|
||||||
|
"""
|
||||||
|
balance_dict = frappe._dict()
|
||||||
|
for account_info in balance_info.split("&"):
|
||||||
|
account_info = account_info.split('|')
|
||||||
|
balance_dict[account_info[0]] = dict(
|
||||||
|
current_balance=fmt_money(account_info[2], currency="KES"),
|
||||||
|
available_balance=fmt_money(account_info[3], currency="KES"),
|
||||||
|
reserved_balance=fmt_money(account_info[4], currency="KES"),
|
||||||
|
uncleared_balance=fmt_money(account_info[5], currency="KES")
|
||||||
|
)
|
||||||
|
return dumps(balance_dict)
|
||||||
|
|
||||||
|
def fetch_param_value(response, key, key_field):
|
||||||
|
"""Fetch the specified key from list of dictionary. Key is identified via the key field."""
|
||||||
|
for param in response:
|
||||||
|
if param[key_field] == key:
|
||||||
|
return param["Value"]
|
||||||
@@ -0,0 +1,240 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
|
# See license.txt
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
from json import dumps
|
||||||
|
import frappe
|
||||||
|
import unittest
|
||||||
|
from erpnext.erpnext_integrations.doctype.mpesa_settings.mpesa_settings import process_balance_info, verify_transaction
|
||||||
|
from erpnext.accounts.doctype.pos_invoice.test_pos_invoice import create_pos_invoice
|
||||||
|
|
||||||
|
class TestMpesaSettings(unittest.TestCase):
|
||||||
|
def test_creation_of_payment_gateway(self):
|
||||||
|
create_mpesa_settings(payment_gateway_name="_Test")
|
||||||
|
|
||||||
|
mode_of_payment = frappe.get_doc("Mode of Payment", "Mpesa-_Test")
|
||||||
|
self.assertTrue(frappe.db.exists("Payment Gateway Account", {'payment_gateway': "Mpesa-_Test"}))
|
||||||
|
self.assertTrue(mode_of_payment.name)
|
||||||
|
self.assertEquals(mode_of_payment.type, "Phone")
|
||||||
|
|
||||||
|
def test_processing_of_account_balance(self):
|
||||||
|
mpesa_doc = create_mpesa_settings(payment_gateway_name="_Account Balance")
|
||||||
|
mpesa_doc.get_account_balance_info()
|
||||||
|
|
||||||
|
callback_response = get_account_balance_callback_payload()
|
||||||
|
process_balance_info(**callback_response)
|
||||||
|
integration_request = frappe.get_doc("Integration Request", "AG_20200927_00007cdb1f9fb6494315")
|
||||||
|
|
||||||
|
# test integration request creation and successful update of the status on receiving callback response
|
||||||
|
self.assertTrue(integration_request)
|
||||||
|
self.assertEquals(integration_request.status, "Completed")
|
||||||
|
|
||||||
|
# test formatting of account balance received as string to json with appropriate currency symbol
|
||||||
|
mpesa_doc.reload()
|
||||||
|
self.assertEquals(mpesa_doc.account_balance, dumps({
|
||||||
|
"Working Account": {
|
||||||
|
"current_balance": "Sh 481,000.00",
|
||||||
|
"available_balance": "Sh 481,000.00",
|
||||||
|
"reserved_balance": "Sh 0.00",
|
||||||
|
"uncleared_balance": "Sh 0.00"
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
def test_processing_of_callback_payload(self):
|
||||||
|
create_mpesa_settings(payment_gateway_name="Payment")
|
||||||
|
mpesa_account = frappe.db.get_value("Payment Gateway Account", {"payment_gateway": 'Mpesa-Payment'}, "payment_account")
|
||||||
|
frappe.db.set_value("Account", mpesa_account, "account_currency", "KES")
|
||||||
|
|
||||||
|
pos_invoice = create_pos_invoice(do_not_submit=1)
|
||||||
|
pos_invoice.append("payments", {'mode_of_payment': 'Mpesa-Payment', 'account': mpesa_account, 'amount': 500})
|
||||||
|
pos_invoice.contact_mobile = "093456543894"
|
||||||
|
pos_invoice.currency = "KES"
|
||||||
|
pos_invoice.save()
|
||||||
|
|
||||||
|
pr = pos_invoice.create_payment_request()
|
||||||
|
# test payment request creation
|
||||||
|
self.assertEquals(pr.payment_gateway, "Mpesa-Payment")
|
||||||
|
|
||||||
|
callback_response = get_payment_callback_payload()
|
||||||
|
verify_transaction(**callback_response)
|
||||||
|
# test creation of integration request
|
||||||
|
integration_request = frappe.get_doc("Integration Request", "ws_CO_061020201133231972")
|
||||||
|
|
||||||
|
# test integration request creation and successful update of the status on receiving callback response
|
||||||
|
self.assertTrue(integration_request)
|
||||||
|
self.assertEquals(integration_request.status, "Completed")
|
||||||
|
|
||||||
|
pos_invoice.reload()
|
||||||
|
integration_request.reload()
|
||||||
|
self.assertEquals(pos_invoice.mpesa_receipt_number, "LGR7OWQX0R")
|
||||||
|
self.assertEquals(integration_request.status, "Completed")
|
||||||
|
|
||||||
|
def create_mpesa_settings(payment_gateway_name="Express"):
|
||||||
|
if frappe.db.exists("Mpesa Settings", payment_gateway_name):
|
||||||
|
return frappe.get_doc("Mpesa Settings", payment_gateway_name)
|
||||||
|
|
||||||
|
doc = frappe.get_doc(dict( #nosec
|
||||||
|
doctype="Mpesa Settings",
|
||||||
|
payment_gateway_name=payment_gateway_name,
|
||||||
|
consumer_key="5sMu9LVI1oS3oBGPJfh3JyvLHwZOdTKn",
|
||||||
|
consumer_secret="VI1oS3oBGPJfh3JyvLHw",
|
||||||
|
online_passkey="LVI1oS3oBGPJfh3JyvLHwZOd",
|
||||||
|
till_number="174379"
|
||||||
|
))
|
||||||
|
|
||||||
|
doc.insert(ignore_permissions=True)
|
||||||
|
return doc
|
||||||
|
|
||||||
|
def get_test_account_balance_response():
|
||||||
|
"""Response received after calling the account balance API."""
|
||||||
|
return {
|
||||||
|
"ResultType":0,
|
||||||
|
"ResultCode":0,
|
||||||
|
"ResultDesc":"The service request has been accepted successfully.",
|
||||||
|
"OriginatorConversationID":"10816-694520-2",
|
||||||
|
"ConversationID":"AG_20200927_00007cdb1f9fb6494315",
|
||||||
|
"TransactionID":"LGR0000000",
|
||||||
|
"ResultParameters":{
|
||||||
|
"ResultParameter":[
|
||||||
|
{
|
||||||
|
"Key":"ReceiptNo",
|
||||||
|
"Value":"LGR919G2AV"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key":"Conversation ID",
|
||||||
|
"Value":"AG_20170727_00004492b1b6d0078fbe"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key":"FinalisedTime",
|
||||||
|
"Value":20170727101415
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key":"Amount",
|
||||||
|
"Value":10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key":"TransactionStatus",
|
||||||
|
"Value":"Completed"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key":"ReasonType",
|
||||||
|
"Value":"Salary Payment via API"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key":"TransactionReason"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key":"DebitPartyCharges",
|
||||||
|
"Value":"Fee For B2C Payment|KES|33.00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key":"DebitAccountType",
|
||||||
|
"Value":"Utility Account"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key":"InitiatedTime",
|
||||||
|
"Value":20170727101415
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key":"Originator Conversation ID",
|
||||||
|
"Value":"19455-773836-1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key":"CreditPartyName",
|
||||||
|
"Value":"254708374149 - John Doe"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Key":"DebitPartyName",
|
||||||
|
"Value":"600134 - Safaricom157"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"ReferenceData":{
|
||||||
|
"ReferenceItem":{
|
||||||
|
"Key":"Occasion",
|
||||||
|
"Value":"aaaa"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_payment_request_response_payload():
|
||||||
|
"""Response received after successfully calling the stk push process request API."""
|
||||||
|
return {
|
||||||
|
"MerchantRequestID": "8071-27184008-1",
|
||||||
|
"CheckoutRequestID": "ws_CO_061020201133231972",
|
||||||
|
"ResultCode": 0,
|
||||||
|
"ResultDesc": "The service request is processed successfully.",
|
||||||
|
"CallbackMetadata": {
|
||||||
|
"Item": [
|
||||||
|
{ "Name": "Amount", "Value": 500.0 },
|
||||||
|
{ "Name": "MpesaReceiptNumber", "Value": "LGR7OWQX0R" },
|
||||||
|
{ "Name": "TransactionDate", "Value": 20201006113336 },
|
||||||
|
{ "Name": "PhoneNumber", "Value": 254723575670 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_payment_callback_payload():
|
||||||
|
"""Response received from the server as callback after calling the stkpush process request API."""
|
||||||
|
return {
|
||||||
|
"Body":{
|
||||||
|
"stkCallback":{
|
||||||
|
"MerchantRequestID":"19465-780693-1",
|
||||||
|
"CheckoutRequestID":"ws_CO_061020201133231972",
|
||||||
|
"ResultCode":0,
|
||||||
|
"ResultDesc":"The service request is processed successfully.",
|
||||||
|
"CallbackMetadata":{
|
||||||
|
"Item":[
|
||||||
|
{
|
||||||
|
"Name":"Amount",
|
||||||
|
"Value":500
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name":"MpesaReceiptNumber",
|
||||||
|
"Value":"LGR7OWQX0R"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name":"Balance"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name":"TransactionDate",
|
||||||
|
"Value":20170727154800
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name":"PhoneNumber",
|
||||||
|
"Value":254721566839
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_account_balance_callback_payload():
|
||||||
|
"""Response received from the server as callback after calling the account balance API."""
|
||||||
|
return {
|
||||||
|
"Result":{
|
||||||
|
"ResultType": 0,
|
||||||
|
"ResultCode": 0,
|
||||||
|
"ResultDesc": "The service request is processed successfully.",
|
||||||
|
"OriginatorConversationID": "16470-170099139-1",
|
||||||
|
"ConversationID": "AG_20200927_00007cdb1f9fb6494315",
|
||||||
|
"TransactionID": "OIR0000000",
|
||||||
|
"ResultParameters": {
|
||||||
|
"ResultParameter": [
|
||||||
|
{
|
||||||
|
"Key": "AccountBalance",
|
||||||
|
"Value": "Working Account|KES|481000.00|481000.00|0.00|0.00"
|
||||||
|
},
|
||||||
|
{ "Key": "BOCompletedTime", "Value": 20200927234123 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"ReferenceData": {
|
||||||
|
"ReferenceItem": {
|
||||||
|
"Key": "QueueTimeoutURL",
|
||||||
|
"Value": "https://internalsandbox.safaricom.co.ke/mpesa/abresults/v1/submit"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import frappe
|
|||||||
from frappe import _
|
from frappe import _
|
||||||
import base64, hashlib, hmac
|
import base64, hashlib, hmac
|
||||||
from six.moves.urllib.parse import urlparse
|
from six.moves.urllib.parse import urlparse
|
||||||
|
from erpnext import get_default_company
|
||||||
|
|
||||||
def validate_webhooks_request(doctype, hmac_key, secret_key='secret'):
|
def validate_webhooks_request(doctype, hmac_key, secret_key='secret'):
|
||||||
def innerfn(fn):
|
def innerfn(fn):
|
||||||
@@ -41,3 +42,22 @@ def get_webhook_address(connector_name, method, exclude_uri=False):
|
|||||||
server_url = '{uri.scheme}://{uri.netloc}/api/method/{endpoint}'.format(uri=urlparse(url), endpoint=endpoint)
|
server_url = '{uri.scheme}://{uri.netloc}/api/method/{endpoint}'.format(uri=urlparse(url), endpoint=endpoint)
|
||||||
|
|
||||||
return server_url
|
return server_url
|
||||||
|
|
||||||
|
def create_mode_of_payment(gateway, payment_type="General"):
|
||||||
|
payment_gateway_account = frappe.db.get_value("Payment Gateway Account", {
|
||||||
|
"payment_gateway": gateway
|
||||||
|
}, ['payment_account'])
|
||||||
|
|
||||||
|
if not frappe.db.exists("Mode of Payment", gateway) and payment_gateway_account:
|
||||||
|
mode_of_payment = frappe.get_doc({
|
||||||
|
"doctype": "Mode of Payment",
|
||||||
|
"mode_of_payment": gateway,
|
||||||
|
"enabled": 1,
|
||||||
|
"type": payment_type,
|
||||||
|
"accounts": [{
|
||||||
|
"doctype": "Mode of Payment Account",
|
||||||
|
"company": get_default_company(),
|
||||||
|
"default_account": payment_gateway_account
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
mode_of_payment.insert(ignore_permissions=True)
|
||||||
@@ -43,7 +43,8 @@
|
|||||||
"ignore_user_permissions": 1,
|
"ignore_user_permissions": 1,
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Dosage",
|
"label": "Dosage",
|
||||||
"options": "Prescription Dosage"
|
"options": "Prescription Dosage",
|
||||||
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "period",
|
"fieldname": "period",
|
||||||
@@ -51,14 +52,16 @@
|
|||||||
"ignore_user_permissions": 1,
|
"ignore_user_permissions": 1,
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Period",
|
"label": "Period",
|
||||||
"options": "Prescription Duration"
|
"options": "Prescription Duration",
|
||||||
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "dosage_form",
|
"fieldname": "dosage_form",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"ignore_user_permissions": 1,
|
"ignore_user_permissions": 1,
|
||||||
"label": "Dosage Form",
|
"label": "Dosage Form",
|
||||||
"options": "Dosage Form"
|
"options": "Dosage Form",
|
||||||
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "column_break_7",
|
"fieldname": "column_break_7",
|
||||||
@@ -72,7 +75,7 @@
|
|||||||
"label": "Comment"
|
"label": "Comment"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "use_interval",
|
"depends_on": "usage_interval",
|
||||||
"fieldname": "interval",
|
"fieldname": "interval",
|
||||||
"fieldtype": "Int",
|
"fieldtype": "Int",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
@@ -80,6 +83,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "1",
|
"default": "1",
|
||||||
|
"depends_on": "usage_interval",
|
||||||
"fieldname": "update_schedule",
|
"fieldname": "update_schedule",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"hidden": 1,
|
"hidden": 1,
|
||||||
@@ -99,12 +103,13 @@
|
|||||||
"default": "0",
|
"default": "0",
|
||||||
"fieldname": "usage_interval",
|
"fieldname": "usage_interval",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
|
"hidden": 1,
|
||||||
"label": "Dosage by Time Interval"
|
"label": "Dosage by Time Interval"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-02-26 17:02:42.741338",
|
"modified": "2020-09-30 23:32:09.495288",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Healthcare",
|
"module": "Healthcare",
|
||||||
"name": "Drug Prescription",
|
"name": "Drug Prescription",
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
// For license information, please see license.txt
|
||||||
|
|
||||||
|
frappe.ui.form.on('Inpatient Medication Entry', {
|
||||||
|
refresh: function(frm) {
|
||||||
|
// Ignore cancellation of doctype on cancel all
|
||||||
|
frm.ignore_doctypes_on_cancel_all = ['Stock Entry'];
|
||||||
|
|
||||||
|
frm.set_query('item_code', () => {
|
||||||
|
return {
|
||||||
|
filters: {
|
||||||
|
is_stock_item: 1
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
frm.set_query('drug_code', 'medication_orders', () => {
|
||||||
|
return {
|
||||||
|
filters: {
|
||||||
|
is_stock_item: 1
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
get_medication_orders: function(frm) {
|
||||||
|
frappe.call({
|
||||||
|
method: 'get_medication_orders',
|
||||||
|
doc: frm.doc,
|
||||||
|
freeze: true,
|
||||||
|
freeze_message: __('Fetching Pending Medication Orders'),
|
||||||
|
callback: function() {
|
||||||
|
refresh_field('medication_orders');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,203 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"autoname": "naming_series:",
|
||||||
|
"creation": "2020-09-25 14:13:20.111906",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"naming_series",
|
||||||
|
"company",
|
||||||
|
"column_break_3",
|
||||||
|
"posting_date",
|
||||||
|
"status",
|
||||||
|
"filters_section",
|
||||||
|
"item_code",
|
||||||
|
"assigned_to_practitioner",
|
||||||
|
"patient",
|
||||||
|
"practitioner",
|
||||||
|
"service_unit",
|
||||||
|
"column_break_11",
|
||||||
|
"from_date",
|
||||||
|
"to_date",
|
||||||
|
"from_time",
|
||||||
|
"to_time",
|
||||||
|
"select_medication_orders_section",
|
||||||
|
"get_medication_orders",
|
||||||
|
"medication_orders",
|
||||||
|
"section_break_18",
|
||||||
|
"update_stock",
|
||||||
|
"warehouse",
|
||||||
|
"amended_from"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "naming_series",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"label": "Naming Series",
|
||||||
|
"options": "HLC-IME-.YYYY.-"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "company",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"in_standard_filter": 1,
|
||||||
|
"label": "Company",
|
||||||
|
"options": "Company",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_3",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "posting_date",
|
||||||
|
"fieldtype": "Date",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"in_standard_filter": 1,
|
||||||
|
"label": "Posting Date",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "status",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"label": "Status",
|
||||||
|
"options": "\nDraft\nSubmitted\nPending\nIn Process\nCompleted\nCancelled",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"collapsible": 1,
|
||||||
|
"fieldname": "filters_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Filters"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "item_code",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Item Code (Drug)",
|
||||||
|
"options": "Item"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "update_stock",
|
||||||
|
"description": "Warehouse from where medication stock should be consumed",
|
||||||
|
"fieldname": "warehouse",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Medication Warehouse",
|
||||||
|
"mandatory_depends_on": "update_stock",
|
||||||
|
"options": "Warehouse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "patient",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Patient",
|
||||||
|
"options": "Patient"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "service_unit",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Healthcare Service Unit",
|
||||||
|
"options": "Healthcare Service Unit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_11",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "from_date",
|
||||||
|
"fieldtype": "Date",
|
||||||
|
"label": "From Date"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "to_date",
|
||||||
|
"fieldtype": "Date",
|
||||||
|
"label": "To Date"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "amended_from",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Amended From",
|
||||||
|
"no_copy": 1,
|
||||||
|
"options": "Inpatient Medication Entry",
|
||||||
|
"print_hide": 1,
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "practitioner",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Healthcare Practitioner",
|
||||||
|
"options": "Healthcare Practitioner"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "select_medication_orders_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Medication Orders"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "medication_orders",
|
||||||
|
"fieldtype": "Table",
|
||||||
|
"label": "Inpatient Medication Orders",
|
||||||
|
"options": "Inpatient Medication Entry Detail",
|
||||||
|
"read_only": 1,
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "eval:doc.docstatus!==1",
|
||||||
|
"fieldname": "get_medication_orders",
|
||||||
|
"fieldtype": "Button",
|
||||||
|
"label": "Get Pending Medication Orders",
|
||||||
|
"print_hide": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "assigned_to_practitioner",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Assigned To",
|
||||||
|
"options": "User"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_18",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Stock Details"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "1",
|
||||||
|
"fieldname": "update_stock",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Update Stock"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "from_time",
|
||||||
|
"fieldtype": "Time",
|
||||||
|
"label": "From Time"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "to_time",
|
||||||
|
"fieldtype": "Time",
|
||||||
|
"label": "To Time"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"is_submittable": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2020-09-30 23:40:45.528715",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Healthcare",
|
||||||
|
"name": "Inpatient Medication Entry",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "System Manager",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"track_changes": 1
|
||||||
|
}
|
||||||
@@ -0,0 +1,273 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
import frappe
|
||||||
|
from frappe import _
|
||||||
|
from frappe.model.document import Document
|
||||||
|
from frappe.utils import flt, get_link_to_form, getdate, nowtime
|
||||||
|
from erpnext.stock.utils import get_latest_stock_qty
|
||||||
|
from erpnext.healthcare.doctype.healthcare_settings.healthcare_settings import get_account
|
||||||
|
|
||||||
|
class InpatientMedicationEntry(Document):
|
||||||
|
def validate(self):
|
||||||
|
self.validate_medication_orders()
|
||||||
|
|
||||||
|
def get_medication_orders(self):
|
||||||
|
self.validate_datetime_filters()
|
||||||
|
|
||||||
|
# pull inpatient medication orders based on selected filters
|
||||||
|
orders = get_pending_medication_orders(self)
|
||||||
|
|
||||||
|
if orders:
|
||||||
|
self.add_mo_to_table(orders)
|
||||||
|
return self
|
||||||
|
else:
|
||||||
|
self.set('medication_orders', [])
|
||||||
|
frappe.msgprint(_('No pending medication orders found for selected criteria'))
|
||||||
|
|
||||||
|
def validate_datetime_filters(self):
|
||||||
|
if self.from_date and self.to_date:
|
||||||
|
self.validate_from_to_dates('from_date', 'to_date')
|
||||||
|
|
||||||
|
if self.from_date and getdate(self.from_date) > getdate():
|
||||||
|
frappe.throw(_('From Date cannot be after the current date.'))
|
||||||
|
|
||||||
|
if self.to_date and getdate(self.to_date) > getdate():
|
||||||
|
frappe.throw(_('To Date cannot be after the current date.'))
|
||||||
|
|
||||||
|
if self.from_time and self.from_time > nowtime():
|
||||||
|
frappe.throw(_('From Time cannot be after the current time.'))
|
||||||
|
|
||||||
|
if self.to_time and self.to_time > nowtime():
|
||||||
|
frappe.throw(_('To Time cannot be after the current time.'))
|
||||||
|
|
||||||
|
def add_mo_to_table(self, orders):
|
||||||
|
# Add medication orders in the child table
|
||||||
|
self.set('medication_orders', [])
|
||||||
|
|
||||||
|
for data in orders:
|
||||||
|
self.append('medication_orders', {
|
||||||
|
'patient': data.patient,
|
||||||
|
'patient_name': data.patient_name,
|
||||||
|
'inpatient_record': data.inpatient_record,
|
||||||
|
'service_unit': data.service_unit,
|
||||||
|
'datetime': "%s %s" % (data.date, data.time or "00:00:00"),
|
||||||
|
'drug_code': data.drug,
|
||||||
|
'drug_name': data.drug_name,
|
||||||
|
'dosage': data.dosage,
|
||||||
|
'dosage_form': data.dosage_form,
|
||||||
|
'against_imo': data.parent,
|
||||||
|
'against_imoe': data.name
|
||||||
|
})
|
||||||
|
|
||||||
|
def on_submit(self):
|
||||||
|
self.validate_medication_orders()
|
||||||
|
success_msg = ""
|
||||||
|
if self.update_stock:
|
||||||
|
stock_entry = self.process_stock()
|
||||||
|
success_msg += _('Stock Entry {0} created and ').format(
|
||||||
|
frappe.bold(get_link_to_form('Stock Entry', stock_entry)))
|
||||||
|
|
||||||
|
self.update_medication_orders()
|
||||||
|
success_msg += _('Inpatient Medication Orders updated successfully')
|
||||||
|
frappe.msgprint(success_msg, title=_('Success'), indicator='green')
|
||||||
|
|
||||||
|
def validate_medication_orders(self):
|
||||||
|
for entry in self.medication_orders:
|
||||||
|
docstatus, is_completed = frappe.db.get_value('Inpatient Medication Order Entry', entry.against_imoe,
|
||||||
|
['docstatus', 'is_completed'])
|
||||||
|
|
||||||
|
if docstatus == 2:
|
||||||
|
frappe.throw(_('Row {0}: Cannot create Inpatient Medication Entry against cancelled Inpatient Medication Order {1}').format(
|
||||||
|
entry.idx, get_link_to_form(entry.against_imo)))
|
||||||
|
|
||||||
|
if is_completed:
|
||||||
|
frappe.throw(_('Row {0}: This Medication Order is already marked as completed').format(
|
||||||
|
entry.idx))
|
||||||
|
|
||||||
|
def on_cancel(self):
|
||||||
|
self.cancel_stock_entries()
|
||||||
|
self.update_medication_orders(on_cancel=True)
|
||||||
|
|
||||||
|
def process_stock(self):
|
||||||
|
allow_negative_stock = frappe.db.get_single_value('Stock Settings', 'allow_negative_stock')
|
||||||
|
if not allow_negative_stock:
|
||||||
|
self.check_stock_qty()
|
||||||
|
|
||||||
|
return self.make_stock_entry()
|
||||||
|
|
||||||
|
def update_medication_orders(self, on_cancel=False):
|
||||||
|
orders, order_entry_map = self.get_order_entry_map()
|
||||||
|
# mark completion status
|
||||||
|
is_completed = 1
|
||||||
|
if on_cancel:
|
||||||
|
is_completed = 0
|
||||||
|
|
||||||
|
frappe.db.sql("""
|
||||||
|
UPDATE `tabInpatient Medication Order Entry`
|
||||||
|
SET is_completed = %(is_completed)s
|
||||||
|
WHERE name IN %(orders)s
|
||||||
|
""", {'orders': orders, 'is_completed': is_completed})
|
||||||
|
|
||||||
|
# update status and completed orders count
|
||||||
|
for order, count in order_entry_map.items():
|
||||||
|
medication_order = frappe.get_doc('Inpatient Medication Order', order)
|
||||||
|
completed_orders = flt(count)
|
||||||
|
current_value = frappe.db.get_value('Inpatient Medication Order', order, 'completed_orders')
|
||||||
|
|
||||||
|
if on_cancel:
|
||||||
|
completed_orders = flt(current_value) - flt(count)
|
||||||
|
else:
|
||||||
|
completed_orders = flt(current_value) + flt(count)
|
||||||
|
|
||||||
|
medication_order.db_set('completed_orders', completed_orders)
|
||||||
|
medication_order.set_status()
|
||||||
|
|
||||||
|
def get_order_entry_map(self):
|
||||||
|
# for marking order completion status
|
||||||
|
orders = []
|
||||||
|
# orders mapped
|
||||||
|
order_entry_map = dict()
|
||||||
|
|
||||||
|
for entry in self.medication_orders:
|
||||||
|
orders.append(entry.against_imoe)
|
||||||
|
parent = entry.against_imo
|
||||||
|
if not order_entry_map.get(parent):
|
||||||
|
order_entry_map[parent] = 0
|
||||||
|
|
||||||
|
order_entry_map[parent] += 1
|
||||||
|
|
||||||
|
return orders, order_entry_map
|
||||||
|
|
||||||
|
def check_stock_qty(self):
|
||||||
|
from erpnext.stock.stock_ledger import NegativeStockError
|
||||||
|
|
||||||
|
drug_availability = dict()
|
||||||
|
for d in self.medication_orders:
|
||||||
|
if not drug_availability.get(d.drug_code):
|
||||||
|
drug_availability[d.drug_code] = 0
|
||||||
|
drug_availability[d.drug_code] += flt(d.dosage)
|
||||||
|
|
||||||
|
for drug, dosage in drug_availability.items():
|
||||||
|
available_qty = get_latest_stock_qty(drug, self.warehouse)
|
||||||
|
|
||||||
|
# validate qty
|
||||||
|
if flt(available_qty) < flt(dosage):
|
||||||
|
frappe.throw(_('Quantity not available for {0} in warehouse {1}').format(
|
||||||
|
frappe.bold(drug), frappe.bold(self.warehouse))
|
||||||
|
+ '<br><br>' + _('Available quantity is {0}, you need {1}').format(
|
||||||
|
frappe.bold(available_qty), frappe.bold(dosage))
|
||||||
|
+ '<br><br>' + _('Please enable Allow Negative Stock in Stock Settings or create Stock Entry to proceed.'),
|
||||||
|
NegativeStockError, title=_('Insufficient Stock'))
|
||||||
|
|
||||||
|
def make_stock_entry(self):
|
||||||
|
stock_entry = frappe.new_doc('Stock Entry')
|
||||||
|
stock_entry.purpose = 'Material Issue'
|
||||||
|
stock_entry.set_stock_entry_type()
|
||||||
|
stock_entry.from_warehouse = self.warehouse
|
||||||
|
stock_entry.company = self.company
|
||||||
|
stock_entry.inpatient_medication_entry = self.name
|
||||||
|
cost_center = frappe.get_cached_value('Company', self.company, 'cost_center')
|
||||||
|
expense_account = get_account(None, 'expense_account', 'Healthcare Settings', self.company)
|
||||||
|
|
||||||
|
for entry in self.medication_orders:
|
||||||
|
se_child = stock_entry.append('items')
|
||||||
|
se_child.item_code = entry.drug_code
|
||||||
|
se_child.item_name = entry.drug_name
|
||||||
|
se_child.uom = frappe.db.get_value('Item', entry.drug_code, 'stock_uom')
|
||||||
|
se_child.stock_uom = se_child.uom
|
||||||
|
se_child.qty = flt(entry.dosage)
|
||||||
|
# in stock uom
|
||||||
|
se_child.conversion_factor = 1
|
||||||
|
se_child.cost_center = cost_center
|
||||||
|
se_child.expense_account = expense_account
|
||||||
|
# references
|
||||||
|
se_child.patient = entry.patient
|
||||||
|
se_child.inpatient_medication_entry_child = entry.name
|
||||||
|
|
||||||
|
stock_entry.submit()
|
||||||
|
return stock_entry.name
|
||||||
|
|
||||||
|
def cancel_stock_entries(self):
|
||||||
|
stock_entries = frappe.get_all('Stock Entry', {'inpatient_medication_entry': self.name})
|
||||||
|
for entry in stock_entries:
|
||||||
|
doc = frappe.get_doc('Stock Entry', entry.name)
|
||||||
|
doc.cancel()
|
||||||
|
|
||||||
|
|
||||||
|
def get_pending_medication_orders(entry):
|
||||||
|
filters, values = get_filters(entry)
|
||||||
|
|
||||||
|
data = frappe.db.sql("""
|
||||||
|
SELECT
|
||||||
|
ip.inpatient_record, ip.patient, ip.patient_name,
|
||||||
|
entry.name, entry.parent, entry.drug, entry.drug_name,
|
||||||
|
entry.dosage, entry.dosage_form, entry.date, entry.time, entry.instructions
|
||||||
|
FROM
|
||||||
|
`tabInpatient Medication Order` ip
|
||||||
|
INNER JOIN
|
||||||
|
`tabInpatient Medication Order Entry` entry
|
||||||
|
ON
|
||||||
|
ip.name = entry.parent
|
||||||
|
WHERE
|
||||||
|
ip.docstatus = 1 and
|
||||||
|
ip.company = %(company)s and
|
||||||
|
entry.is_completed = 0
|
||||||
|
{0}
|
||||||
|
ORDER BY
|
||||||
|
entry.date, entry.time
|
||||||
|
""".format(filters), values, as_dict=1)
|
||||||
|
|
||||||
|
for doc in data:
|
||||||
|
inpatient_record = doc.inpatient_record
|
||||||
|
doc['service_unit'] = get_current_healthcare_service_unit(inpatient_record)
|
||||||
|
|
||||||
|
if entry.service_unit and doc.service_unit != entry.service_unit:
|
||||||
|
data.remove(doc)
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def get_filters(entry):
|
||||||
|
filters = ''
|
||||||
|
values = dict(company=entry.company)
|
||||||
|
if entry.from_date:
|
||||||
|
filters += ' and entry.date >= %(from_date)s'
|
||||||
|
values['from_date'] = entry.from_date
|
||||||
|
|
||||||
|
if entry.to_date:
|
||||||
|
filters += ' and entry.date <= %(to_date)s'
|
||||||
|
values['to_date'] = entry.to_date
|
||||||
|
|
||||||
|
if entry.from_time:
|
||||||
|
filters += ' and entry.time >= %(from_time)s'
|
||||||
|
values['from_time'] = entry.from_time
|
||||||
|
|
||||||
|
if entry.to_time:
|
||||||
|
filters += ' and entry.time <= %(to_time)s'
|
||||||
|
values['to_time'] = entry.to_time
|
||||||
|
|
||||||
|
if entry.patient:
|
||||||
|
filters += ' and ip.patient = %(patient)s'
|
||||||
|
values['patient'] = entry.patient
|
||||||
|
|
||||||
|
if entry.practitioner:
|
||||||
|
filters += ' and ip.practitioner = %(practitioner)s'
|
||||||
|
values['practitioner'] = entry.practitioner
|
||||||
|
|
||||||
|
if entry.item_code:
|
||||||
|
filters += ' and entry.drug = %(item_code)s'
|
||||||
|
values['item_code'] = entry.item_code
|
||||||
|
|
||||||
|
if entry.assigned_to_practitioner:
|
||||||
|
filters += ' and ip._assign LIKE %(assigned_to)s'
|
||||||
|
values['assigned_to'] = '%' + entry.assigned_to_practitioner + '%'
|
||||||
|
|
||||||
|
return filters, values
|
||||||
|
|
||||||
|
|
||||||
|
def get_current_healthcare_service_unit(inpatient_record):
|
||||||
|
ip_record = frappe.get_doc('Inpatient Record', inpatient_record)
|
||||||
|
return ip_record.inpatient_occupancies[-1].service_unit
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
from frappe import _
|
||||||
|
|
||||||
|
def get_data():
|
||||||
|
return {
|
||||||
|
'fieldname': 'against_imoe',
|
||||||
|
'internal_links': {
|
||||||
|
'Inpatient Medication Order': ['medication_orders', 'against_imo']
|
||||||
|
},
|
||||||
|
'transactions': [
|
||||||
|
{
|
||||||
|
'label': _('Reference'),
|
||||||
|
'items': ['Inpatient Medication Order']
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
|
# See license.txt
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
import unittest
|
||||||
|
from frappe.utils import add_days, getdate, now_datetime
|
||||||
|
from erpnext.healthcare.doctype.inpatient_record.test_inpatient_record import create_patient, create_inpatient, get_healthcare_service_unit, mark_invoiced_inpatient_occupancy
|
||||||
|
from erpnext.healthcare.doctype.inpatient_record.inpatient_record import admit_patient, discharge_patient, schedule_discharge
|
||||||
|
from erpnext.healthcare.doctype.inpatient_medication_order.test_inpatient_medication_order import create_ipmo, create_ipme
|
||||||
|
from erpnext.healthcare.doctype.healthcare_settings.healthcare_settings import get_account
|
||||||
|
|
||||||
|
class TestInpatientMedicationEntry(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
frappe.db.sql("""delete from `tabInpatient Record`""")
|
||||||
|
frappe.db.sql("""delete from `tabInpatient Medication Order`""")
|
||||||
|
frappe.db.sql("""delete from `tabInpatient Medication Entry`""")
|
||||||
|
self.patient = create_patient()
|
||||||
|
|
||||||
|
# Admit
|
||||||
|
ip_record = create_inpatient(self.patient)
|
||||||
|
ip_record.expected_length_of_stay = 0
|
||||||
|
ip_record.save()
|
||||||
|
ip_record.reload()
|
||||||
|
service_unit = get_healthcare_service_unit()
|
||||||
|
admit_patient(ip_record, service_unit, now_datetime())
|
||||||
|
self.ip_record = ip_record
|
||||||
|
|
||||||
|
def test_filters_for_fetching_pending_mo(self):
|
||||||
|
ipmo = create_ipmo(self.patient)
|
||||||
|
ipmo.submit()
|
||||||
|
ipmo.reload()
|
||||||
|
|
||||||
|
date = add_days(getdate(), -1)
|
||||||
|
filters = frappe._dict(
|
||||||
|
from_date=date,
|
||||||
|
to_date=date,
|
||||||
|
from_time='',
|
||||||
|
to_time='',
|
||||||
|
item_code='Dextromethorphan',
|
||||||
|
patient=self.patient
|
||||||
|
)
|
||||||
|
|
||||||
|
ipme = create_ipme(filters, update_stock=0)
|
||||||
|
|
||||||
|
# 3 dosages per day
|
||||||
|
self.assertEqual(len(ipme.medication_orders), 3)
|
||||||
|
self.assertEqual(getdate(ipme.medication_orders[0].datetime), date)
|
||||||
|
|
||||||
|
def test_ipme_with_stock_update(self):
|
||||||
|
ipmo = create_ipmo(self.patient)
|
||||||
|
ipmo.submit()
|
||||||
|
ipmo.reload()
|
||||||
|
|
||||||
|
date = add_days(getdate(), -1)
|
||||||
|
filters = frappe._dict(
|
||||||
|
from_date=date,
|
||||||
|
to_date=date,
|
||||||
|
from_time='',
|
||||||
|
to_time='',
|
||||||
|
item_code='Dextromethorphan',
|
||||||
|
patient=self.patient
|
||||||
|
)
|
||||||
|
|
||||||
|
make_stock_entry()
|
||||||
|
ipme = create_ipme(filters, update_stock=1)
|
||||||
|
ipme.submit()
|
||||||
|
ipme.reload()
|
||||||
|
|
||||||
|
# test order completed
|
||||||
|
is_order_completed = frappe.db.get_value('Inpatient Medication Order Entry',
|
||||||
|
ipme.medication_orders[0].against_imoe, 'is_completed')
|
||||||
|
self.assertEqual(is_order_completed, 1)
|
||||||
|
|
||||||
|
# test stock entry
|
||||||
|
stock_entry = frappe.db.exists('Stock Entry', {'inpatient_medication_entry': ipme.name})
|
||||||
|
self.assertTrue(stock_entry)
|
||||||
|
|
||||||
|
# check references
|
||||||
|
stock_entry = frappe.get_doc('Stock Entry', stock_entry)
|
||||||
|
self.assertEqual(stock_entry.items[0].patient, self.patient)
|
||||||
|
self.assertEqual(stock_entry.items[0].inpatient_medication_entry_child, ipme.medication_orders[0].name)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
# cleanup - Discharge
|
||||||
|
schedule_discharge(frappe.as_json({'patient': self.patient}))
|
||||||
|
self.ip_record.reload()
|
||||||
|
mark_invoiced_inpatient_occupancy(self.ip_record)
|
||||||
|
|
||||||
|
self.ip_record.reload()
|
||||||
|
discharge_patient(self.ip_record)
|
||||||
|
|
||||||
|
for entry in frappe.get_all('Inpatient Medication Entry'):
|
||||||
|
doc = frappe.get_doc('Inpatient Medication Entry', entry.name)
|
||||||
|
doc.cancel()
|
||||||
|
frappe.db.delete('Stock Entry', {'inpatient_medication_entry': doc.name})
|
||||||
|
doc.delete()
|
||||||
|
|
||||||
|
for entry in frappe.get_all('Inpatient Medication Order'):
|
||||||
|
doc = frappe.get_doc('Inpatient Medication Order', entry.name)
|
||||||
|
doc.cancel()
|
||||||
|
doc.delete()
|
||||||
|
|
||||||
|
def make_stock_entry():
|
||||||
|
frappe.db.set_value('Company', '_Test Company', {
|
||||||
|
'stock_adjustment_account': 'Stock Adjustment - _TC',
|
||||||
|
'default_inventory_account': 'Stock In Hand - _TC'
|
||||||
|
})
|
||||||
|
stock_entry = frappe.new_doc('Stock Entry')
|
||||||
|
stock_entry.stock_entry_type = 'Material Receipt'
|
||||||
|
stock_entry.company = '_Test Company'
|
||||||
|
stock_entry.to_warehouse = 'Stores - _TC'
|
||||||
|
expense_account = get_account(None, 'expense_account', 'Healthcare Settings', '_Test Company')
|
||||||
|
se_child = stock_entry.append('items')
|
||||||
|
se_child.item_code = 'Dextromethorphan'
|
||||||
|
se_child.item_name = 'Dextromethorphan'
|
||||||
|
se_child.uom = 'Nos'
|
||||||
|
se_child.stock_uom = 'Nos'
|
||||||
|
se_child.qty = 6
|
||||||
|
se_child.t_warehouse = 'Stores - _TC'
|
||||||
|
# in stock uom
|
||||||
|
se_child.conversion_factor = 1.0
|
||||||
|
se_child.expense_account = expense_account
|
||||||
|
stock_entry.submit()
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"creation": "2020-09-25 14:56:32.636569",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"patient",
|
||||||
|
"patient_name",
|
||||||
|
"inpatient_record",
|
||||||
|
"column_break_4",
|
||||||
|
"service_unit",
|
||||||
|
"datetime",
|
||||||
|
"medication_details_section",
|
||||||
|
"drug_code",
|
||||||
|
"drug_name",
|
||||||
|
"dosage",
|
||||||
|
"available_qty",
|
||||||
|
"dosage_form",
|
||||||
|
"column_break_10",
|
||||||
|
"instructions",
|
||||||
|
"references_section",
|
||||||
|
"against_imo",
|
||||||
|
"against_imoe"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"columns": 2,
|
||||||
|
"fieldname": "patient",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Patient",
|
||||||
|
"options": "Patient",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fetch_from": "patient.patient_name",
|
||||||
|
"fieldname": "patient_name",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Patient Name",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"columns": 2,
|
||||||
|
"fieldname": "drug_code",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Drug Code",
|
||||||
|
"options": "Item",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fetch_from": "drug_code.item_name",
|
||||||
|
"fieldname": "drug_name",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Drug Name",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"columns": 1,
|
||||||
|
"fieldname": "dosage",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Dosage",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "dosage_form",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Dosage Form",
|
||||||
|
"options": "Dosage Form"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fetch_from": "patient.inpatient_record",
|
||||||
|
"fieldname": "inpatient_record",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Inpatient Record",
|
||||||
|
"options": "Inpatient Record",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"collapsible": 1,
|
||||||
|
"fieldname": "references_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "References"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_4",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "medication_details_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Medication Details"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_10",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"columns": 3,
|
||||||
|
"fieldname": "datetime",
|
||||||
|
"fieldtype": "Datetime",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Datetime",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "instructions",
|
||||||
|
"fieldtype": "Small Text",
|
||||||
|
"label": "Instructions"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"columns": 2,
|
||||||
|
"fieldname": "service_unit",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Service Unit",
|
||||||
|
"options": "Healthcare Service Unit",
|
||||||
|
"read_only": 1,
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "against_imo",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Against Inpatient Medication Order",
|
||||||
|
"no_copy": 1,
|
||||||
|
"options": "Inpatient Medication Order",
|
||||||
|
"print_hide": 1,
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "against_imoe",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Against Inpatient Medication Order Entry",
|
||||||
|
"no_copy": 1,
|
||||||
|
"print_hide": 1,
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "available_qty",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"hidden": 1,
|
||||||
|
"label": "Available Qty",
|
||||||
|
"no_copy": 1,
|
||||||
|
"print_hide": 1,
|
||||||
|
"read_only": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"istable": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2020-09-30 14:48:23.648223",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Healthcare",
|
||||||
|
"name": "Inpatient Medication Entry Detail",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [],
|
||||||
|
"quick_entry": 1,
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"track_changes": 1
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
# import frappe
|
||||||
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
class InpatientMedicationEntryDetail(Document):
|
||||||
|
pass
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user