Merge branch 'develop' into skip-non-apply-tds-invoices-develop
This commit is contained in:
commit
8ba05dc674
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
|
@ -1,58 +1,126 @@
|
||||
{
|
||||
"custom_fields": [
|
||||
{
|
||||
"_assign": null,
|
||||
"_comments": null,
|
||||
"_liked_by": null,
|
||||
"_user_tags": null,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"collapsible_depends_on": null,
|
||||
"columns": 0,
|
||||
"creation": "2018-12-28 22:29:21.828090",
|
||||
"default": null,
|
||||
"depends_on": null,
|
||||
"description": null,
|
||||
"docstatus": 0,
|
||||
"dt": "Address",
|
||||
"fetch_from": null,
|
||||
"fieldname": "tax_category",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"idx": 14,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"insert_after": "fax",
|
||||
"label": "Tax Category",
|
||||
"modified": "2018-12-28 22:29:21.828090",
|
||||
"modified_by": "Administrator",
|
||||
"name": "Address-tax_category",
|
||||
"no_copy": 0,
|
||||
"options": "Tax Category",
|
||||
"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,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0,
|
||||
"_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": "2018-12-28 22:29:21.828090",
|
||||
"default": null,
|
||||
"depends_on": null,
|
||||
"description": null,
|
||||
"docstatus": 0,
|
||||
"dt": "Address",
|
||||
"fetch_from": null,
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "tax_category",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"hide_border": 0,
|
||||
"hide_days": 0,
|
||||
"hide_seconds": 0,
|
||||
"idx": 15,
|
||||
"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": "fax",
|
||||
"label": "Tax Category",
|
||||
"length": 0,
|
||||
"mandatory_depends_on": null,
|
||||
"modified": "2018-12-28 22:29:21.828090",
|
||||
"modified_by": "Administrator",
|
||||
"name": "Address-tax_category",
|
||||
"no_copy": 0,
|
||||
"options": "Tax Category",
|
||||
"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,
|
||||
"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,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0,
|
||||
"width": null
|
||||
}
|
||||
],
|
||||
"custom_perms": [],
|
||||
"doctype": "Address",
|
||||
"property_setters": [],
|
||||
],
|
||||
"custom_perms": [],
|
||||
"doctype": "Address",
|
||||
"property_setters": [],
|
||||
"sync_on_migrate": 1
|
||||
}
|
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,
|
||||
"shortcuts": [
|
||||
{
|
||||
"label": "Chart Of Accounts",
|
||||
"label": "Chart of Accounts",
|
||||
"link_to": "Account",
|
||||
"type": "DocType"
|
||||
},
|
||||
|
@ -2,7 +2,7 @@ frappe.provide("frappe.treeview_settings")
|
||||
|
||||
frappe.treeview_settings["Account"] = {
|
||||
breadcrumb: "Accounts",
|
||||
title: __("Chart Of Accounts"),
|
||||
title: __("Chart of Accounts"),
|
||||
get_tree_root: false,
|
||||
filters: [
|
||||
{
|
||||
@ -97,7 +97,7 @@ frappe.treeview_settings["Account"] = {
|
||||
treeview.page.add_inner_button(__("Journal Entry"), function() {
|
||||
frappe.new_doc('Journal Entry', {company: get_company()});
|
||||
}, __('Create'));
|
||||
treeview.page.add_inner_button(__("New Company"), function() {
|
||||
treeview.page.add_inner_button(__("Company"), function() {
|
||||
frappe.new_doc('Company');
|
||||
}, __('Create'));
|
||||
|
||||
|
@ -40,7 +40,7 @@
|
||||
"fields": [
|
||||
{
|
||||
"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",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
@ -48,23 +48,23 @@
|
||||
"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",
|
||||
"fieldtype": "Date",
|
||||
"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",
|
||||
"fieldname": "frozen_accounts_modifier",
|
||||
"fieldtype": "Link",
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"fieldtype": "Select",
|
||||
"label": "Determine Address Tax Category From",
|
||||
@ -75,7 +75,7 @@
|
||||
"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",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
@ -127,7 +127,7 @@
|
||||
"default": "0",
|
||||
"fieldname": "show_inclusive_tax_in_print",
|
||||
"fieldtype": "Check",
|
||||
"label": "Show Inclusive Tax In Print"
|
||||
"label": "Show Inclusive Tax in Print"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_12",
|
||||
@ -165,7 +165,7 @@
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"fieldtype": "Check",
|
||||
"label": "Use Custom Cash Flow Format"
|
||||
@ -177,7 +177,7 @@
|
||||
"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",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Over Billing Allowance (%)"
|
||||
@ -199,7 +199,7 @@
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"fieldtype": "Check",
|
||||
"label": "Book Deferred Entries Via Journal Entry"
|
||||
@ -214,7 +214,7 @@
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"fieldtype": "Select",
|
||||
"label": "Book Deferred Entries Based On",
|
||||
@ -226,7 +226,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2020-10-07 14:58:50.325577",
|
||||
"modified": "2020-10-13 11:32:52.268826",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Settings",
|
||||
@ -254,4 +254,4 @@
|
||||
"sort_field": "modified",
|
||||
"sort_order": "ASC",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
@ -23,13 +23,13 @@ class CashierClosing(Document):
|
||||
where posting_date=%s and posting_time>=%s and posting_time<=%s and owner=%s
|
||||
""", (self.date, self.from_time, self.time, self.user))
|
||||
self.outstanding_amount = flt(values[0][0] if values else 0)
|
||||
|
||||
|
||||
def make_calculations(self):
|
||||
total = 0.00
|
||||
for i in self.payments:
|
||||
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):
|
||||
if self.from_time >= self.time:
|
||||
|
@ -195,7 +195,7 @@ def build_response_as_excel(writer):
|
||||
reader = csv.reader(f)
|
||||
|
||||
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()
|
||||
os.remove(filename)
|
||||
|
@ -210,7 +210,7 @@ erpnext.accounts.JournalEntry = frappe.ui.form.Controller.extend({
|
||||
$.each(this.frm.doc.accounts || [], function(i, 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());
|
||||
}
|
||||
},
|
||||
|
@ -195,88 +195,91 @@ def create_sales_invoice_record(qty=1):
|
||||
|
||||
def create_records():
|
||||
# create a new loyalty Account
|
||||
if frappe.db.exists("Account", "Loyalty - _TC"):
|
||||
return
|
||||
|
||||
frappe.get_doc({
|
||||
"doctype": "Account",
|
||||
"account_name": "Loyalty",
|
||||
"parent_account": "Direct Expenses - _TC",
|
||||
"company": "_Test Company",
|
||||
"is_group": 0,
|
||||
"account_type": "Expense Account",
|
||||
}).insert()
|
||||
if not frappe.db.exists("Account", "Loyalty - _TC"):
|
||||
frappe.get_doc({
|
||||
"doctype": "Account",
|
||||
"account_name": "Loyalty",
|
||||
"parent_account": "Direct Expenses - _TC",
|
||||
"company": "_Test Company",
|
||||
"is_group": 0,
|
||||
"account_type": "Expense Account",
|
||||
}).insert()
|
||||
|
||||
# create a new loyalty program Single tier
|
||||
frappe.get_doc({
|
||||
"doctype": "Loyalty Program",
|
||||
"loyalty_program_name": "Test Single Loyalty",
|
||||
"auto_opt_in": 1,
|
||||
"from_date": today(),
|
||||
"loyalty_program_type": "Single Tier Program",
|
||||
"conversion_factor": 1,
|
||||
"expiry_duration": 10,
|
||||
"company": "_Test Company",
|
||||
"cost_center": "Main - _TC",
|
||||
"expense_account": "Loyalty - _TC",
|
||||
"collection_rules": [{
|
||||
'tier_name': 'Silver',
|
||||
'collection_factor': 1000,
|
||||
'min_spent': 1000
|
||||
}]
|
||||
}).insert()
|
||||
|
||||
# create a new customer
|
||||
frappe.get_doc({
|
||||
"customer_group": "_Test Customer Group",
|
||||
"customer_name": "Test Loyalty Customer",
|
||||
"customer_type": "Individual",
|
||||
"doctype": "Customer",
|
||||
"territory": "_Test Territory"
|
||||
}).insert()
|
||||
|
||||
# create a new loyalty program Multiple tier
|
||||
frappe.get_doc({
|
||||
"doctype": "Loyalty Program",
|
||||
"loyalty_program_name": "Test Multiple Loyalty",
|
||||
"auto_opt_in": 1,
|
||||
"from_date": today(),
|
||||
"loyalty_program_type": "Multiple Tier Program",
|
||||
"conversion_factor": 1,
|
||||
"expiry_duration": 10,
|
||||
"company": "_Test Company",
|
||||
"cost_center": "Main - _TC",
|
||||
"expense_account": "Loyalty - _TC",
|
||||
"collection_rules": [
|
||||
{
|
||||
if not frappe.db.exists("Loyalty Program","Test Single Loyalty"):
|
||||
frappe.get_doc({
|
||||
"doctype": "Loyalty Program",
|
||||
"loyalty_program_name": "Test Single Loyalty",
|
||||
"auto_opt_in": 1,
|
||||
"from_date": today(),
|
||||
"loyalty_program_type": "Single Tier Program",
|
||||
"conversion_factor": 1,
|
||||
"expiry_duration": 10,
|
||||
"company": "_Test Company",
|
||||
"cost_center": "Main - _TC",
|
||||
"expense_account": "Loyalty - _TC",
|
||||
"collection_rules": [{
|
||||
'tier_name': 'Silver',
|
||||
'collection_factor': 1000,
|
||||
'min_spent': 10000
|
||||
},
|
||||
{
|
||||
'tier_name': 'Gold',
|
||||
'collection_factor': 1000,
|
||||
'min_spent': 19000
|
||||
}
|
||||
]
|
||||
}).insert()
|
||||
'min_spent': 1000
|
||||
}]
|
||||
}).insert()
|
||||
|
||||
# create a new customer
|
||||
if not frappe.db.exists("Customer","Test Loyalty Customer"):
|
||||
frappe.get_doc({
|
||||
"customer_group": "_Test Customer Group",
|
||||
"customer_name": "Test Loyalty Customer",
|
||||
"customer_type": "Individual",
|
||||
"doctype": "Customer",
|
||||
"territory": "_Test Territory"
|
||||
}).insert()
|
||||
|
||||
# create a new loyalty program Multiple tier
|
||||
if not frappe.db.exists("Loyalty Program","Test Multiple Loyalty"):
|
||||
frappe.get_doc({
|
||||
"doctype": "Loyalty Program",
|
||||
"loyalty_program_name": "Test Multiple Loyalty",
|
||||
"auto_opt_in": 1,
|
||||
"from_date": today(),
|
||||
"loyalty_program_type": "Multiple Tier Program",
|
||||
"conversion_factor": 1,
|
||||
"expiry_duration": 10,
|
||||
"company": "_Test Company",
|
||||
"cost_center": "Main - _TC",
|
||||
"expense_account": "Loyalty - _TC",
|
||||
"collection_rules": [
|
||||
{
|
||||
'tier_name': 'Silver',
|
||||
'collection_factor': 1000,
|
||||
'min_spent': 10000
|
||||
},
|
||||
{
|
||||
'tier_name': 'Gold',
|
||||
'collection_factor': 1000,
|
||||
'min_spent': 19000
|
||||
}
|
||||
]
|
||||
}).insert()
|
||||
|
||||
# create an item
|
||||
item = frappe.get_doc({
|
||||
"doctype": "Item",
|
||||
"item_code": "Loyal Item",
|
||||
"item_name": "Loyal Item",
|
||||
"item_group": "All Item Groups",
|
||||
"company": "_Test Company",
|
||||
"is_stock_item": 1,
|
||||
"opening_stock": 100,
|
||||
"valuation_rate": 10000,
|
||||
}).insert()
|
||||
if not frappe.db.exists("Item", "Loyal Item"):
|
||||
frappe.get_doc({
|
||||
"doctype": "Item",
|
||||
"item_code": "Loyal Item",
|
||||
"item_name": "Loyal Item",
|
||||
"item_group": "All Item Groups",
|
||||
"company": "_Test Company",
|
||||
"is_stock_item": 1,
|
||||
"opening_stock": 100,
|
||||
"valuation_rate": 10000,
|
||||
}).insert()
|
||||
|
||||
# create item price
|
||||
frappe.get_doc({
|
||||
"doctype": "Item Price",
|
||||
"price_list": "Standard Selling",
|
||||
"item_code": item.item_code,
|
||||
"price_list_rate": 10000
|
||||
}).insert()
|
||||
if not frappe.db.exists("Item Price", {"price_list": "Standard Selling", "item_code": "Loyal Item"}):
|
||||
frappe.get_doc({
|
||||
"doctype": "Item Price",
|
||||
"price_list": "Standard Selling",
|
||||
"item_code": "Loyal Item",
|
||||
"price_list_rate": 10000
|
||||
}).insert()
|
||||
|
@ -1,4 +1,5 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_import": 1,
|
||||
"allow_rename": 1,
|
||||
"autoname": "field:mode_of_payment",
|
||||
@ -28,7 +29,7 @@
|
||||
"fieldtype": "Select",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Type",
|
||||
"options": "Cash\nBank\nGeneral"
|
||||
"options": "Cash\nBank\nGeneral\nPhone"
|
||||
},
|
||||
{
|
||||
"fieldname": "accounts",
|
||||
@ -45,7 +46,9 @@
|
||||
],
|
||||
"icon": "fa fa-credit-card",
|
||||
"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",
|
||||
"module": "Accounts",
|
||||
"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) {
|
||||
return {
|
||||
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) {
|
||||
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) {
|
||||
frm.disable_save();
|
||||
frm.trigger("make_dashboard");
|
||||
!frm.doc.import_in_progress && frm.trigger("make_dashboard");
|
||||
frm.page.set_primary_action(__('Create Invoices'), () => {
|
||||
let btn_primary = frm.page.btn_primary.get(0);
|
||||
return frm.call({
|
||||
doc: frm.doc,
|
||||
freeze: true,
|
||||
btn: $(btn_primary),
|
||||
method: "make_invoices",
|
||||
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();
|
||||
}
|
||||
}
|
||||
freeze_message: __("Creating {0} Invoice", [frm.doc.invoice_type])
|
||||
});
|
||||
});
|
||||
|
||||
if (frm.doc.create_missing_party) {
|
||||
frm.set_df_property("party", "fieldtype", "Data", frm.doc.name, "invoices");
|
||||
}
|
||||
},
|
||||
|
||||
setup_company_filters: function(frm) {
|
||||
|
@ -4,9 +4,12 @@
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
import traceback
|
||||
from json import dumps
|
||||
from frappe import _, scrub
|
||||
from frappe.utils import flt, nowdate
|
||||
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
|
||||
|
||||
|
||||
@ -61,67 +64,48 @@ class OpeningInvoiceCreationTool(Document):
|
||||
prepare_invoice_summary(doctype, invoices)
|
||||
|
||||
return invoices_summary, max_count
|
||||
|
||||
def make_invoices(self):
|
||||
names = []
|
||||
mandatory_error_msg = _("Row {0}: {1} is required to create the Opening {2} Invoices")
|
||||
|
||||
def validate_company(self):
|
||||
if not self.company:
|
||||
frappe.throw(_("Please select the Company"))
|
||||
|
||||
def set_missing_values(self, row):
|
||||
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()
|
||||
|
||||
company_details = frappe.get_cached_value('Company', self.company,
|
||||
["default_currency", "default_letter_head"], as_dict=1) or {}
|
||||
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:
|
||||
if not row.qty:
|
||||
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:
|
||||
if not row:
|
||||
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:
|
||||
args.update({
|
||||
invoice.update({
|
||||
"currency": company_details.get("default_currency"),
|
||||
"letter_head": company_details.get("default_letter_head")
|
||||
})
|
||||
invoices.append(invoice)
|
||||
|
||||
doc = frappe.get_doc(args).insert()
|
||||
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
|
||||
return invoices
|
||||
|
||||
def add_party(self, party_type, party):
|
||||
party_doc = frappe.new_doc(party_type)
|
||||
@ -140,14 +124,12 @@ class OpeningInvoiceCreationTool(Document):
|
||||
|
||||
def get_invoice_dict(self, row=None):
|
||||
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:
|
||||
frappe.throw(
|
||||
_("Please set the Default Cost Center in {0} company.").format(frappe.bold(self.company))
|
||||
)
|
||||
frappe.throw(_("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)
|
||||
|
||||
return frappe._dict({
|
||||
@ -161,18 +143,9 @@ class OpeningInvoiceCreationTool(Document):
|
||||
"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()
|
||||
|
||||
args = frappe._dict({
|
||||
invoice = frappe._dict({
|
||||
"items": [item],
|
||||
"is_opening": "Yes",
|
||||
"set_posting_time": 1,
|
||||
@ -180,21 +153,76 @@ class OpeningInvoiceCreationTool(Document):
|
||||
"cost_center": self.cost_center,
|
||||
"due_date": row.due_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"
|
||||
})
|
||||
|
||||
accounting_dimension = get_accounting_dimensions()
|
||||
|
||||
for dimension in accounting_dimension:
|
||||
args.update({
|
||||
invoice.update({
|
||||
dimension: item.get(dimension)
|
||||
})
|
||||
|
||||
if self.invoice_type == "Sales":
|
||||
args["is_pos"] = 0
|
||||
return invoice
|
||||
|
||||
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()
|
||||
def get_temporary_opening_account(company=None):
|
||||
|
@ -44,7 +44,7 @@ class TestOpeningInvoiceCreationTool(unittest.TestCase):
|
||||
0: ["_Test Supplier", 300, "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):
|
||||
party = "Customer" if args.get("invoice_type", "Sales") == "Sales" else "Supplier"
|
||||
|
@ -1,313 +1,98 @@
|
||||
{
|
||||
"allow_copy": 0,
|
||||
"allow_guest_to_view": 0,
|
||||
"allow_import": 0,
|
||||
"allow_rename": 0,
|
||||
"beta": 0,
|
||||
"creation": "2015-12-23 21:31:52.699821",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "",
|
||||
"editable_grid": 1,
|
||||
"actions": [],
|
||||
"creation": "2015-12-23 21:31:52.699821",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"field_order": [
|
||||
"payment_gateway",
|
||||
"payment_channel",
|
||||
"is_default",
|
||||
"column_break_4",
|
||||
"payment_account",
|
||||
"currency",
|
||||
"payment_request_message",
|
||||
"message",
|
||||
"message_examples"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "payment_gateway",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"fieldname": "payment_gateway",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Payment Gateway",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Payment Gateway",
|
||||
"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
|
||||
},
|
||||
"label": "Payment Gateway",
|
||||
"options": "Payment Gateway",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "is_default",
|
||||
"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": "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
|
||||
},
|
||||
"default": "0",
|
||||
"fieldname": "is_default",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Default"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "column_break_4",
|
||||
"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
|
||||
},
|
||||
"fieldname": "column_break_4",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "payment_account",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"fieldname": "payment_account",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Payment Account",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Account",
|
||||
"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
|
||||
},
|
||||
"label": "Payment Account",
|
||||
"options": "Account",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fetch_from": "payment_account.account_currency",
|
||||
"fieldname": "currency",
|
||||
"fieldtype": "Read Only",
|
||||
"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": "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
|
||||
},
|
||||
"fieldtype": "Read Only",
|
||||
"label": "Currency"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "payment_request_message",
|
||||
"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
|
||||
},
|
||||
"depends_on": "eval: doc.payment_channel !== \"Phone\"",
|
||||
"fieldname": "payment_request_message",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"fieldname": "message",
|
||||
"fieldtype": "Small Text",
|
||||
"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": "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
|
||||
},
|
||||
"default": "Please click on the link below to make your payment",
|
||||
"fieldname": "message",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Default Payment Request Message"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "message_examples",
|
||||
"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",
|
||||
"length": 0,
|
||||
"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,
|
||||
"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
|
||||
"fieldname": "message_examples",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Message Examples",
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"default": "Email",
|
||||
"fieldname": "payment_channel",
|
||||
"fieldtype": "Select",
|
||||
"label": "Payment Channel",
|
||||
"options": "\nEmail\nPhone"
|
||||
}
|
||||
],
|
||||
"has_web_view": 0,
|
||||
"hide_heading": 0,
|
||||
"hide_toolbar": 0,
|
||||
"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",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Gateway Account",
|
||||
"name_case": "",
|
||||
"owner": "Administrator",
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2020-09-20 13:30:27.722852",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Gateway Account",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"amend": 0,
|
||||
"cancel": 0,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"if_owner": 0,
|
||||
"import": 0,
|
||||
"permlevel": 0,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Accounts Manager",
|
||||
"set_user_permissions": 0,
|
||||
"share": 1,
|
||||
"submit": 0,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Accounts Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 0,
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"show_name_in_global_search": 0,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 0,
|
||||
"track_seen": 0
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC"
|
||||
}
|
@ -37,6 +37,11 @@ frappe.ui.form.on("Payment Reconciliation Payment", {
|
||||
erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.extend({
|
||||
onload: function() {
|
||||
var me = this;
|
||||
|
||||
this.frm.set_query("party", function() {
|
||||
check_mandatory(me.frm);
|
||||
});
|
||||
|
||||
this.frm.set_query("party_type", function() {
|
||||
return {
|
||||
"filters": {
|
||||
@ -46,37 +51,39 @@ erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.ext
|
||||
});
|
||||
|
||||
this.frm.set_query('receivable_payable_account', function() {
|
||||
if(!me.frm.doc.company || !me.frm.doc.party_type) {
|
||||
frappe.msgprint(__("Please select Company and Party Type first"));
|
||||
} else {
|
||||
return{
|
||||
filters: {
|
||||
"company": me.frm.doc.company,
|
||||
"is_group": 0,
|
||||
"account_type": frappe.boot.party_account_types[me.frm.doc.party_type]
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
check_mandatory(me.frm);
|
||||
return {
|
||||
filters: {
|
||||
"company": me.frm.doc.company,
|
||||
"is_group": 0,
|
||||
"account_type": frappe.boot.party_account_types[me.frm.doc.party_type]
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
this.frm.set_query('bank_cash_account', function() {
|
||||
if(!me.frm.doc.company) {
|
||||
frappe.msgprint(__("Please select Company first"));
|
||||
} else {
|
||||
return{
|
||||
filters:[
|
||||
['Account', 'company', '=', me.frm.doc.company],
|
||||
['Account', 'is_group', '=', 0],
|
||||
['Account', 'account_type', 'in', ['Bank', 'Cash']]
|
||||
]
|
||||
};
|
||||
}
|
||||
check_mandatory(me.frm, true);
|
||||
return {
|
||||
filters:[
|
||||
['Account', 'company', '=', me.frm.doc.company],
|
||||
['Account', 'is_group', '=', 0],
|
||||
['Account', 'account_type', 'in', ['Bank', 'Cash']]
|
||||
]
|
||||
};
|
||||
});
|
||||
|
||||
this.frm.set_value('party_type', '');
|
||||
this.frm.set_value('party', '');
|
||||
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() {
|
||||
@ -90,7 +97,7 @@ erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.ext
|
||||
|
||||
party: function() {
|
||||
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({
|
||||
method: "erpnext.accounts.party.get_party_account",
|
||||
args: {
|
||||
@ -99,7 +106,7 @@ erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.ext
|
||||
party: me.frm.doc.party
|
||||
},
|
||||
callback: function(r) {
|
||||
if(!r.exc && r.message) {
|
||||
if (!r.exc && 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 `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`.is_cancelled = 0
|
||||
GROUP BY `tab{doc}`.name
|
||||
Having
|
||||
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) {
|
||||
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){
|
||||
frm.add_custom_button(__('Resend Payment Email'), function(){
|
||||
frappe.call({
|
||||
|
@ -48,6 +48,7 @@
|
||||
"section_break_7",
|
||||
"payment_gateway",
|
||||
"payment_account",
|
||||
"payment_channel",
|
||||
"payment_order",
|
||||
"amended_from"
|
||||
],
|
||||
@ -230,6 +231,7 @@
|
||||
"label": "Recipient Message And Payment Details"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.payment_channel != \"Phone\"",
|
||||
"fieldname": "print_format",
|
||||
"fieldtype": "Select",
|
||||
"label": "Print Format"
|
||||
@ -241,6 +243,7 @@
|
||||
"label": "To"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.payment_channel != \"Phone\"",
|
||||
"fieldname": "subject",
|
||||
"fieldtype": "Data",
|
||||
"in_global_search": 1,
|
||||
@ -277,16 +280,18 @@
|
||||
"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",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.payment_channel != \"Phone\"",
|
||||
"fieldname": "message",
|
||||
"fieldtype": "Text",
|
||||
"label": "Message"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.payment_channel != \"Phone\"",
|
||||
"fieldname": "message_examples",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Message Examples",
|
||||
@ -347,12 +352,21 @@
|
||||
"options": "Payment Request",
|
||||
"print_hide": 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,
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-07-17 14:06:42.185763",
|
||||
"modified": "2020-09-18 12:24:14.178853",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Request",
|
||||
|
@ -36,7 +36,7 @@ class PaymentRequest(Document):
|
||||
ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name)
|
||||
if (hasattr(ref_doc, "order_type") \
|
||||
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:
|
||||
frappe.throw(_("Total Payment Request amount cannot be greater than {0} amount")
|
||||
@ -76,11 +76,25 @@ class PaymentRequest(Document):
|
||||
or self.flags.mute_email:
|
||||
send_mail = False
|
||||
|
||||
if send_mail:
|
||||
if send_mail and self.payment_channel != "Phone":
|
||||
self.set_payment_request_url()
|
||||
self.send_email()
|
||||
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):
|
||||
self.check_if_payment_entry_exists()
|
||||
self.set_as_cancelled()
|
||||
@ -105,13 +119,14 @@ class PaymentRequest(Document):
|
||||
return False
|
||||
|
||||
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()
|
||||
|
||||
if 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')
|
||||
|
||||
def get_payment_url(self):
|
||||
@ -140,10 +155,14 @@ class PaymentRequest(Document):
|
||||
})
|
||||
|
||||
def set_as_paid(self):
|
||||
payment_entry = self.create_payment_entry()
|
||||
self.make_invoice()
|
||||
if self.payment_channel == "Phone":
|
||||
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):
|
||||
"""create entry"""
|
||||
@ -151,7 +170,7 @@ class PaymentRequest(Document):
|
||||
|
||||
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
|
||||
elif self.reference_doctype == "Purchase Invoice":
|
||||
party_account = ref_doc.credit_to
|
||||
@ -166,8 +185,8 @@ class PaymentRequest(Document):
|
||||
else:
|
||||
party_amount = self.grand_total
|
||||
|
||||
payment_entry = get_payment_entry(self.reference_doctype, self.reference_name,
|
||||
party_amount=party_amount, bank_account=self.payment_account, bank_amount=bank_amount)
|
||||
payment_entry = get_payment_entry(self.reference_doctype, self.reference_name, party_amount=party_amount,
|
||||
bank_account=self.payment_account, bank_amount=bank_amount)
|
||||
|
||||
payment_entry.update({
|
||||
"reference_no": self.name,
|
||||
@ -255,7 +274,7 @@ class PaymentRequest(Document):
|
||||
|
||||
# if shopping cart enabled and in 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
|
||||
if success_url:
|
||||
@ -280,7 +299,9 @@ def make_payment_request(**args):
|
||||
args = frappe._dict(args)
|
||||
|
||||
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":
|
||||
from erpnext.accounts.doctype.loyalty_program.loyalty_program import validate_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)
|
||||
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'))
|
||||
if args.get('party_type') else '')
|
||||
|
||||
@ -314,9 +333,11 @@ def make_payment_request(**args):
|
||||
"payment_gateway_account": gateway_account.get("name"),
|
||||
"payment_gateway": gateway_account.get("payment_gateway"),
|
||||
"payment_account": gateway_account.get("payment_account"),
|
||||
"payment_channel": gateway_account.get("payment_channel"),
|
||||
"payment_request_type": args.get("payment_request_type"),
|
||||
"currency": ref_doc.currency,
|
||||
"grand_total": grand_total,
|
||||
"mode_of_payment": args.mode_of_payment,
|
||||
"email_to": args.recipient_id or ref_doc.owner,
|
||||
"subject": _("Payment Request for {0}").format(args.dn),
|
||||
"message": gateway_account.get("message") or get_dummy_message(ref_doc),
|
||||
@ -344,7 +365,7 @@ def make_payment_request(**args):
|
||||
|
||||
return pr.as_dict()
|
||||
|
||||
def get_amount(ref_doc):
|
||||
def get_amount(ref_doc, payment_account=None):
|
||||
"""get amount based on doctype"""
|
||||
dt = ref_doc.doctype
|
||||
if dt in ["Sales Order", "Purchase Order"]:
|
||||
@ -356,6 +377,12 @@ def get_amount(ref_doc):
|
||||
else:
|
||||
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":
|
||||
grand_total = ref_doc.outstanding_amount
|
||||
|
||||
@ -366,6 +393,10 @@ def get_amount(ref_doc):
|
||||
frappe.throw(_("Payment Entry is already created"))
|
||||
|
||||
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("""
|
||||
select sum(grand_total)
|
||||
from `tabPayment Request`
|
||||
@ -373,7 +404,9 @@ def get_existing_payment_request_amount(ref_dt, ref_dn):
|
||||
reference_doctype = %s
|
||||
and reference_name = %s
|
||||
and docstatus = 1
|
||||
and status != 'Paid'
|
||||
and (status != 'Paid'
|
||||
or (payment_channel = 'Phone'
|
||||
and status = 'Paid'))
|
||||
""", (ref_dt, ref_dn))
|
||||
return flt(existing_payment_request_amount[0][0]) if existing_payment_request_amount else 0
|
||||
|
||||
|
@ -45,6 +45,7 @@
|
||||
"unique": 0
|
||||
},
|
||||
{
|
||||
"description": "Provide the invoice portion in percent",
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 1,
|
||||
@ -170,6 +171,7 @@
|
||||
"unique": 0
|
||||
},
|
||||
{
|
||||
"description": "Give number of days according to prior selection",
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 1,
|
||||
@ -305,7 +307,7 @@
|
||||
"issingle": 0,
|
||||
"istable": 0,
|
||||
"max_attachments": 0,
|
||||
"modified": "2018-03-08 10:47:32.830478",
|
||||
"modified": "2020-10-14 10:47:32.830478",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Term",
|
||||
@ -381,4 +383,4 @@
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1,
|
||||
"track_seen": 0
|
||||
}
|
||||
}
|
||||
|
@ -51,6 +51,7 @@ frappe.ui.form.on('POS Closing Entry', {
|
||||
args: {
|
||||
start: frappe.datetime.get_datetime_as_string(frm.doc.period_start_date),
|
||||
end: frappe.datetime.get_datetime_as_string(frm.doc.period_end_date),
|
||||
pos_profile: frm.doc.pos_profile,
|
||||
user: frm.doc.user
|
||||
},
|
||||
callback: (r) => {
|
||||
|
@ -14,19 +14,51 @@ from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import
|
||||
|
||||
class POSClosingEntry(Document):
|
||||
def validate(self):
|
||||
user = frappe.get_all('POS Closing Entry',
|
||||
filters = { 'user': self.user, 'docstatus': 1 },
|
||||
if frappe.db.get_value("POS Opening Entry", self.pos_opening_entry, "status") != "Open":
|
||||
frappe.throw(_("Selected POS Opening Entry should be open."), title=_("Invalid Opening Entry"))
|
||||
|
||||
self.validate_pos_closing()
|
||||
self.validate_pos_invoices()
|
||||
|
||||
def validate_pos_closing(self):
|
||||
user = frappe.get_all("POS Closing Entry",
|
||||
filters = { "user": self.user, "docstatus": 1, "pos_profile": self.pos_profile },
|
||||
or_filters = {
|
||||
'period_start_date': ('between', [self.period_start_date, self.period_end_date]),
|
||||
'period_end_date': ('between', [self.period_start_date, self.period_end_date])
|
||||
"period_start_date": ("between", [self.period_start_date, self.period_end_date]),
|
||||
"period_end_date": ("between", [self.period_start_date, self.period_end_date])
|
||||
})
|
||||
|
||||
if user:
|
||||
frappe.throw(_("POS Closing Entry {} against {} between selected period"
|
||||
.format(frappe.bold("already exists"), frappe.bold(self.user))), title=_("Invalid Period"))
|
||||
bold_already_exists = frappe.bold(_("already exists"))
|
||||
bold_user = frappe.bold(self.user)
|
||||
frappe.throw(_("POS Closing Entry {} against {} between selected period")
|
||||
.format(bold_already_exists, bold_user), title=_("Invalid Period"))
|
||||
|
||||
def validate_pos_invoices(self):
|
||||
invalid_rows = []
|
||||
for d in self.pos_transactions:
|
||||
invalid_row = {'idx': d.idx}
|
||||
pos_invoice = frappe.db.get_values("POS Invoice", d.pos_invoice,
|
||||
["consolidated_invoice", "pos_profile", "docstatus", "owner"], as_dict=1)[0]
|
||||
if pos_invoice.consolidated_invoice:
|
||||
invalid_row.setdefault('msg', []).append(_('POS Invoice is {}').format(frappe.bold("already consolidated")))
|
||||
invalid_rows.append(invalid_row)
|
||||
continue
|
||||
if pos_invoice.pos_profile != self.pos_profile:
|
||||
invalid_row.setdefault('msg', []).append(_("POS Profile doesn't matches {}").format(frappe.bold(self.pos_profile)))
|
||||
if pos_invoice.docstatus != 1:
|
||||
invalid_row.setdefault('msg', []).append(_('POS Invoice is not {}').format(frappe.bold("submitted")))
|
||||
if pos_invoice.owner != self.user:
|
||||
invalid_row.setdefault('msg', []).append(_("POS Invoice isn't created by user {}").format(frappe.bold(self.owner)))
|
||||
|
||||
if frappe.db.get_value("POS Opening Entry", self.pos_opening_entry, "status") != "Open":
|
||||
frappe.throw(_("Selected POS Opening Entry should be open."), title=_("Invalid Opening Entry"))
|
||||
if invalid_row.get('msg'):
|
||||
invalid_rows.append(invalid_row)
|
||||
|
||||
if not invalid_rows:
|
||||
return
|
||||
|
||||
error_list = [_("Row #{}: {}").format(row.get('idx'), row.get('msg')) for row in invalid_rows]
|
||||
frappe.throw(error_list, title=_("Invalid POS Invoices"), as_list=True)
|
||||
|
||||
def on_submit(self):
|
||||
merge_pos_invoices(self.pos_transactions)
|
||||
@ -47,16 +79,15 @@ def get_cashiers(doctype, txt, searchfield, start, page_len, filters):
|
||||
return [c['user'] for c in cashiers_list]
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_pos_invoices(start, end, user):
|
||||
def get_pos_invoices(start, end, pos_profile, user):
|
||||
data = frappe.db.sql("""
|
||||
select
|
||||
name, timestamp(posting_date, posting_time) as "timestamp"
|
||||
from
|
||||
`tabPOS Invoice`
|
||||
where
|
||||
owner = %s and docstatus = 1 and
|
||||
(consolidated_invoice is NULL or consolidated_invoice = '')
|
||||
""", (user), as_dict=1)
|
||||
owner = %s and docstatus = 1 and pos_profile = %s and ifnull(consolidated_invoice,'') = ''
|
||||
""", (user, pos_profile), as_dict=1)
|
||||
|
||||
data = list(filter(lambda d: get_datetime(start) <= get_datetime(d.timestamp) <= get_datetime(end), data))
|
||||
# need to get taxes and payments so can't avoid get_doc
|
||||
@ -76,7 +107,8 @@ def make_closing_entry_from_opening(opening_entry):
|
||||
closing_entry.net_total = 0
|
||||
closing_entry.total_quantity = 0
|
||||
|
||||
invoices = get_pos_invoices(closing_entry.period_start_date, closing_entry.period_end_date, closing_entry.user)
|
||||
invoices = get_pos_invoices(closing_entry.period_start_date, closing_entry.period_end_date,
|
||||
closing_entry.pos_profile, closing_entry.user)
|
||||
|
||||
pos_transactions = []
|
||||
taxes = []
|
||||
|
@ -45,7 +45,7 @@ class TestPOSClosingEntry(unittest.TestCase):
|
||||
frappe.set_user("Administrator")
|
||||
frappe.db.sql("delete from `tabPOS Profile`")
|
||||
|
||||
def init_user_and_profile():
|
||||
def init_user_and_profile(**args):
|
||||
user = 'test@example.com'
|
||||
test_user = frappe.get_doc('User', user)
|
||||
|
||||
@ -53,7 +53,7 @@ def init_user_and_profile():
|
||||
test_user.add_roles(*roles)
|
||||
frappe.set_user(user)
|
||||
|
||||
pos_profile = make_pos_profile()
|
||||
pos_profile = make_pos_profile(**args)
|
||||
pos_profile.append('applicable_for_users', {
|
||||
'default': 1,
|
||||
'user': user
|
||||
|
@ -7,8 +7,8 @@
|
||||
"field_order": [
|
||||
"mode_of_payment",
|
||||
"opening_amount",
|
||||
"closing_amount",
|
||||
"expected_amount",
|
||||
"closing_amount",
|
||||
"difference"
|
||||
],
|
||||
"fields": [
|
||||
@ -26,8 +26,7 @@
|
||||
"in_list_view": 1,
|
||||
"label": "Expected Amount",
|
||||
"options": "company:company_currency",
|
||||
"read_only": 1,
|
||||
"reqd": 1
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "difference",
|
||||
@ -55,9 +54,10 @@
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-05-29 15:03:34.533607",
|
||||
"modified": "2020-10-23 16:45:43.662034",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Closing Entry Detail",
|
||||
|
@ -9,80 +9,63 @@ erpnext.selling.POSInvoiceController = erpnext.selling.SellingController.extend(
|
||||
this._super(doc);
|
||||
},
|
||||
|
||||
onload() {
|
||||
onload(doc) {
|
||||
this._super();
|
||||
if(this.frm.doc.__islocal && this.frm.doc.is_pos) {
|
||||
//Load pos profile data on the invoice if the default value of Is POS is 1
|
||||
|
||||
me.frm.script_manager.trigger("is_pos");
|
||||
me.frm.refresh_fields();
|
||||
if(doc.__islocal && doc.is_pos && frappe.get_route_str() !== 'point-of-sale') {
|
||||
this.frm.script_manager.trigger("is_pos");
|
||||
this.frm.refresh_fields();
|
||||
}
|
||||
},
|
||||
|
||||
refresh(doc) {
|
||||
this._super();
|
||||
if (doc.docstatus == 1 && !doc.is_return) {
|
||||
if(doc.outstanding_amount >= 0 || Math.abs(flt(doc.outstanding_amount)) < flt(doc.grand_total)) {
|
||||
cur_frm.add_custom_button(__('Return'),
|
||||
this.make_sales_return, __('Create'));
|
||||
cur_frm.page.set_inner_btn_group_as_primary(__('Create'));
|
||||
}
|
||||
this.frm.add_custom_button(__('Return'), this.make_sales_return, __('Create'));
|
||||
this.frm.page.set_inner_btn_group_as_primary(__('Create'));
|
||||
}
|
||||
|
||||
if (this.frm.doc.is_return) {
|
||||
if (doc.is_return && doc.__islocal) {
|
||||
this.frm.return_print_format = "Sales Invoice Return";
|
||||
cur_frm.set_value('consolidated_invoice', '');
|
||||
this.frm.set_value('consolidated_invoice', '');
|
||||
}
|
||||
},
|
||||
|
||||
is_pos: function(frm){
|
||||
is_pos: function() {
|
||||
this.set_pos_data();
|
||||
},
|
||||
|
||||
set_pos_data: function() {
|
||||
set_pos_data: async function() {
|
||||
if(this.frm.doc.is_pos) {
|
||||
this.frm.set_value("allocate_advances_automatically", 0);
|
||||
if(!this.frm.doc.company) {
|
||||
this.frm.set_value("is_pos", 0);
|
||||
frappe.msgprint(__("Please specify Company to proceed"));
|
||||
} else {
|
||||
var me = this;
|
||||
return this.frm.call({
|
||||
doc: me.frm.doc,
|
||||
const r = await this.frm.call({
|
||||
doc: this.frm.doc,
|
||||
method: "set_missing_values",
|
||||
callback: function(r) {
|
||||
if(!r.exc) {
|
||||
if(r.message) {
|
||||
me.frm.pos_print_format = r.message.print_format || "";
|
||||
me.frm.meta.default_print_format = r.message.print_format || "";
|
||||
me.frm.allow_edit_rate = r.message.allow_edit_rate;
|
||||
me.frm.allow_edit_discount = r.message.allow_edit_discount;
|
||||
me.frm.doc.campaign = r.message.campaign;
|
||||
me.frm.allow_print_before_pay = r.message.allow_print_before_pay;
|
||||
}
|
||||
me.frm.script_manager.trigger("update_stock");
|
||||
me.calculate_taxes_and_totals();
|
||||
if(me.frm.doc.taxes_and_charges) {
|
||||
me.frm.script_manager.trigger("taxes_and_charges");
|
||||
}
|
||||
frappe.model.set_default_values(me.frm.doc);
|
||||
me.set_dynamic_labels();
|
||||
|
||||
}
|
||||
}
|
||||
freeze: true
|
||||
});
|
||||
if(!r.exc) {
|
||||
if(r.message) {
|
||||
this.frm.pos_print_format = r.message.print_format || "";
|
||||
this.frm.meta.default_print_format = r.message.print_format || "";
|
||||
this.frm.doc.campaign = r.message.campaign;
|
||||
this.frm.allow_print_before_pay = r.message.allow_print_before_pay;
|
||||
}
|
||||
this.frm.script_manager.trigger("update_stock");
|
||||
this.calculate_taxes_and_totals();
|
||||
this.frm.doc.taxes_and_charges && this.frm.script_manager.trigger("taxes_and_charges");
|
||||
frappe.model.set_default_values(this.frm.doc);
|
||||
this.set_dynamic_labels();
|
||||
}
|
||||
}
|
||||
}
|
||||
else this.frm.trigger("refresh");
|
||||
},
|
||||
|
||||
customer() {
|
||||
if (!this.frm.doc.customer) return
|
||||
|
||||
if (this.frm.doc.is_pos){
|
||||
var pos_profile = this.frm.doc.pos_profile;
|
||||
}
|
||||
var me = this;
|
||||
const pos_profile = this.frm.doc.pos_profile;
|
||||
if(this.frm.updating_party_details) return;
|
||||
erpnext.utils.get_party_details(this.frm,
|
||||
"erpnext.accounts.party.get_party_details", {
|
||||
@ -92,8 +75,8 @@ erpnext.selling.POSInvoiceController = erpnext.selling.SellingController.extend(
|
||||
account: this.frm.doc.debit_to,
|
||||
price_list: this.frm.doc.selling_price_list,
|
||||
pos_profile: pos_profile
|
||||
}, function() {
|
||||
me.apply_pricing_rule();
|
||||
}, () => {
|
||||
this.apply_pricing_rule();
|
||||
});
|
||||
},
|
||||
|
||||
@ -201,5 +184,22 @@ frappe.ui.form.on('POS Invoice', {
|
||||
}
|
||||
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",
|
||||
"label": "Is Return (Credit Note)",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"set_only_once": 1
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break1",
|
||||
@ -461,7 +460,7 @@
|
||||
},
|
||||
{
|
||||
"fieldname": "contact_mobile",
|
||||
"fieldtype": "Small Text",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Mobile No",
|
||||
"read_only": 1
|
||||
@ -1579,10 +1578,9 @@
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-09-07 12:43:09.138720",
|
||||
"modified": "2020-09-28 16:51:24.641755",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Invoice",
|
||||
|
@ -10,11 +10,10 @@ from erpnext.controllers.selling_controller import SellingController
|
||||
from frappe.utils import cint, flt, add_months, today, date_diff, getdate, add_days, cstr, nowdate
|
||||
from erpnext.accounts.utils import get_account_currency
|
||||
from erpnext.accounts.party import get_party_account, get_due_date
|
||||
from erpnext.accounts.doctype.loyalty_program.loyalty_program import \
|
||||
get_loyalty_program_details_with_points, validate_loyalty_points
|
||||
|
||||
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.accounts.doctype.payment_request.payment_request import make_payment_request
|
||||
from erpnext.accounts.doctype.loyalty_program.loyalty_program import validate_loyalty_points
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_pos_reserved_serial_nos, get_serial_nos
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import SalesInvoice, get_bank_cash_account, update_multi_mode_option, get_mode_of_payment_info
|
||||
|
||||
from six import iteritems
|
||||
|
||||
@ -29,8 +28,7 @@ class POSInvoice(SalesInvoice):
|
||||
# run on validate method of selling controller
|
||||
super(SalesInvoice, self).validate()
|
||||
self.validate_auto_set_posting_time()
|
||||
self.validate_pos_paid_amount()
|
||||
self.validate_pos_return()
|
||||
self.validate_mode_of_payment()
|
||||
self.validate_uom_is_integer("stock_uom", "stock_qty")
|
||||
self.validate_uom_is_integer("uom", "qty")
|
||||
self.validate_debit_to_acc()
|
||||
@ -40,11 +38,11 @@ class POSInvoice(SalesInvoice):
|
||||
self.validate_item_cost_centers()
|
||||
self.validate_serialised_or_batched_item()
|
||||
self.validate_stock_availablility()
|
||||
self.validate_return_items()
|
||||
self.validate_return_items_qty()
|
||||
self.set_status()
|
||||
self.set_account_for_mode_of_payment()
|
||||
self.validate_pos()
|
||||
self.verify_payment_amount()
|
||||
self.validate_payment_amount()
|
||||
self.validate_loyalty_transaction()
|
||||
|
||||
def on_submit(self):
|
||||
@ -57,6 +55,7 @@ class POSInvoice(SalesInvoice):
|
||||
against_psi_doc.make_loyalty_point_entry()
|
||||
if self.redeem_loyalty_points and self.loyalty_points:
|
||||
self.apply_loyalty_points()
|
||||
self.check_phone_payments()
|
||||
self.set_status(update=True)
|
||||
|
||||
def on_cancel(self):
|
||||
@ -69,77 +68,116 @@ class POSInvoice(SalesInvoice):
|
||||
against_psi_doc.delete_loyalty_point_entry()
|
||||
against_psi_doc.make_loyalty_point_entry()
|
||||
|
||||
def validate_stock_availablility(self):
|
||||
allow_negative_stock = frappe.db.get_value('Stock Settings', None, 'allow_negative_stock')
|
||||
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):
|
||||
if self.is_return:
|
||||
return
|
||||
|
||||
allow_negative_stock = frappe.db.get_value('Stock Settings', None, 'allow_negative_stock')
|
||||
error_msg = []
|
||||
for d in self.get('items'):
|
||||
msg = ""
|
||||
if d.serial_no:
|
||||
filters = {
|
||||
"item_code": d.item_code,
|
||||
"warehouse": d.warehouse,
|
||||
"delivery_document_no": "",
|
||||
"sales_invoice": ""
|
||||
}
|
||||
filters = { "item_code": d.item_code, "warehouse": d.warehouse }
|
||||
if d.batch_no:
|
||||
filters["batch_no"] = d.batch_no
|
||||
reserved_serial_nos, unreserved_serial_nos = get_pos_reserved_serial_nos(filters)
|
||||
serial_nos = d.serial_no.split("\n")
|
||||
serial_nos = ' '.join(serial_nos).split() # remove whitespaces
|
||||
invalid_serial_nos = []
|
||||
for s in serial_nos:
|
||||
if s in reserved_serial_nos:
|
||||
invalid_serial_nos.append(s)
|
||||
|
||||
if len(invalid_serial_nos):
|
||||
multiple_nos = 's' if len(invalid_serial_nos) > 1 else ''
|
||||
frappe.throw(_("Row #{}: Serial No{}. {} has already been transacted into another POS Invoice. \
|
||||
Please select valid serial no.".format(d.idx, multiple_nos,
|
||||
frappe.bold(', '.join(invalid_serial_nos)))), title=_("Not Available"))
|
||||
reserved_serial_nos = get_pos_reserved_serial_nos(filters)
|
||||
serial_nos = get_serial_nos(d.serial_no)
|
||||
invalid_serial_nos = [s for s in serial_nos if s in reserved_serial_nos]
|
||||
|
||||
bold_invalid_serial_nos = frappe.bold(', '.join(invalid_serial_nos))
|
||||
if len(invalid_serial_nos) == 1:
|
||||
msg = (_("Row #{}: Serial No. {} has already been transacted into another POS Invoice. Please select valid serial no.")
|
||||
.format(d.idx, bold_invalid_serial_nos))
|
||||
elif invalid_serial_nos:
|
||||
msg = (_("Row #{}: Serial Nos. {} has already been transacted into another POS Invoice. Please select valid serial no.")
|
||||
.format(d.idx, bold_invalid_serial_nos))
|
||||
|
||||
else:
|
||||
if allow_negative_stock:
|
||||
return
|
||||
|
||||
available_stock = get_stock_availability(d.item_code, d.warehouse)
|
||||
if not (flt(available_stock) > 0):
|
||||
frappe.throw(_('Row #{}: Item Code: {} is not available under warehouse {}.'
|
||||
.format(d.idx, frappe.bold(d.item_code), frappe.bold(d.warehouse))), title=_("Not Available"))
|
||||
item_code, warehouse, qty = frappe.bold(d.item_code), frappe.bold(d.warehouse), frappe.bold(d.qty)
|
||||
if flt(available_stock) <= 0:
|
||||
msg = (_('Row #{}: Item Code: {} is not available under warehouse {}.').format(d.idx, item_code, warehouse))
|
||||
elif flt(available_stock) < flt(d.qty):
|
||||
frappe.msgprint(_('Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}. \
|
||||
Available quantity {}.'.format(d.idx, frappe.bold(d.item_code),
|
||||
frappe.bold(d.warehouse), frappe.bold(d.qty))), title=_("Not Available"))
|
||||
msg = (_('Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}. Available quantity {}.')
|
||||
.format(d.idx, item_code, warehouse, qty))
|
||||
if msg:
|
||||
error_msg.append(msg)
|
||||
|
||||
if error_msg:
|
||||
frappe.throw(error_msg, title=_("Item Unavailable"), as_list=True)
|
||||
|
||||
def validate_serialised_or_batched_item(self):
|
||||
error_msg = []
|
||||
for d in self.get("items"):
|
||||
serialized = d.get("has_serial_no")
|
||||
batched = d.get("has_batch_no")
|
||||
no_serial_selected = not d.get("serial_no")
|
||||
no_batch_selected = not d.get("batch_no")
|
||||
|
||||
|
||||
msg = ""
|
||||
item_code = frappe.bold(d.item_code)
|
||||
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.'
|
||||
.format(d.idx, frappe.bold(d.item_code))), title=_("Invalid Item"))
|
||||
msg = (_('Row #{}: Please select a serial no and batch against item: {} or remove it to complete transaction.')
|
||||
.format(d.idx, item_code))
|
||||
if serialized and no_serial_selected:
|
||||
frappe.throw(_('Row #{}: No serial number selected against item: {}. Please select one or remove it to complete transaction.'
|
||||
.format(d.idx, frappe.bold(d.item_code))), title=_("Invalid Item"))
|
||||
msg = (_('Row #{}: No serial number selected against item: {}. Please select one or remove it to complete transaction.')
|
||||
.format(d.idx, item_code))
|
||||
if batched and no_batch_selected:
|
||||
frappe.throw(_('Row #{}: No batch selected against item: {}. Please select a batch or remove it to complete transaction.'
|
||||
.format(d.idx, frappe.bold(d.item_code))), title=_("Invalid Item"))
|
||||
msg = (_('Row #{}: No batch selected against item: {}. Please select a batch or remove it to complete transaction.')
|
||||
.format(d.idx, item_code))
|
||||
if msg:
|
||||
error_msg.append(msg)
|
||||
|
||||
def validate_return_items(self):
|
||||
if error_msg:
|
||||
frappe.throw(error_msg, title=_("Invalid Item"), as_list=True)
|
||||
|
||||
def validate_return_items_qty(self):
|
||||
if not self.get("is_return"): return
|
||||
|
||||
for d in self.get("items"):
|
||||
if d.get("qty") > 0:
|
||||
frappe.throw(_("Row #{}: You cannot add postive quantities in a return invoice. Please remove item {} to complete the return.")
|
||||
.format(d.idx, frappe.bold(d.item_code)), title=_("Invalid Item"))
|
||||
frappe.throw(
|
||||
_("Row #{}: You cannot add postive quantities in a return invoice. Please remove item {} to complete the return.")
|
||||
.format(d.idx, frappe.bold(d.item_code)), title=_("Invalid Item")
|
||||
)
|
||||
if d.get("serial_no"):
|
||||
serial_nos = get_serial_nos(d.serial_no)
|
||||
for sr in serial_nos:
|
||||
serial_no_exists = frappe.db.exists("POS Invoice Item", {
|
||||
"parent": self.return_against,
|
||||
"serial_no": ["like", d.get("serial_no")]
|
||||
})
|
||||
if not serial_no_exists:
|
||||
bold_return_against = frappe.bold(self.return_against)
|
||||
bold_serial_no = frappe.bold(sr)
|
||||
frappe.throw(
|
||||
_("Row #{}: Serial No {} cannot be returned since it was not transacted in original invoice {}")
|
||||
.format(d.idx, bold_serial_no, bold_return_against)
|
||||
)
|
||||
|
||||
def validate_pos_paid_amount(self):
|
||||
if len(self.payments) == 0 and self.is_pos:
|
||||
def validate_mode_of_payment(self):
|
||||
if len(self.payments) == 0:
|
||||
frappe.throw(_("At least one mode of payment is required for POS invoice."))
|
||||
|
||||
def validate_change_account(self):
|
||||
if frappe.db.get_value("Account", self.account_for_change_amount, "company") != self.company:
|
||||
if self.change_amount and self.account_for_change_amount and \
|
||||
frappe.db.get_value("Account", self.account_for_change_amount, "company") != self.company:
|
||||
frappe.throw(_("The selected change account {} doesn't belongs to Company {}.").format(self.account_for_change_amount, self.company))
|
||||
|
||||
def validate_change_amount(self):
|
||||
@ -150,23 +188,21 @@ class POSInvoice(SalesInvoice):
|
||||
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:
|
||||
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 validate_payment_amount(self):
|
||||
total_amount_in_payments = 0
|
||||
for entry in self.payments:
|
||||
total_amount_in_payments += entry.amount
|
||||
if not self.is_return and entry.amount < 0:
|
||||
frappe.throw(_("Row #{0} (Payment Table): Amount must be positive").format(entry.idx))
|
||||
if self.is_return and entry.amount > 0:
|
||||
frappe.throw(_("Row #{0} (Payment Table): Amount must be negative").format(entry.idx))
|
||||
|
||||
def validate_pos_return(self):
|
||||
if self.is_pos and self.is_return:
|
||||
total_amount_in_payments = 0
|
||||
for payment in self.payments:
|
||||
total_amount_in_payments += payment.amount
|
||||
if self.is_return:
|
||||
invoice_total = self.rounded_total or self.grand_total
|
||||
if total_amount_in_payments < invoice_total:
|
||||
frappe.throw(_("Total payments amount can't be greater than {}".format(-invoice_total)))
|
||||
if total_amount_in_payments and total_amount_in_payments < invoice_total:
|
||||
frappe.throw(_("Total payments amount can't be greater than {}").format(-invoice_total))
|
||||
|
||||
def validate_loyalty_transaction(self):
|
||||
if self.redeem_loyalty_points and (not self.loyalty_redemption_account or not self.loyalty_redemption_cost_center):
|
||||
@ -220,55 +256,45 @@ class POSInvoice(SalesInvoice):
|
||||
pos_profile = get_pos_profile(self.company) or {}
|
||||
self.pos_profile = pos_profile.get('name')
|
||||
|
||||
pos = {}
|
||||
profile = {}
|
||||
if self.pos_profile:
|
||||
pos = frappe.get_doc('POS Profile', self.pos_profile)
|
||||
profile = frappe.get_doc('POS Profile', self.pos_profile)
|
||||
|
||||
if not self.get('payments') and not for_validate:
|
||||
update_multi_mode_option(self, pos)
|
||||
|
||||
if not self.account_for_change_amount:
|
||||
self.account_for_change_amount = frappe.get_cached_value('Company', self.company, 'default_cash_account')
|
||||
|
||||
if pos:
|
||||
if not for_validate:
|
||||
self.tax_category = pos.get("tax_category")
|
||||
update_multi_mode_option(self, profile)
|
||||
|
||||
if self.is_return and not for_validate:
|
||||
add_return_modes(self, profile)
|
||||
|
||||
if profile:
|
||||
if not for_validate and not self.customer:
|
||||
self.customer = pos.customer
|
||||
self.customer = profile.customer
|
||||
|
||||
self.ignore_pricing_rule = pos.ignore_pricing_rule
|
||||
if pos.get('account_for_change_amount'):
|
||||
self.account_for_change_amount = pos.get('account_for_change_amount')
|
||||
if pos.get('warehouse'):
|
||||
self.set_warehouse = pos.get('warehouse')
|
||||
self.ignore_pricing_rule = profile.ignore_pricing_rule
|
||||
self.account_for_change_amount = profile.get('account_for_change_amount') or self.account_for_change_amount
|
||||
self.set_warehouse = profile.get('warehouse') or self.set_warehouse
|
||||
|
||||
for fieldname in ('naming_series', 'currency', 'letter_head', 'tc_name',
|
||||
for fieldname in ('currency', 'letter_head', 'tc_name',
|
||||
'company', 'select_print_heading', 'write_off_account', 'taxes_and_charges',
|
||||
'write_off_cost_center', 'apply_discount_on', 'cost_center'):
|
||||
if (not for_validate) or (for_validate and not self.get(fieldname)):
|
||||
self.set(fieldname, pos.get(fieldname))
|
||||
|
||||
if pos.get("company_address"):
|
||||
self.company_address = pos.get("company_address")
|
||||
'write_off_cost_center', 'apply_discount_on', 'cost_center', 'tax_category',
|
||||
'ignore_pricing_rule', 'company_address', 'update_stock'):
|
||||
if not for_validate:
|
||||
self.set(fieldname, profile.get(fieldname))
|
||||
|
||||
if self.customer:
|
||||
customer_price_list, customer_group = frappe.db.get_value("Customer", self.customer, ['default_price_list', 'customer_group'])
|
||||
customer_group_price_list = frappe.db.get_value("Customer Group", customer_group, 'default_price_list')
|
||||
selling_price_list = customer_price_list or customer_group_price_list or pos.get('selling_price_list')
|
||||
selling_price_list = customer_price_list or customer_group_price_list or profile.get('selling_price_list')
|
||||
else:
|
||||
selling_price_list = pos.get('selling_price_list')
|
||||
selling_price_list = profile.get('selling_price_list')
|
||||
|
||||
if selling_price_list:
|
||||
self.set('selling_price_list', selling_price_list)
|
||||
|
||||
if not for_validate:
|
||||
self.update_stock = cint(pos.get("update_stock"))
|
||||
|
||||
# set pos values in items
|
||||
for item in self.get("items"):
|
||||
if item.get('item_code'):
|
||||
profile_details = get_pos_profile_item_details(pos, frappe._dict(item.as_dict()), pos)
|
||||
profile_details = get_pos_profile_item_details(profile.get("company"), frappe._dict(item.as_dict()), profile)
|
||||
for fname, val in iteritems(profile_details):
|
||||
if (not for_validate) or (for_validate and not item.get(fname)):
|
||||
item.set(fname, val)
|
||||
@ -281,10 +307,13 @@ class POSInvoice(SalesInvoice):
|
||||
if self.taxes_and_charges and not len(self.get("taxes")):
|
||||
self.set_taxes()
|
||||
|
||||
return pos
|
||||
if not self.account_for_change_amount:
|
||||
self.account_for_change_amount = frappe.get_cached_value('Company', self.company, 'default_cash_account')
|
||||
|
||||
return profile
|
||||
|
||||
def set_missing_values(self, for_validate=False):
|
||||
pos = self.set_pos_fields(for_validate)
|
||||
profile = self.set_pos_fields(for_validate)
|
||||
|
||||
if not self.debit_to:
|
||||
self.debit_to = get_party_account("Customer", self.customer, self.company)
|
||||
@ -294,17 +323,15 @@ class POSInvoice(SalesInvoice):
|
||||
|
||||
super(SalesInvoice, self).set_missing_values(for_validate)
|
||||
|
||||
print_format = pos.get("print_format") if pos else None
|
||||
print_format = profile.get("print_format") if profile else None
|
||||
if not print_format and not cint(frappe.db.get_value('Print Format', 'POS Invoice', 'disabled')):
|
||||
print_format = 'POS Invoice'
|
||||
|
||||
if pos:
|
||||
if profile:
|
||||
return {
|
||||
"print_format": print_format,
|
||||
"allow_edit_rate": pos.get("allow_user_to_edit_rate"),
|
||||
"allow_edit_discount": pos.get("allow_user_to_edit_discount"),
|
||||
"campaign": pos.get("campaign"),
|
||||
"allow_print_before_pay": pos.get("allow_print_before_pay")
|
||||
"campaign": profile.get("campaign"),
|
||||
"allow_print_before_pay": profile.get("allow_print_before_pay")
|
||||
}
|
||||
|
||||
def set_account_for_mode_of_payment(self):
|
||||
@ -313,6 +340,32 @@ class POSInvoice(SalesInvoice):
|
||||
if not pay.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()
|
||||
def get_stock_availability(item_code, warehouse):
|
||||
latest_sle = frappe.db.sql("""select qty_after_transaction
|
||||
@ -334,11 +387,9 @@ def get_stock_availability(item_code, warehouse):
|
||||
sle_qty = latest_sle[0].qty_after_transaction or 0 if latest_sle else 0
|
||||
pos_sales_qty = pos_sales_qty[0].qty or 0 if pos_sales_qty else 0
|
||||
|
||||
if sle_qty and pos_sales_qty and sle_qty > pos_sales_qty:
|
||||
if sle_qty and pos_sales_qty:
|
||||
return sle_qty - pos_sales_qty
|
||||
else:
|
||||
# when sle_qty is 0
|
||||
# when sle_qty > 0 and pos_sales_qty is 0
|
||||
return sle_qty
|
||||
|
||||
@frappe.whitelist()
|
||||
@ -371,4 +422,19 @@ def make_merge_log(invoices):
|
||||
})
|
||||
|
||||
if merge_log.get('pos_invoices'):
|
||||
return merge_log.as_dict()
|
||||
return merge_log.as_dict()
|
||||
|
||||
def add_return_modes(doc, pos_profile):
|
||||
def append_payment(payment_mode):
|
||||
payment = doc.append('payments', {})
|
||||
payment.default = payment_mode.default
|
||||
payment.mode_of_payment = payment_mode.parent
|
||||
payment.account = payment_mode.default_account
|
||||
payment.type = payment_mode.type
|
||||
|
||||
for pos_payment_method in pos_profile.get('payments'):
|
||||
pos_payment_method = pos_payment_method.as_dict()
|
||||
mode_of_payment = pos_payment_method.mode_of_payment
|
||||
if pos_payment_method.allow_in_returns and not [d for d in doc.get('payments') if d.mode_of_payment == mode_of_payment]:
|
||||
payment_mode = get_mode_of_payment_info(mode_of_payment, doc.company)
|
||||
append_payment(payment_mode[0])
|
@ -8,6 +8,7 @@ import unittest, copy, time
|
||||
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
|
||||
from erpnext.accounts.doctype.pos_invoice.pos_invoice import make_sales_return
|
||||
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
|
||||
|
||||
class TestPOSInvoice(unittest.TestCase):
|
||||
def test_timestamp_change(self):
|
||||
@ -222,29 +223,29 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
|
||||
se = make_serialized_item(company='_Test Company with perpetual inventory',
|
||||
target_warehouse="Stores - TCP1", cost_center='Main - TCP1', expense_account='Cost of Goods Sold - TCP1')
|
||||
se = make_serialized_item(company='_Test Company',
|
||||
target_warehouse="Stores - _TC", cost_center='Main - _TC', expense_account='Cost of Goods Sold - _TC')
|
||||
|
||||
serial_nos = get_serial_nos(se.get("items")[0].serial_no)
|
||||
|
||||
pos = create_pos_invoice(company='_Test Company with perpetual inventory', debit_to='Debtors - TCP1',
|
||||
account_for_change_amount='Cash - TCP1', warehouse='Stores - TCP1', income_account='Sales - TCP1',
|
||||
expense_account='Cost of Goods Sold - TCP1', cost_center='Main - TCP1',
|
||||
pos = create_pos_invoice(company='_Test Company', debit_to='Debtors - _TC',
|
||||
account_for_change_amount='Cash - _TC', warehouse='Stores - _TC', income_account='Sales - _TC',
|
||||
expense_account='Cost of Goods Sold - _TC', cost_center='Main - _TC',
|
||||
item=se.get("items")[0].item_code, rate=1000, do_not_save=1)
|
||||
|
||||
pos.get("items")[0].serial_no = serial_nos[0]
|
||||
pos.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - TCP1', 'amount': 1000})
|
||||
pos.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 1000})
|
||||
|
||||
pos.insert()
|
||||
pos.submit()
|
||||
|
||||
pos2 = create_pos_invoice(company='_Test Company with perpetual inventory', debit_to='Debtors - TCP1',
|
||||
account_for_change_amount='Cash - TCP1', warehouse='Stores - TCP1', income_account='Sales - TCP1',
|
||||
expense_account='Cost of Goods Sold - TCP1', cost_center='Main - TCP1',
|
||||
pos2 = create_pos_invoice(company='_Test Company', debit_to='Debtors - _TC',
|
||||
account_for_change_amount='Cash - _TC', warehouse='Stores - _TC', income_account='Sales - _TC',
|
||||
expense_account='Cost of Goods Sold - _TC', cost_center='Main - _TC',
|
||||
item=se.get("items")[0].item_code, rate=1000, do_not_save=1)
|
||||
|
||||
pos2.get("items")[0].serial_no = serial_nos[0]
|
||||
pos2.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - TCP1', 'amount': 1000})
|
||||
pos2.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 1000})
|
||||
|
||||
self.assertRaises(frappe.ValidationError, pos2.insert)
|
||||
|
||||
@ -286,7 +287,7 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
|
||||
after_redeem_lp_details = get_loyalty_program_details_with_points(inv.customer, company=inv.company, loyalty_program=inv.loyalty_program)
|
||||
self.assertEqual(after_redeem_lp_details.loyalty_points, 9)
|
||||
|
||||
|
||||
def test_merging_into_sales_invoice_with_discount(self):
|
||||
from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import init_user_and_profile
|
||||
from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import merge_pos_invoices
|
||||
@ -295,7 +296,7 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
test_user, pos_profile = init_user_and_profile()
|
||||
pos_inv = create_pos_invoice(rate=300, additional_discount_percentage=10, do_not_submit=1)
|
||||
pos_inv.append('payments', {
|
||||
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 300
|
||||
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 270
|
||||
})
|
||||
pos_inv.submit()
|
||||
|
||||
@ -309,9 +310,9 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
|
||||
pos_inv.load_from_db()
|
||||
rounded_total = frappe.db.get_value("Sales Invoice", pos_inv.consolidated_invoice, "rounded_total")
|
||||
self.assertEqual(rounded_total, 3500)
|
||||
self.assertEqual(rounded_total, 3470)
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
|
||||
def test_merging_into_sales_invoice_with_discount_and_inclusive_tax(self):
|
||||
from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import init_user_and_profile
|
||||
from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import merge_pos_invoices
|
||||
@ -361,7 +362,7 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
if not frappe.db.get_single_value("Selling Settings", "validate_selling_price"):
|
||||
frappe.db.set_value("Selling Settings", "Selling Settings", "validate_selling_price", 1)
|
||||
|
||||
make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", qty=1, basic_rate=300)
|
||||
make_purchase_receipt(item_code="_Test Item", warehouse="_Test Warehouse - _TC", qty=1, rate=300)
|
||||
frappe.db.sql("delete from `tabPOS Invoice`")
|
||||
test_user, pos_profile = init_user_and_profile()
|
||||
pos_inv = create_pos_invoice(rate=300, do_not_submit=1)
|
||||
@ -413,8 +414,6 @@ def create_pos_invoice(**args):
|
||||
pos_inv.is_pos = 1
|
||||
pos_inv.pos_profile = args.pos_profile or pos_profile.name
|
||||
|
||||
pos_inv.set_missing_values()
|
||||
|
||||
if args.posting_date:
|
||||
pos_inv.set_posting_time = 1
|
||||
pos_inv.posting_date = args.posting_date or frappe.utils.nowdate()
|
||||
@ -428,6 +427,8 @@ def create_pos_invoice(**args):
|
||||
pos_inv.conversion_rate = args.conversion_rate or 1
|
||||
pos_inv.account_for_change_amount = args.account_for_change_amount or "Cash - _TC"
|
||||
|
||||
pos_inv.set_missing_values()
|
||||
|
||||
pos_inv.append("items", {
|
||||
"item_code": args.item or args.item_code or "_Test Item",
|
||||
"warehouse": args.warehouse or "_Test Warehouse - _TC",
|
||||
|
@ -26,18 +26,25 @@ class POSInvoiceMergeLog(Document):
|
||||
for d in self.pos_invoices:
|
||||
status, docstatus, is_return, return_against = frappe.db.get_value(
|
||||
'POS Invoice', d.pos_invoice, ['status', 'docstatus', 'is_return', 'return_against'])
|
||||
|
||||
|
||||
bold_pos_invoice = frappe.bold(d.pos_invoice)
|
||||
bold_status = frappe.bold(status)
|
||||
if docstatus != 1:
|
||||
frappe.throw(_("Row #{}: POS Invoice {} is not submitted yet").format(d.idx, d.pos_invoice))
|
||||
frappe.throw(_("Row #{}: POS Invoice {} is not submitted yet").format(d.idx, bold_pos_invoice))
|
||||
if status == "Consolidated":
|
||||
frappe.throw(_("Row #{}: POS Invoice {} has been {}").format(d.idx, d.pos_invoice, status))
|
||||
if is_return and return_against not in [d.pos_invoice for d in self.pos_invoices] and status != "Consolidated":
|
||||
# if return entry is not getting merged in the current pos closing and if it is not consolidated
|
||||
frappe.throw(
|
||||
_("Row #{}: Return Invoice {} cannot be made against unconsolidated invoice. \
|
||||
You can add original invoice {} manually to proceed.")
|
||||
.format(d.idx, frappe.bold(d.pos_invoice), frappe.bold(return_against))
|
||||
)
|
||||
frappe.throw(_("Row #{}: POS Invoice {} has been {}").format(d.idx, bold_pos_invoice, bold_status))
|
||||
if is_return and return_against and return_against not in [d.pos_invoice for d in self.pos_invoices]:
|
||||
bold_return_against = frappe.bold(return_against)
|
||||
return_against_status = frappe.db.get_value('POS Invoice', return_against, "status")
|
||||
if return_against_status != "Consolidated":
|
||||
# if return entry is not getting merged in the current pos closing and if it is not consolidated
|
||||
bold_unconsolidated = frappe.bold("not Consolidated")
|
||||
msg = (_("Row #{}: Original Invoice {} of return invoice {} is {}. ")
|
||||
.format(d.idx, bold_return_against, bold_pos_invoice, bold_unconsolidated))
|
||||
msg += _("Original invoice should be consolidated before or along with the return invoice.")
|
||||
msg += "<br><br>"
|
||||
msg += _("You can add original invoice {} manually to proceed.").format(bold_return_against)
|
||||
frappe.throw(msg)
|
||||
|
||||
def on_submit(self):
|
||||
pos_invoice_docs = [frappe.get_doc("POS Invoice", d.pos_invoice) for d in self.pos_invoices]
|
||||
|
@ -17,18 +17,25 @@ class POSOpeningEntry(StatusUpdater):
|
||||
|
||||
def validate_pos_profile_and_cashier(self):
|
||||
if self.company != frappe.db.get_value("POS Profile", self.pos_profile, "company"):
|
||||
frappe.throw(_("POS Profile {} does not belongs to company {}".format(self.pos_profile, self.company)))
|
||||
frappe.throw(_("POS Profile {} does not belongs to company {}").format(self.pos_profile, self.company))
|
||||
|
||||
if not cint(frappe.db.get_value("User", self.user, "enabled")):
|
||||
frappe.throw(_("User {} has been disabled. Please select valid user/cashier".format(self.user)))
|
||||
frappe.throw(_("User {} is disabled. Please select valid user/cashier").format(self.user))
|
||||
|
||||
def validate_payment_method_account(self):
|
||||
invalid_modes = []
|
||||
for d in self.balance_details:
|
||||
account = frappe.db.get_value("Mode of Payment Account",
|
||||
{"parent": d.mode_of_payment, "company": self.company}, "default_account")
|
||||
if not account:
|
||||
frappe.throw(_("Please set default Cash or Bank account in Mode of Payment {0}")
|
||||
.format(get_link_to_form("Mode of Payment", mode_of_payment)), title=_("Missing Account"))
|
||||
invalid_modes.append(get_link_to_form("Mode of Payment", d.mode_of_payment))
|
||||
|
||||
if invalid_modes:
|
||||
if invalid_modes == 1:
|
||||
msg = _("Please set default Cash or Bank account in Mode of Payment {}")
|
||||
else:
|
||||
msg = _("Please set default Cash or Bank account in Mode of Payments {}")
|
||||
frappe.throw(msg.format(", ".join(invalid_modes)), title=_("Missing Account"))
|
||||
|
||||
def on_submit(self):
|
||||
self.set_status(update=True)
|
@ -6,6 +6,7 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"default",
|
||||
"allow_in_returns",
|
||||
"mode_of_payment"
|
||||
],
|
||||
"fields": [
|
||||
@ -24,11 +25,19 @@
|
||||
"label": "Mode of Payment",
|
||||
"options": "Mode of Payment",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "allow_in_returns",
|
||||
"fieldtype": "Check",
|
||||
"in_list_view": 1,
|
||||
"label": "Allow In Returns"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-05-29 15:08:41.704844",
|
||||
"modified": "2020-10-20 12:58:46.114456",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Payment Method",
|
||||
|
@ -290,28 +290,30 @@
|
||||
"fieldname": "warehouse",
|
||||
"fieldtype": "Link",
|
||||
"label": "Warehouse",
|
||||
"mandatory_depends_on": "update_stock",
|
||||
"oldfieldname": "warehouse",
|
||||
"oldfieldtype": "Link",
|
||||
"options": "Warehouse"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "update_stock",
|
||||
"fieldtype": "Check",
|
||||
"label": "Update Stock"
|
||||
"options": "Warehouse",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "ignore_pricing_rule",
|
||||
"fieldtype": "Check",
|
||||
"label": "Ignore Pricing Rule"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "update_stock",
|
||||
"fieldtype": "Check",
|
||||
"label": "Update Stock",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"icon": "icon-cog",
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2020-10-01 17:29:27.759088",
|
||||
"modified": "2020-10-20 13:16:50.665081",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Profile",
|
||||
|
@ -56,19 +56,29 @@ class POSProfile(Document):
|
||||
if not self.payments:
|
||||
frappe.throw(_("Payment methods are mandatory. Please add at least one payment method."))
|
||||
|
||||
default_mode_of_payment = [d.default for d in self.payments if d.default]
|
||||
if not default_mode_of_payment:
|
||||
default_mode = [d.default for d in self.payments if d.default]
|
||||
if not default_mode:
|
||||
frappe.throw(_("Please select a default mode of payment"))
|
||||
|
||||
if len(default_mode_of_payment) > 1:
|
||||
if len(default_mode) > 1:
|
||||
frappe.throw(_("You can only select one mode of payment as default"))
|
||||
|
||||
invalid_modes = []
|
||||
for d in self.payments:
|
||||
account = frappe.db.get_value("Mode of Payment Account",
|
||||
{"parent": d.mode_of_payment, "company": self.company}, "default_account")
|
||||
account = frappe.db.get_value(
|
||||
"Mode of Payment Account",
|
||||
{"parent": d.mode_of_payment, "company": self.company},
|
||||
"default_account"
|
||||
)
|
||||
if not account:
|
||||
frappe.throw(_("Please set default Cash or Bank account in Mode of Payment {0}")
|
||||
.format(get_link_to_form("Mode of Payment", mode_of_payment)), title=_("Missing Account"))
|
||||
invalid_modes.append(get_link_to_form("Mode of Payment", d.mode_of_payment))
|
||||
|
||||
if invalid_modes:
|
||||
if invalid_modes == 1:
|
||||
msg = _("Please set default Cash or Bank account in Mode of Payment {}")
|
||||
else:
|
||||
msg = _("Please set default Cash or Bank account in Mode of Payments {}")
|
||||
frappe.throw(msg.format(", ".join(invalid_modes)), title=_("Missing Account"))
|
||||
|
||||
def on_update(self):
|
||||
self.set_defaults()
|
||||
|
@ -9,8 +9,7 @@ frappe.ui.form.on('POS Settings', {
|
||||
get_invoice_fields: function(frm) {
|
||||
frappe.model.with_doctype("POS Invoice", () => {
|
||||
var fields = $.map(frappe.get_doc("DocType", "POS Invoice").fields, function(d) {
|
||||
if (frappe.model.no_value_type.indexOf(d.fieldtype) === -1 ||
|
||||
['Table', 'Button'].includes(d.fieldtype)) {
|
||||
if (frappe.model.no_value_type.indexOf(d.fieldtype) === -1 || ['Button'].includes(d.fieldtype)) {
|
||||
return { label: d.label + ' (' + d.fieldtype + ')', value: d.fieldname };
|
||||
} else {
|
||||
return null;
|
||||
|
@ -361,6 +361,7 @@
|
||||
"fieldname": "bill_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Supplier Invoice Date",
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "bill_date",
|
||||
"oldfieldtype": "Date",
|
||||
"print_hide": 1
|
||||
@ -1333,8 +1334,7 @@
|
||||
"icon": "fa fa-file-text",
|
||||
"idx": 204,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-08-03 23:20:04.466153",
|
||||
"modified": "2020-09-21 12:22:09.164068",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice",
|
||||
|
@ -151,14 +151,16 @@ class PurchaseInvoice(BuyingController):
|
||||
["account_type", "report_type", "account_currency"], as_dict=True)
|
||||
|
||||
if account.report_type != "Balance Sheet":
|
||||
frappe.throw(_("Please ensure {} account is a Balance Sheet account. \
|
||||
You can change the parent account to a Balance Sheet account or select a different account.")
|
||||
.format(frappe.bold("Credit To")), title=_("Invalid Account"))
|
||||
frappe.throw(
|
||||
_("Please ensure {} account is a Balance Sheet account. You can change the parent account to a Balance Sheet account or select a different account.")
|
||||
.format(frappe.bold("Credit To")), title=_("Invalid Account")
|
||||
)
|
||||
|
||||
if self.supplier and account.account_type != "Payable":
|
||||
frappe.throw(_("Please ensure {} account is a Payable account. \
|
||||
Change the account type to Payable or select a different account.")
|
||||
.format(frappe.bold("Credit To")), title=_("Invalid Account"))
|
||||
frappe.throw(
|
||||
_("Please ensure {} account is a Payable account. Change the account type to Payable or select a different account.")
|
||||
.format(frappe.bold("Credit To")), title=_("Invalid Account")
|
||||
)
|
||||
|
||||
self.party_account_currency = account.account_currency
|
||||
|
||||
@ -244,10 +246,10 @@ class PurchaseInvoice(BuyingController):
|
||||
|
||||
if self.update_stock and (not item.from_warehouse):
|
||||
if for_validate and item.expense_account and item.expense_account != warehouse_account[item.warehouse]["account"]:
|
||||
frappe.msgprint(_('''Row {0}: Expense Head changed to {1} because account {2}
|
||||
is not linked to warehouse {3} or it is not the default inventory account'''.format(
|
||||
item.idx, frappe.bold(warehouse_account[item.warehouse]["account"]),
|
||||
frappe.bold(item.expense_account), frappe.bold(item.warehouse))))
|
||||
msg = _("Row {}: Expense Head changed to {} ").format(item.idx, frappe.bold(warehouse_account[item.warehouse]["account"]))
|
||||
msg += _("because account {} is not linked to warehouse {} ").format(frappe.bold(item.expense_account), frappe.bold(item.warehouse))
|
||||
msg += _("or it is not the default inventory account")
|
||||
frappe.msgprint(msg, title=_("Expense Head Changed"))
|
||||
|
||||
item.expense_account = warehouse_account[item.warehouse]["account"]
|
||||
else:
|
||||
@ -259,19 +261,19 @@ class PurchaseInvoice(BuyingController):
|
||||
|
||||
if negative_expense_booked_in_pr:
|
||||
if for_validate and item.expense_account and item.expense_account != stock_not_billed_account:
|
||||
frappe.msgprint(_('''Row {0}: Expense Head changed to {1} because
|
||||
expense is booked against this account in Purchase Receipt {2}'''.format(
|
||||
item.idx, frappe.bold(stock_not_billed_account), frappe.bold(item.purchase_receipt))))
|
||||
msg = _("Row {}: Expense Head changed to {} ").format(item.idx, frappe.bold(stock_not_billed_account))
|
||||
msg += _("because expense is booked against this account in Purchase Receipt {}").format(frappe.bold(item.purchase_receipt))
|
||||
frappe.msgprint(msg, title=_("Expense Head Changed"))
|
||||
|
||||
item.expense_account = stock_not_billed_account
|
||||
else:
|
||||
# If no purchase receipt present then book expense in 'Stock Received But Not Billed'
|
||||
# This is done in cases when Purchase Invoice is created before Purchase Receipt
|
||||
if for_validate and item.expense_account and item.expense_account != stock_not_billed_account:
|
||||
frappe.msgprint(_('''Row {0}: Expense Head changed to {1} as no Purchase
|
||||
Receipt is created against Item {2}. This is done to handle accounting for cases
|
||||
when Purchase Receipt is created after Purchase Invoice'''.format(
|
||||
item.idx, frappe.bold(stock_not_billed_account), frappe.bold(item.item_code))))
|
||||
msg = _("Row {}: Expense Head changed to {} ").format(item.idx, frappe.bold(stock_not_billed_account))
|
||||
msg += _("as no Purchase Receipt is created against Item {}. ").format(frappe.bold(item.item_code))
|
||||
msg += _("This is done to handle accounting for cases when Purchase Receipt is created after Purchase Invoice")
|
||||
frappe.msgprint(msg, title=_("Expense Head Changed"))
|
||||
|
||||
item.expense_account = stock_not_billed_account
|
||||
|
||||
@ -299,10 +301,11 @@ class PurchaseInvoice(BuyingController):
|
||||
|
||||
for d in self.get('items'):
|
||||
if not d.purchase_order:
|
||||
throw(_("""Purchase Order Required for item {0}
|
||||
To submit the invoice without purchase order please set
|
||||
{1} as {2} in {3}""").format(frappe.bold(d.item_code), frappe.bold(_('Purchase Order Required')),
|
||||
frappe.bold('No'), get_link_to_form('Buying Settings', 'Buying Settings', 'Buying Settings')))
|
||||
msg = _("Purchase Order Required for item {}").format(frappe.bold(d.item_code))
|
||||
msg += "<br><br>"
|
||||
msg += _("To submit the invoice without purchase order please set {} ").format(frappe.bold(_('Purchase Order Required')))
|
||||
msg += _("as {} in {}").format(frappe.bold('No'), get_link_to_form('Buying Settings', 'Buying Settings', 'Buying Settings'))
|
||||
throw(msg, title=_("Mandatory Purchase Order"))
|
||||
|
||||
def pr_required(self):
|
||||
stock_items = self.get_stock_items()
|
||||
@ -313,10 +316,11 @@ class PurchaseInvoice(BuyingController):
|
||||
|
||||
for d in self.get('items'):
|
||||
if not d.purchase_receipt and d.item_code in stock_items:
|
||||
throw(_("""Purchase Receipt Required for item {0}
|
||||
To submit the invoice without purchase receipt please set
|
||||
{1} as {2} in {3}""").format(frappe.bold(d.item_code), frappe.bold(_('Purchase Receipt Required')),
|
||||
frappe.bold('No'), get_link_to_form('Buying Settings', 'Buying Settings', 'Buying Settings')))
|
||||
msg = _("Purchase Receipt Required for item {}").format(frappe.bold(d.item_code))
|
||||
msg += "<br><br>"
|
||||
msg += _("To submit the invoice without purchase receipt please set {} ").format(frappe.bold(_('Purchase Receipt Required')))
|
||||
msg += _("as {} in {}").format(frappe.bold('No'), get_link_to_form('Buying Settings', 'Buying Settings', 'Buying Settings'))
|
||||
throw(msg, title=_("Mandatory Purchase Receipt"))
|
||||
|
||||
def validate_write_off_account(self):
|
||||
if self.write_off_amount and not self.write_off_account:
|
||||
|
@ -479,14 +479,14 @@ class SalesInvoice(SellingController):
|
||||
frappe.throw(_("Debit To is required"), title=_("Account Missing"))
|
||||
|
||||
if account.report_type != "Balance Sheet":
|
||||
frappe.throw(_("Please ensure {} account is a Balance Sheet account. \
|
||||
You can change the parent account to a Balance Sheet account or select a different account.")
|
||||
.format(frappe.bold("Debit To")), title=_("Invalid Account"))
|
||||
msg = _("Please ensure {} account is a Balance Sheet account. ").format(frappe.bold("Debit To"))
|
||||
msg += _("You can change the parent account to a Balance Sheet account or select a different account.")
|
||||
frappe.throw(msg, title=_("Invalid Account"))
|
||||
|
||||
if self.customer and account.account_type != "Receivable":
|
||||
frappe.throw(_("Please ensure {} account is a Receivable account. \
|
||||
Change the account type to Receivable or select a different account.")
|
||||
.format(frappe.bold("Debit To")), title=_("Invalid Account"))
|
||||
msg = _("Please ensure {} account is a Receivable account. ").format(frappe.bold("Debit To"))
|
||||
msg += _("Change the account type to Receivable or select a different account.")
|
||||
frappe.throw(msg, title=_("Invalid Account"))
|
||||
|
||||
self.party_account_currency = account.account_currency
|
||||
|
||||
@ -572,7 +572,8 @@ class SalesInvoice(SellingController):
|
||||
|
||||
def validate_pos(self):
|
||||
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)):
|
||||
frappe.throw(_("Paid amount + Write Off Amount can not be greater than Grand Total"))
|
||||
|
||||
@ -1140,8 +1141,10 @@ class SalesInvoice(SellingController):
|
||||
where redeem_against=%s''', (lp_entry[0].name), as_dict=1)
|
||||
if against_lp_entry:
|
||||
invoice_list = ", ".join([d.invoice for d in against_lp_entry])
|
||||
frappe.throw(_('''{} can't be cancelled since the Loyalty Points earned has been redeemed.
|
||||
First cancel the {} No {}''').format(self.doctype, self.doctype, invoice_list))
|
||||
frappe.throw(
|
||||
_('''{} can't be cancelled since the Loyalty Points earned has been redeemed. First cancel the {} No {}''')
|
||||
.format(self.doctype, self.doctype, invoice_list)
|
||||
)
|
||||
else:
|
||||
frappe.db.sql('''delete from `tabLoyalty Point Entry` where invoice=%s''', (self.name))
|
||||
# Set loyalty program
|
||||
@ -1612,17 +1615,25 @@ def update_multi_mode_option(doc, pos_profile):
|
||||
payment.type = payment_mode.type
|
||||
|
||||
doc.set('payments', [])
|
||||
invalid_modes = []
|
||||
for pos_payment_method in pos_profile.get('payments'):
|
||||
pos_payment_method = pos_payment_method.as_dict()
|
||||
|
||||
payment_mode = get_mode_of_payment_info(pos_payment_method.mode_of_payment, doc.company)
|
||||
if not payment_mode:
|
||||
frappe.throw(_("Please set default Cash or Bank account in Mode of Payment {0}")
|
||||
.format(get_link_to_form("Mode of Payment", pos_payment_method.mode_of_payment)), title=_("Missing Account"))
|
||||
invalid_modes.append(get_link_to_form("Mode of Payment", pos_payment_method.mode_of_payment))
|
||||
continue
|
||||
|
||||
payment_mode[0].default = pos_payment_method.default
|
||||
append_payment(payment_mode[0])
|
||||
|
||||
if invalid_modes:
|
||||
if invalid_modes == 1:
|
||||
msg = _("Please set default Cash or Bank account in Mode of Payment {}")
|
||||
else:
|
||||
msg = _("Please set default Cash or Bank account in Mode of Payments {}")
|
||||
frappe.throw(msg.format(", ".join(invalid_modes)), title=_("Missing Account"))
|
||||
|
||||
def get_all_mode_of_payments(doc):
|
||||
return frappe.db.sql("""
|
||||
select mpa.default_account, mpa.parent, mp.type as type
|
||||
|
@ -171,7 +171,7 @@ def validate_account_for_perpetual_inventory(gl_map):
|
||||
frappe.throw(_("Account: {0} can only be updated via Stock Transactions")
|
||||
.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"),
|
||||
currency=frappe.get_cached_value('Company', gl_map[0].company, "default_currency"))
|
||||
|
||||
|
@ -20,7 +20,7 @@
|
||||
"owner": "Administrator",
|
||||
"steps": [
|
||||
{
|
||||
"step": "Chart Of Accounts"
|
||||
"step": "Chart of Accounts"
|
||||
},
|
||||
{
|
||||
"step": "Setup Taxes"
|
||||
|
@ -10,11 +10,11 @@
|
||||
"is_skipped": 0,
|
||||
"modified": "2020-05-14 17:40:28.410447",
|
||||
"modified_by": "Administrator",
|
||||
"name": "Chart Of Accounts",
|
||||
"name": "Chart of Accounts",
|
||||
"owner": "Administrator",
|
||||
"path": "Tree/Account",
|
||||
"reference_document": "Account",
|
||||
"show_full_form": 0,
|
||||
"title": "Review Chart Of Accounts",
|
||||
"title": "Review Chart of Accounts",
|
||||
"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))
|
||||
|
||||
if payment_doctype == "Payment Entry" and payment_entry.unallocated_amount > transaction.unallocated_amount:
|
||||
frappe.throw(_("The unallocated amount of Payment Entry {0} \
|
||||
is greater than the Bank Transaction's unallocated amount").format(payment_name))
|
||||
frappe.throw(_("The unallocated amount of Payment Entry {0} is greater than the Bank Transaction's unallocated amount").format(payment_name))
|
||||
|
||||
if transaction.unallocated_amount == 0:
|
||||
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)],
|
||||
["docstatus", "=", "1"], ["payment_type", "=", [payment_type, "Internal Transfer"]], ["ifnull(clearance_date, '')", "=", ""], ["{0}".format(account_from_to), "=", "{0}".format(bank_account)]])
|
||||
|
||||
if transaction.credit > 0:
|
||||
journal_entries = frappe.db.sql("""
|
||||
SELECT
|
||||
'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
|
||||
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 = %s
|
||||
AND
|
||||
jea.debit_in_account_currency like %s
|
||||
AND
|
||||
je.docstatus = 1
|
||||
""", (bank_account, amount), as_dict=True)
|
||||
else:
|
||||
journal_entries = frappe.db.sql("""
|
||||
SELECT
|
||||
'Journal Entry' as doctype, je.name, je.posting_date, je.cheque_no as reference_no,
|
||||
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)
|
||||
jea_side = "debit" if transaction.credit > 0 else "credit"
|
||||
journal_entries = frappe.db.sql(f"""
|
||||
SELECT
|
||||
'Journal Entry' as doctype, je.name, je.posting_date, je.cheque_no as reference_no,
|
||||
jea.account_currency as currency, je.pay_to_recd_from as party, je.cheque_date as reference_date,
|
||||
jea.{jea_side}_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.{jea_side}_in_account_currency like %(txt)s
|
||||
AND
|
||||
je.docstatus = 1
|
||||
""", {
|
||||
'bank_account': bank_account,
|
||||
'txt': '%%%s%%' % amount
|
||||
}, as_dict=True)
|
||||
|
||||
if transaction.credit > 0:
|
||||
sales_invoices = frappe.db.sql("""
|
||||
@ -264,7 +243,11 @@ def check_amount_vs_description(amount_matching, description_matching):
|
||||
continue
|
||||
|
||||
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:
|
||||
result.append(am_match)
|
||||
if result:
|
||||
|
@ -203,7 +203,7 @@ def set_account_and_due_date(party, account, party_type, company, posting_date,
|
||||
return out
|
||||
|
||||
@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`.
|
||||
Will first search in party (Customer / Supplier) record, if not found,
|
||||
will search in group (Customer Group / Supplier Group),
|
||||
|
@ -3,6 +3,14 @@
|
||||
|
||||
frappe.query_reports["Bank Reconciliation Statement"] = {
|
||||
"filters": [
|
||||
{
|
||||
"fieldname":"company",
|
||||
"label": __("Company"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Company",
|
||||
"reqd": 1,
|
||||
"default": frappe.defaults.get_user_default("Company")
|
||||
},
|
||||
{
|
||||
"fieldname":"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"]: "",
|
||||
"reqd": 1,
|
||||
"get_query": function() {
|
||||
var company = frappe.query_report.get_filter_value('company')
|
||||
return {
|
||||
"query": "erpnext.controllers.queries.get_account_list",
|
||||
"filters": [
|
||||
['Account', 'account_type', 'in', 'Bank, Cash'],
|
||||
['Account', 'is_group', '=', 0],
|
||||
['Account', 'disabled', '=', 0],
|
||||
['Account', 'company', '=', company],
|
||||
]
|
||||
}
|
||||
}
|
||||
@ -34,4 +45,4 @@ frappe.query_reports["Bank Reconciliation Statement"] = {
|
||||
"fieldtype": "Check"
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -307,7 +307,7 @@ def get_accounts(company, root_type):
|
||||
where company=%s and root_type=%s order by lft""", (company, root_type), as_dict=True)
|
||||
|
||||
|
||||
def filter_accounts(accounts, depth=10):
|
||||
def filter_accounts(accounts, depth=20):
|
||||
parent_children_map = {}
|
||||
accounts_by_name = {}
|
||||
for d in accounts:
|
||||
|
@ -796,7 +796,7 @@ def get_children(doctype, parent, company, is_root=False):
|
||||
|
||||
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
|
||||
|
||||
company = frappe.db.get_value("Global Defaults", None, "default_company")
|
||||
@ -831,7 +831,8 @@ def create_payment_gateway_account(gateway):
|
||||
"is_default": 1,
|
||||
"payment_gateway": gateway,
|
||||
"payment_account": bank_account.name,
|
||||
"currency": bank_account.account_currency
|
||||
"currency": bank_account.account_currency,
|
||||
"payment_channel": payment_channel
|
||||
}).insert(ignore_permissions=True)
|
||||
|
||||
except frappe.DuplicateEntryError:
|
||||
|
@ -46,26 +46,26 @@
|
||||
{
|
||||
"fieldname": "po_required",
|
||||
"fieldtype": "Select",
|
||||
"label": "Purchase Order Required for Purchase Invoice & Receipt Creation",
|
||||
"label": "Is Purchase Order Required for Purchase Invoice & Receipt Creation?",
|
||||
"options": "No\nYes"
|
||||
},
|
||||
{
|
||||
"fieldname": "pr_required",
|
||||
"fieldtype": "Select",
|
||||
"label": "Purchase Receipt Required for Purchase Invoice Creation",
|
||||
"label": "Is Purchase Receipt Required for Purchase Invoice Creation?",
|
||||
"options": "No\nYes"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "maintain_same_rate",
|
||||
"fieldtype": "Check",
|
||||
"label": "Maintain same rate throughout purchase cycle"
|
||||
"label": "Maintain Same Rate Throughout the Purchase Cycle"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "allow_multiple_items",
|
||||
"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",
|
||||
@ -93,9 +93,10 @@
|
||||
],
|
||||
"icon": "fa fa-cog",
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2020-05-15 14:49:32.513611",
|
||||
"modified": "2020-10-13 12:00:23.276329",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Buying Settings",
|
||||
@ -113,4 +114,4 @@
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC"
|
||||
}
|
||||
}
|
||||
|
@ -19,6 +19,8 @@ from erpnext.controllers.accounts_controller import update_child_qty_rate
|
||||
from erpnext.controllers.status_updater import OverAllowanceError
|
||||
from erpnext.manufacturing.doctype.blanket_order.test_blanket_order import make_blanket_order
|
||||
|
||||
from erpnext.stock.doctype.batch.test_batch import make_new_batch
|
||||
from erpnext.controllers.buying_controller import get_backflushed_subcontracted_raw_materials
|
||||
|
||||
class TestPurchaseOrder(unittest.TestCase):
|
||||
def test_make_purchase_receipt(self):
|
||||
@ -203,9 +205,39 @@ class TestPurchaseOrder(unittest.TestCase):
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
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"
|
||||
item = "_Test Item Home Desktop 100"
|
||||
|
||||
if not frappe.db.exists("Item Tax", {"parent":item, "item_tax_template":tax_template}):
|
||||
item_doc = frappe.get_doc("Item", item)
|
||||
item_doc.append("taxes", {
|
||||
@ -237,17 +269,25 @@ class TestPurchaseOrder(unittest.TestCase):
|
||||
|
||||
items = json.dumps([
|
||||
{'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)
|
||||
|
||||
po.reload()
|
||||
self.assertEqual(po.taxes[0].tax_amount, 60)
|
||||
self.assertEqual(po.taxes[0].total, 660)
|
||||
self.assertEqual(po.taxes[0].tax_amount, 70)
|
||||
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
|
||||
where parent = %(item)s and item_tax_template = %(tax)s""",
|
||||
{"item": item, "tax": tax_template})
|
||||
where parent = %(item)s and item_tax_template = %(tax)s""", {"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):
|
||||
po = create_purchase_order(item_code="_Test FG Item", is_subcontracted="Yes")
|
||||
@ -648,15 +688,15 @@ class TestPurchaseOrder(unittest.TestCase):
|
||||
|
||||
def test_exploded_items_in_subcontracted(self):
|
||||
item_code = "_Test Subcontracted FG Item 1"
|
||||
make_subcontracted_item(item_code)
|
||||
make_subcontracted_item(item_code=item_code)
|
||||
|
||||
po = create_purchase_order(item_code=item_code, qty=1,
|
||||
is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC")
|
||||
is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC", include_exploded_items=1)
|
||||
|
||||
name = frappe.db.get_value('BOM', {'item': item_code}, 'name')
|
||||
bom = frappe.get_doc('BOM', name)
|
||||
|
||||
exploded_items = sorted([d.item_code for d in bom.exploded_items])
|
||||
exploded_items = sorted([d.item_code for d in bom.exploded_items if not d.get('sourced_by_supplier')])
|
||||
supplied_items = sorted([d.rm_item_code for d in po.supplied_items])
|
||||
self.assertEquals(exploded_items, supplied_items)
|
||||
|
||||
@ -664,13 +704,13 @@ class TestPurchaseOrder(unittest.TestCase):
|
||||
is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC", include_exploded_items=0)
|
||||
|
||||
supplied_items1 = sorted([d.rm_item_code for d in po1.supplied_items])
|
||||
bom_items = sorted([d.item_code for d in bom.items])
|
||||
bom_items = sorted([d.item_code for d in bom.items if not d.get('sourced_by_supplier')])
|
||||
|
||||
self.assertEquals(supplied_items1, bom_items)
|
||||
|
||||
def test_backflush_based_on_stock_entry(self):
|
||||
item_code = "_Test Subcontracted FG Item 1"
|
||||
make_subcontracted_item(item_code)
|
||||
make_subcontracted_item(item_code=item_code)
|
||||
make_item('Sub Contracted Raw Material 1', {
|
||||
'is_stock_item': 1,
|
||||
'is_sub_contracted_item': 1
|
||||
@ -729,6 +769,129 @@ class TestPurchaseOrder(unittest.TestCase):
|
||||
|
||||
update_backflush_based_on("BOM")
|
||||
|
||||
def test_backflushed_based_on_for_multiple_batches(self):
|
||||
item_code = "_Test Subcontracted FG Item 2"
|
||||
make_item('Sub Contracted Raw Material 2', {
|
||||
'is_stock_item': 1,
|
||||
'is_sub_contracted_item': 1
|
||||
})
|
||||
|
||||
make_subcontracted_item(item_code=item_code, has_batch_no=1, create_new_batch=1,
|
||||
raw_materials=["Sub Contracted Raw Material 2"])
|
||||
|
||||
update_backflush_based_on("Material Transferred for Subcontract")
|
||||
|
||||
order_qty = 500
|
||||
po = create_purchase_order(item_code=item_code, qty=order_qty,
|
||||
is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC")
|
||||
|
||||
make_stock_entry(target="_Test Warehouse - _TC",
|
||||
item_code = "Sub Contracted Raw Material 2", qty=552, basic_rate=100)
|
||||
|
||||
rm_items = [
|
||||
{"item_code":item_code,"rm_item_code":"Sub Contracted Raw Material 2","item_name":"_Test Item",
|
||||
"qty":552,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos"}]
|
||||
|
||||
rm_item_string = json.dumps(rm_items)
|
||||
se = frappe.get_doc(make_subcontract_transfer_entry(po.name, rm_item_string))
|
||||
se.submit()
|
||||
|
||||
for batch in ["ABCD1", "ABCD2", "ABCD3", "ABCD4"]:
|
||||
make_new_batch(batch_id=batch, item_code=item_code)
|
||||
|
||||
pr = make_purchase_receipt(po.name)
|
||||
|
||||
# partial receipt
|
||||
pr.get('items')[0].qty = 30
|
||||
pr.get('items')[0].batch_no = "ABCD1"
|
||||
|
||||
purchase_order = po.name
|
||||
purchase_order_item = po.items[0].name
|
||||
|
||||
for batch_no, qty in {"ABCD2": 60, "ABCD3": 70, "ABCD4":40}.items():
|
||||
pr.append("items", {
|
||||
"item_code": pr.get('items')[0].item_code,
|
||||
"item_name": pr.get('items')[0].item_name,
|
||||
"uom": pr.get('items')[0].uom,
|
||||
"stock_uom": pr.get('items')[0].stock_uom,
|
||||
"warehouse": pr.get('items')[0].warehouse,
|
||||
"conversion_factor": pr.get('items')[0].conversion_factor,
|
||||
"cost_center": pr.get('items')[0].cost_center,
|
||||
"rate": pr.get('items')[0].rate,
|
||||
"qty": qty,
|
||||
"batch_no": batch_no,
|
||||
"purchase_order": purchase_order,
|
||||
"purchase_order_item": purchase_order_item
|
||||
})
|
||||
|
||||
pr.submit()
|
||||
|
||||
pr1 = make_purchase_receipt(po.name)
|
||||
pr1.get('items')[0].qty = 300
|
||||
pr1.get('items')[0].batch_no = "ABCD1"
|
||||
pr1.save()
|
||||
|
||||
pr_key = ("Sub Contracted Raw Material 2", po.name)
|
||||
consumed_qty = get_backflushed_subcontracted_raw_materials([po.name]).get(pr_key)
|
||||
|
||||
self.assertTrue(pr1.supplied_items[0].consumed_qty > 0)
|
||||
self.assertTrue(pr1.supplied_items[0].consumed_qty, flt(552.0) - flt(consumed_qty))
|
||||
|
||||
update_backflush_based_on("BOM")
|
||||
|
||||
def test_supplied_qty_against_subcontracted_po(self):
|
||||
item_code = "_Test Subcontracted FG Item 5"
|
||||
make_item('Sub Contracted Raw Material 4', {
|
||||
'is_stock_item': 1,
|
||||
'is_sub_contracted_item': 1
|
||||
})
|
||||
|
||||
make_subcontracted_item(item_code=item_code, raw_materials=["Sub Contracted Raw Material 4"])
|
||||
|
||||
update_backflush_based_on("Material Transferred for Subcontract")
|
||||
|
||||
order_qty = 250
|
||||
po = create_purchase_order(item_code=item_code, qty=order_qty,
|
||||
is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC", do_not_save=True)
|
||||
|
||||
# Add same subcontracted items multiple times
|
||||
po.append("items", {
|
||||
"item_code": item_code,
|
||||
"qty": order_qty,
|
||||
"schedule_date": add_days(nowdate(), 1),
|
||||
"warehouse": "_Test Warehouse - _TC"
|
||||
})
|
||||
|
||||
po.set_missing_values()
|
||||
po.submit()
|
||||
|
||||
# Material receipt entry for the raw materials which will be send to supplier
|
||||
make_stock_entry(target="_Test Warehouse - _TC",
|
||||
item_code = "Sub Contracted Raw Material 4", qty=500, basic_rate=100)
|
||||
|
||||
rm_items = [
|
||||
{
|
||||
"item_code":item_code,"rm_item_code":"Sub Contracted Raw Material 4","item_name":"_Test Item",
|
||||
"qty":250,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos", "name": po.supplied_items[0].name
|
||||
},
|
||||
{
|
||||
"item_code":item_code,"rm_item_code":"Sub Contracted Raw Material 4","item_name":"_Test Item",
|
||||
"qty":250,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos", "name": po.supplied_items[1].name
|
||||
},
|
||||
]
|
||||
|
||||
# Raw Materials transfer entry from stores to supplier's warehouse
|
||||
rm_item_string = json.dumps(rm_items)
|
||||
se = frappe.get_doc(make_subcontract_transfer_entry(po.name, rm_item_string))
|
||||
se.submit()
|
||||
|
||||
po_doc = frappe.get_doc("Purchase Order", po.name)
|
||||
for row in po_doc.supplied_items:
|
||||
# Valid that whether transferred quantity is matching with supplied qty or not in the purchase order
|
||||
self.assertEqual(row.supplied_qty, 250.0)
|
||||
|
||||
update_backflush_based_on("BOM")
|
||||
|
||||
def test_advance_payment_entry_unlink_against_purchase_order(self):
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
|
||||
frappe.db.set_value("Accounts Settings", "Accounts Settings",
|
||||
@ -801,27 +964,33 @@ def make_pr_against_po(po, received_qty=0):
|
||||
pr.submit()
|
||||
return pr
|
||||
|
||||
def make_subcontracted_item(item_code):
|
||||
def make_subcontracted_item(**args):
|
||||
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
|
||||
|
||||
if not frappe.db.exists('Item', item_code):
|
||||
make_item(item_code, {
|
||||
args = frappe._dict(args)
|
||||
|
||||
if not frappe.db.exists('Item', args.item_code):
|
||||
make_item(args.item_code, {
|
||||
'is_stock_item': 1,
|
||||
'is_sub_contracted_item': 1
|
||||
'is_sub_contracted_item': 1,
|
||||
'has_batch_no': args.get("has_batch_no") or 0
|
||||
})
|
||||
|
||||
if not frappe.db.exists('Item', "Test Extra Item 1"):
|
||||
make_item("Test Extra Item 1", {
|
||||
'is_stock_item': 1,
|
||||
})
|
||||
if not args.raw_materials:
|
||||
if not frappe.db.exists('Item', "Test Extra Item 1"):
|
||||
make_item("Test Extra Item 1", {
|
||||
'is_stock_item': 1,
|
||||
})
|
||||
|
||||
if not frappe.db.exists('Item', "Test Extra Item 2"):
|
||||
make_item("Test Extra Item 2", {
|
||||
'is_stock_item': 1,
|
||||
})
|
||||
if not frappe.db.exists('Item', "Test Extra Item 2"):
|
||||
make_item("Test Extra Item 2", {
|
||||
'is_stock_item': 1,
|
||||
})
|
||||
|
||||
if not frappe.db.get_value('BOM', {'item': item_code}, 'name'):
|
||||
make_bom(item = item_code, raw_materials = ['_Test FG Item', 'Test Extra Item 1'])
|
||||
args.raw_materials = ['_Test FG Item', 'Test Extra Item 1']
|
||||
|
||||
if not frappe.db.get_value('BOM', {'item': args.item_code}, 'name'):
|
||||
make_bom(item = args.item_code, raw_materials = args.get("raw_materials"))
|
||||
|
||||
def update_backflush_based_on(based_on):
|
||||
doc = frappe.get_doc('Buying Settings')
|
||||
|
@ -22,8 +22,6 @@ frappe.ui.form.on("Request for Quotation",{
|
||||
},
|
||||
|
||||
onload: function(frm) {
|
||||
frm.add_fetch('email_template', 'response', '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"))
|
||||
}
|
||||
@ -31,14 +29,12 @@ frappe.ui.form.on("Request for Quotation",{
|
||||
|
||||
refresh: function(frm, cdt, cdn) {
|
||||
if (frm.doc.docstatus === 1) {
|
||||
frm.add_custom_button(__('Create'),
|
||||
function(){ frm.trigger("make_suppplier_quotation") }, __("Supplier Quotation"));
|
||||
|
||||
frm.add_custom_button(__("View"),
|
||||
function(){ frappe.set_route('List', 'Supplier Quotation',
|
||||
{'request_for_quotation': frm.doc.name}) }, __("Supplier Quotation"));
|
||||
frm.add_custom_button(__('Supplier Quotation'),
|
||||
function(){ frm.trigger("make_suppplier_quotation") }, __("Create"));
|
||||
|
||||
frm.add_custom_button(__("Send Supplier Emails"), function() {
|
||||
|
||||
frm.add_custom_button(__("Send Emails to Suppliers"), function() {
|
||||
frappe.call({
|
||||
method: 'erpnext.buying.doctype.request_for_quotation.request_for_quotation.send_supplier_emails',
|
||||
freeze: true,
|
||||
@ -49,151 +45,143 @@ frappe.ui.form.on("Request for Quotation",{
|
||||
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) {
|
||||
var doc = frm.doc;
|
||||
var dialog = new frappe.ui.Dialog({
|
||||
title: __("For Supplier"),
|
||||
title: __("Create Supplier Quotation"),
|
||||
fields: [
|
||||
{ "fieldtype": "Select", "label": __("Supplier"),
|
||||
"fieldname": "supplier",
|
||||
"options": doc.suppliers.map(d => d.supplier),
|
||||
"reqd": 1,
|
||||
"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() {
|
||||
var args = dialog.get_values();
|
||||
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.fields_dict['supplier'].df.onchange = () => {
|
||||
var supplier = dialog.get_value('supplier');
|
||||
frm.call('get_supplier_email_preview', {supplier: supplier}).then(result => {
|
||||
dialog.fields_dict.email_preview.$wrapper.empty();
|
||||
dialog.fields_dict.email_preview.$wrapper.append(result.message);
|
||||
});
|
||||
});
|
||||
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({
|
||||
@ -274,9 +226,10 @@ erpnext.buying.RequestforQuotationController = erpnext.buying.BuyingController.e
|
||||
per_ordered: ["<", 99.99]
|
||||
}
|
||||
})
|
||||
}, __("Get items from"));
|
||||
}, __("Get Items From"));
|
||||
|
||||
// Get items from Opportunity
|
||||
this.frm.add_custom_button(__('Opportunity'),
|
||||
this.frm.add_custom_button(__('Opportunity'),
|
||||
function() {
|
||||
erpnext.utils.map_current_doc({
|
||||
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
|
||||
},
|
||||
})
|
||||
}, __("Get items from"));
|
||||
}, __("Get Items From"));
|
||||
|
||||
// Get items from open Material Requests based on supplier
|
||||
this.frm.add_custom_button(__('Possible Supplier'), function() {
|
||||
// Create a dialog window for the user to pick their supplier
|
||||
@ -324,8 +278,13 @@ erpnext.buying.RequestforQuotationController = erpnext.buying.BuyingController.e
|
||||
}
|
||||
}
|
||||
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() {
|
||||
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
|
||||
$.extend(cur_frm.cscript, new erpnext.buying.RequestforQuotationController({frm: cur_frm}));
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"actions": "",
|
||||
"actions": [],
|
||||
"allow_import": 1,
|
||||
"autoname": "naming_series:",
|
||||
"creation": "2016-02-25 01:24:07.224790",
|
||||
@ -12,25 +12,27 @@
|
||||
"vendor",
|
||||
"column_break1",
|
||||
"transaction_date",
|
||||
"status",
|
||||
"amended_from",
|
||||
"suppliers_section",
|
||||
"suppliers",
|
||||
"get_suppliers_button",
|
||||
"items_section",
|
||||
"items",
|
||||
"link_to_mrs",
|
||||
"supplier_response_section",
|
||||
"salutation",
|
||||
"email_template",
|
||||
"col_break_email_1",
|
||||
"subject",
|
||||
"preview",
|
||||
"sec_break_email_2",
|
||||
"message_for_supplier",
|
||||
"terms_section_break",
|
||||
"tc_name",
|
||||
"terms",
|
||||
"printing_settings",
|
||||
"select_print_heading",
|
||||
"letter_head",
|
||||
"more_info",
|
||||
"status",
|
||||
"column_break3",
|
||||
"amended_from"
|
||||
"letter_head"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@ -78,6 +80,7 @@
|
||||
"width": "50%"
|
||||
},
|
||||
{
|
||||
"default": "Today",
|
||||
"fieldname": "transaction_date",
|
||||
"fieldtype": "Date",
|
||||
"in_list_view": 1,
|
||||
@ -94,16 +97,11 @@
|
||||
{
|
||||
"fieldname": "suppliers",
|
||||
"fieldtype": "Table",
|
||||
"label": "Supplier Detail",
|
||||
"label": "Suppliers",
|
||||
"options": "Request for Quotation Supplier",
|
||||
"print_hide": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "get_suppliers_button",
|
||||
"fieldtype": "Button",
|
||||
"label": "Get Suppliers"
|
||||
},
|
||||
{
|
||||
"fieldname": "items_section",
|
||||
"fieldtype": "Section Break",
|
||||
@ -126,8 +124,10 @@
|
||||
"label": "Link to Material Requests"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.__islocal",
|
||||
"fieldname": "supplier_response_section",
|
||||
"fieldtype": "Section Break"
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Email Details"
|
||||
},
|
||||
{
|
||||
"fieldname": "email_template",
|
||||
@ -137,6 +137,9 @@
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fetch_from": "email_template.response",
|
||||
"fetch_if_empty": 1,
|
||||
"fieldname": "message_for_supplier",
|
||||
"fieldtype": "Text Editor",
|
||||
"in_list_view": 1,
|
||||
@ -197,14 +200,6 @@
|
||||
"options": "Letter Head",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "more_info",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "More Information",
|
||||
"oldfieldtype": "Section Break",
|
||||
"options": "fa fa-file-text"
|
||||
},
|
||||
{
|
||||
"fieldname": "status",
|
||||
"fieldtype": "Select",
|
||||
@ -218,10 +213,6 @@
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break3",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "amended_from",
|
||||
"fieldtype": "Link",
|
||||
@ -230,12 +221,46 @@
|
||||
"options": "Request for Quotation",
|
||||
"print_hide": 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",
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-06-25 14:37:21.140194",
|
||||
"modified": "2020-10-16 17:49:09.561929",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Request for Quotation",
|
||||
|
@ -28,6 +28,10 @@ class RequestforQuotation(BuyingController):
|
||||
super(RequestforQuotation, self).set_qty_as_per_stock_uom()
|
||||
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):
|
||||
supplier_list = [d.supplier for d in self.suppliers]
|
||||
if len(supplier_list) != len(set(supplier_list)):
|
||||
@ -51,7 +55,7 @@ class RequestforQuotation(BuyingController):
|
||||
|
||||
def validate_email_id(self, args):
|
||||
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):
|
||||
frappe.db.set(self, 'status', 'Submitted')
|
||||
@ -62,43 +66,58 @@ class RequestforQuotation(BuyingController):
|
||||
def on_cancel(self):
|
||||
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):
|
||||
"""Sends RFQ mail to involved suppliers."""
|
||||
for rfq_supplier in self.suppliers:
|
||||
if rfq_supplier.send_email:
|
||||
self.validate_email_id(rfq_supplier)
|
||||
|
||||
# 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())
|
||||
rfq_supplier.email_sent = 1
|
||||
if not rfq_supplier.contact:
|
||||
rfq_supplier.contact = contact
|
||||
rfq_supplier.save()
|
||||
|
||||
def get_link(self):
|
||||
# RFQ link for supplier portal
|
||||
return get_url("/rfq/" + self.name)
|
||||
|
||||
def update_supplier_part_no(self, args):
|
||||
self.vendor = args.supplier
|
||||
def update_supplier_part_no(self, supplier):
|
||||
self.vendor = supplier
|
||||
for item in self.items:
|
||||
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):
|
||||
'''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):
|
||||
user = frappe.get_doc("User", rfq_supplier.email_id)
|
||||
else:
|
||||
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:
|
||||
contact = frappe.get_doc("Contact", rfq_supplier.contact)
|
||||
else:
|
||||
@ -115,6 +134,10 @@ class RequestforQuotation(BuyingController):
|
||||
|
||||
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):
|
||||
user = frappe.get_doc({
|
||||
'doctype': 'User',
|
||||
@ -129,22 +152,36 @@ class RequestforQuotation(BuyingController):
|
||||
|
||||
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'])
|
||||
if full_name == "Guest":
|
||||
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 = {
|
||||
'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,
|
||||
'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"
|
||||
sender = frappe.session.user not in STANDARD_USERS and frappe.session.user or None
|
||||
message = frappe.get_template(template).render(args)
|
||||
|
||||
if preview:
|
||||
return message
|
||||
|
||||
attachments = self.get_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):
|
||||
for supplier in self.suppliers:
|
||||
if sup_name == None or supplier.supplier == sup_name:
|
||||
if supplier.quote_status != _('No Quote'):
|
||||
quote_status = _('Received')
|
||||
for item in self.items:
|
||||
sqi_count = frappe.db.sql("""
|
||||
SELECT
|
||||
COUNT(sqi.name) as count
|
||||
FROM
|
||||
`tabSupplier Quotation Item` as sqi,
|
||||
`tabSupplier Quotation` as sq
|
||||
WHERE sq.supplier = %(supplier)s
|
||||
AND sqi.docstatus = 1
|
||||
AND sqi.request_for_quotation_item = %(rqi)s
|
||||
AND sqi.parent = sq.name""",
|
||||
{"supplier": supplier.supplier, "rqi": item.name}, as_dict=1)[0]
|
||||
if (sqi_count.count) == 0:
|
||||
quote_status = _('Pending')
|
||||
supplier.quote_status = quote_status
|
||||
quote_status = _('Received')
|
||||
for item in self.items:
|
||||
sqi_count = frappe.db.sql("""
|
||||
SELECT
|
||||
COUNT(sqi.name) as count
|
||||
FROM
|
||||
`tabSupplier Quotation Item` as sqi,
|
||||
`tabSupplier Quotation` as sq
|
||||
WHERE sq.supplier = %(supplier)s
|
||||
AND sqi.docstatus = 1
|
||||
AND sqi.request_for_quotation_item = %(rqi)s
|
||||
AND sqi.parent = sq.name""",
|
||||
{"supplier": supplier.supplier, "rqi": item.name}, as_dict=1)[0]
|
||||
if (sqi_count.count) == 0:
|
||||
quote_status = _('Pending')
|
||||
supplier.quote_status = quote_status
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@ -289,16 +325,15 @@ def create_rfq_items(sq_doc, supplier, data):
|
||||
})
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_pdf(doctype, name, supplier_idx):
|
||||
doc = get_rfq_doc(doctype, name, supplier_idx)
|
||||
def get_pdf(doctype, name, supplier):
|
||||
doc = get_rfq_doc(doctype, name, supplier)
|
||||
if doc:
|
||||
download_pdf(doctype, name, doc=doc)
|
||||
|
||||
def get_rfq_doc(doctype, name, supplier_idx):
|
||||
if cint(supplier_idx):
|
||||
def get_rfq_doc(doctype, name, supplier):
|
||||
if supplier:
|
||||
doc = frappe.get_doc(doctype, name)
|
||||
args = doc.get('suppliers')[cint(supplier_idx) - 1]
|
||||
doc.update_supplier_part_no(args)
|
||||
doc.update_supplier_part_no(supplier)
|
||||
return doc
|
||||
|
||||
@frappe.whitelist()
|
||||
|
@ -9,7 +9,7 @@ import frappe
|
||||
from frappe.utils import nowdate
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.templates.pages.rfq import check_supplier_has_docname_access
|
||||
from erpnext.buying.doctype.request_for_quotation.request_for_quotation import make_supplier_quotation
|
||||
from erpnext.buying.doctype.request_for_quotation.request_for_quotation import make_supplier_quotation_from_rfq
|
||||
from erpnext.buying.doctype.request_for_quotation.request_for_quotation import create_supplier_quotation
|
||||
from erpnext.crm.doctype.opportunity.test_opportunity import make_opportunity
|
||||
from erpnext.crm.doctype.opportunity.opportunity import make_request_for_quotation as make_rfq
|
||||
@ -22,25 +22,21 @@ class TestRequestforQuotation(unittest.TestCase):
|
||||
self.assertEqual(rfq.get('suppliers')[1].quote_status, 'Pending')
|
||||
|
||||
# Submit the first supplier quotation
|
||||
sq = make_supplier_quotation(rfq.name, rfq.get('suppliers')[0].supplier)
|
||||
sq = make_supplier_quotation_from_rfq(rfq.name, for_supplier=rfq.get('suppliers')[0].supplier)
|
||||
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)
|
||||
|
||||
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):
|
||||
rfq = make_request_for_quotation()
|
||||
|
||||
sq = make_supplier_quotation(rfq.name, rfq.get('suppliers')[0].supplier)
|
||||
sq = make_supplier_quotation_from_rfq(rfq.name, for_supplier=rfq.get('suppliers')[0].supplier)
|
||||
sq.submit()
|
||||
|
||||
sq1 = make_supplier_quotation(rfq.name, rfq.get('suppliers')[1].supplier)
|
||||
sq1 = make_supplier_quotation_from_rfq(rfq.name, for_supplier=rfq.get('suppliers')[1].supplier)
|
||||
sq1.submit()
|
||||
|
||||
self.assertEqual(sq.supplier, rfq.get('suppliers')[0].supplier)
|
||||
@ -62,7 +58,7 @@ class TestRequestforQuotation(unittest.TestCase):
|
||||
|
||||
rfq = make_request_for_quotation(supplier_data=supplier_wt_appos)
|
||||
|
||||
sq = make_supplier_quotation(rfq.name, supplier_wt_appos[0].get("supplier"))
|
||||
sq = make_supplier_quotation_from_rfq(rfq.name, for_supplier=supplier_wt_appos[0].get("supplier"))
|
||||
sq.submit()
|
||||
|
||||
frappe.form_dict = frappe.local("form_dict")
|
||||
|
@ -84,9 +84,6 @@ QUnit.test("Test: Request for Quotation", function (assert) {
|
||||
cur_frm.fields_dict.suppliers.grid.grid_rows[0].toggle_view();
|
||||
},
|
||||
() => frappe.timeout(1),
|
||||
() => {
|
||||
frappe.click_check('No Quote');
|
||||
},
|
||||
() => frappe.timeout(1),
|
||||
() => {
|
||||
cur_frm.cur_grid.toggle_view();
|
||||
@ -125,7 +122,6 @@ QUnit.test("Test: Request for Quotation", function (assert) {
|
||||
() => 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[0].doc.no_quote == 1);
|
||||
},
|
||||
() => done()
|
||||
]);
|
||||
|
@ -27,10 +27,11 @@
|
||||
"stock_qty",
|
||||
"warehouse_and_reference",
|
||||
"warehouse",
|
||||
"project_name",
|
||||
"col_break4",
|
||||
"material_request",
|
||||
"material_request_item",
|
||||
"section_break_24",
|
||||
"project_name",
|
||||
"section_break_23",
|
||||
"page_break"
|
||||
],
|
||||
@ -161,7 +162,7 @@
|
||||
{
|
||||
"fieldname": "project_name",
|
||||
"fieldtype": "Link",
|
||||
"label": "Project Name",
|
||||
"label": "Project",
|
||||
"options": "Project",
|
||||
"print_hide": 1
|
||||
},
|
||||
@ -249,11 +250,18 @@
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "section_break_24",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Accounting Dimensions"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-06-12 19:10:36.333441",
|
||||
"modified": "2020-09-24 17:26:46.276934",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Request for Quotation Item",
|
||||
|
@ -1,362 +1,99 @@
|
||||
{
|
||||
"allow_copy": 0,
|
||||
"allow_guest_to_view": 0,
|
||||
"allow_import": 0,
|
||||
"allow_rename": 0,
|
||||
"beta": 0,
|
||||
"creation": "2016-03-29 05:59:11.896885",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"actions": [],
|
||||
"creation": "2016-03-29 05:59:11.896885",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"send_email",
|
||||
"email_sent",
|
||||
"supplier",
|
||||
"contact",
|
||||
"quote_status",
|
||||
"column_break_3",
|
||||
"supplier_name",
|
||||
"email_id"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 1,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "1",
|
||||
"fieldname": "send_email",
|
||||
"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": "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_on_submit": 1,
|
||||
"columns": 2,
|
||||
"default": "1",
|
||||
"fieldname": "send_email",
|
||||
"fieldtype": "Check",
|
||||
"in_list_view": 1,
|
||||
"label": "Send Email"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 1,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.docstatus >= 1",
|
||||
"fieldname": "email_sent",
|
||||
"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",
|
||||
"length": 0,
|
||||
"no_copy": 1,
|
||||
"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_on_submit": 1,
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.docstatus >= 1",
|
||||
"fieldname": "email_sent",
|
||||
"fieldtype": "Check",
|
||||
"label": "Email Sent",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 4,
|
||||
"fieldname": "supplier",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Supplier",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Supplier",
|
||||
"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
|
||||
},
|
||||
"columns": 2,
|
||||
"fieldname": "supplier",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Supplier",
|
||||
"options": "Supplier",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 3,
|
||||
"fieldname": "contact",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Contact",
|
||||
"length": 0,
|
||||
"no_copy": 1,
|
||||
"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_on_submit": 1,
|
||||
"columns": 2,
|
||||
"fieldname": "contact",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Contact",
|
||||
"no_copy": 1,
|
||||
"options": "Contact"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 1,
|
||||
"bold": 0,
|
||||
"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_on_submit": 1,
|
||||
"depends_on": "eval:doc.docstatus >= 1",
|
||||
"fieldname": "quote_status",
|
||||
"fieldtype": "Select",
|
||||
"label": "Quote Status",
|
||||
"options": "Pending\nReceived",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"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",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"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
|
||||
},
|
||||
"fieldname": "column_break_3",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "column_break_3",
|
||||
"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,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"bold": 1,
|
||||
"fetch_from": "supplier.supplier_name",
|
||||
"fieldname": "supplier_name",
|
||||
"fieldtype": "Read Only",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 1,
|
||||
"in_list_view": 0,
|
||||
"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
|
||||
},
|
||||
"fieldname": "supplier_name",
|
||||
"fieldtype": "Read Only",
|
||||
"in_global_search": 1,
|
||||
"label": "Supplier Name"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 3,
|
||||
"columns": 3,
|
||||
"fetch_from": "contact.email_id",
|
||||
"fieldname": "email_id",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Email Id",
|
||||
"length": 0,
|
||||
"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
|
||||
"fieldname": "email_id",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Email Id",
|
||||
"no_copy": 1
|
||||
}
|
||||
],
|
||||
"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,
|
||||
"max_attachments": 0,
|
||||
"modified": "2018-05-16 22:43:30.212408",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Request for Quotation Supplier",
|
||||
"name_case": "",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 0,
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"show_name_in_global_search": 0,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1,
|
||||
"track_seen": 0
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-10-16 12:23:41.769820",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Request for Quotation Supplier",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
@ -91,12 +91,7 @@ class SupplierQuotation(BuyingController):
|
||||
for my_item in self.items) if include_me else 0
|
||||
if (sqi_count.count + self_count) == 0:
|
||||
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)
|
||||
|
||||
def get_list_context(context=None):
|
||||
|
@ -241,7 +241,7 @@
|
||||
"fieldname": "rate",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Rate ",
|
||||
"label": "Rate",
|
||||
"oldfieldname": "import_rate",
|
||||
"oldfieldtype": "Currency",
|
||||
"options": "currency"
|
||||
@ -560,7 +560,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-10-01 16:34:39.703033",
|
||||
"modified": "2020-10-19 12:36:26.913211",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Supplier Quotation Item",
|
||||
|
@ -143,7 +143,7 @@ def get_conditions(filters):
|
||||
conditions = ""
|
||||
|
||||
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"):
|
||||
conditions += """
|
||||
@ -151,10 +151,10 @@ def get_conditions(filters):
|
||||
""" % (frappe.db.escape(filters.get('cost_center')), frappe.db.escape(filters.get('project')))
|
||||
|
||||
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"):
|
||||
conditions += " AND par.transaction_date<='%s'" % filters.get('to_date')
|
||||
conditions += " AND parent.transaction_date<='%s'" % filters.get('to_date')
|
||||
return conditions
|
||||
|
||||
def get_data(filters):
|
||||
@ -198,21 +198,23 @@ def get_mapped_mr_details(conditions):
|
||||
mr_records = {}
|
||||
mr_details = frappe.db.sql("""
|
||||
SELECT
|
||||
par.transaction_date,
|
||||
par.per_ordered,
|
||||
par.owner,
|
||||
parent.transaction_date,
|
||||
parent.per_ordered,
|
||||
parent.owner,
|
||||
child.name,
|
||||
child.parent,
|
||||
child.amount,
|
||||
child.qty,
|
||||
child.item_code,
|
||||
child.uom,
|
||||
par.status
|
||||
FROM `tabMaterial Request` par, `tabMaterial Request Item` child
|
||||
parent.status,
|
||||
child.project,
|
||||
child.cost_center
|
||||
FROM `tabMaterial Request` parent, `tabMaterial Request Item` child
|
||||
WHERE
|
||||
par.per_ordered>=0
|
||||
AND par.name=child.parent
|
||||
AND par.docstatus=1
|
||||
parent.per_ordered>=0
|
||||
AND parent.name=child.parent
|
||||
AND parent.docstatus=1
|
||||
{conditions}
|
||||
""".format(conditions=conditions), as_dict=1) #nosec
|
||||
|
||||
@ -232,7 +234,9 @@ def get_mapped_mr_details(conditions):
|
||||
status=record.status,
|
||||
actual_cost=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)
|
||||
return mr_records, procurement_record_against_mr
|
||||
@ -280,16 +284,16 @@ def get_po_entries(conditions):
|
||||
child.amount,
|
||||
child.base_amount,
|
||||
child.schedule_date,
|
||||
par.transaction_date,
|
||||
par.supplier,
|
||||
par.status,
|
||||
par.owner
|
||||
FROM `tabPurchase Order` par, `tabPurchase Order Item` child
|
||||
parent.transaction_date,
|
||||
parent.supplier,
|
||||
parent.status,
|
||||
parent.owner
|
||||
FROM `tabPurchase Order` parent, `tabPurchase Order Item` child
|
||||
WHERE
|
||||
par.docstatus = 1
|
||||
AND par.name = child.parent
|
||||
AND par.status not in ("Closed","Completed","Cancelled")
|
||||
parent.docstatus = 1
|
||||
AND parent.name = child.parent
|
||||
AND parent.status not in ("Closed","Completed","Cancelled")
|
||||
{conditions}
|
||||
GROUP BY
|
||||
par.name, child.item_code
|
||||
parent.name, child.item_code
|
||||
""".format(conditions=conditions), as_dict=1) #nosec
|
@ -15,9 +15,9 @@ class CallLog(Document):
|
||||
number = strip_number(self.get('from'))
|
||||
self.contact = get_contact_with_phone_number(number)
|
||||
self.lead = get_lead_with_phone_number(number)
|
||||
|
||||
contact = frappe.get_doc("Contact", self.contact)
|
||||
self.customer = contact.get_link_for("Customer")
|
||||
if self.contact:
|
||||
contact = frappe.get_doc("Contact", self.contact)
|
||||
self.customer = contact.get_link_for("Customer")
|
||||
|
||||
def after_insert(self):
|
||||
self.trigger_call_popup()
|
||||
|
@ -63,8 +63,8 @@ def get_data():
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Chart of Accounts Importer",
|
||||
"labe": _("Chart Of Accounts Importer"),
|
||||
"description": _("Import Chart Of Accounts from CSV / Excel files"),
|
||||
"label": _("Chart of Accounts Importer"),
|
||||
"description": _("Import Chart of Accounts from CSV / Excel files"),
|
||||
"onboard": 1
|
||||
},
|
||||
{
|
||||
|
@ -605,8 +605,6 @@ class AccountsController(TransactionBase):
|
||||
from erpnext.accounts.utils import unlink_ref_doc_from_payment_entries
|
||||
|
||||
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'):
|
||||
unlink_ref_doc_from_payment_entries(self)
|
||||
|
||||
@ -948,8 +946,10 @@ def validate_conversion_rate(currency, conversion_rate, conversion_rate_label, c
|
||||
company_currency = frappe.get_cached_value('Company', company, "default_currency")
|
||||
|
||||
if not conversion_rate:
|
||||
throw(_("{0} is mandatory. Maybe Currency Exchange record is not created for {1} to {2}.").format(
|
||||
conversion_rate_label, currency, company_currency))
|
||||
throw(
|
||||
_("{0} is mandatory. Maybe Currency Exchange record is not created for {1} to {2}.")
|
||||
.format(conversion_rate_label, currency, company_currency)
|
||||
)
|
||||
|
||||
|
||||
def validate_taxes_and_charges(tax):
|
||||
@ -1170,6 +1170,31 @@ def set_child_tax_template_and_map(item, child_item, parent_doc):
|
||||
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)
|
||||
|
||||
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):
|
||||
"""
|
||||
Returns a Sales Order Item child item containing the default values
|
||||
@ -1185,6 +1210,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"))
|
||||
child_item.conversion_factor = flt(trans_item.get('conversion_factor')) or conversion_factor
|
||||
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)
|
||||
if not child_item.warehouse:
|
||||
frappe.throw(_("Cannot find {} for item {}. Please set the same in Item Master or Stock Settings.")
|
||||
@ -1209,6 +1235,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_amount = 1 # Initiallize value will update in parent validation
|
||||
set_child_tax_template_and_map(item, child_item, p_doc)
|
||||
add_taxes_from_tax_template(child_item, p_doc)
|
||||
return child_item
|
||||
|
||||
def validate_and_delete_children(parent, data):
|
||||
|
@ -5,7 +5,7 @@ from __future__ import unicode_literals
|
||||
import frappe
|
||||
from frappe import _, msgprint
|
||||
from frappe.utils import flt,cint, cstr, getdate
|
||||
|
||||
from six import iteritems
|
||||
from erpnext.accounts.party import get_party_details
|
||||
from erpnext.stock.get_item_details import get_conversion_factor
|
||||
from erpnext.buying.utils import validate_for_items, update_last_purchase_rate
|
||||
@ -112,8 +112,8 @@ class BuyingController(StockController):
|
||||
"docstatus": 1
|
||||
})]
|
||||
if self.is_return and len(not_cancelled_asset):
|
||||
frappe.throw(_("{} has submitted assets linked to it. You need to cancel the assets to create purchase return.".format(self.return_against)),
|
||||
title=_("Not Allowed"))
|
||||
frappe.throw(_("{} has submitted assets linked to it. You need to cancel the assets to create purchase return.")
|
||||
.format(self.return_against), title=_("Not Allowed"))
|
||||
|
||||
def get_asset_items(self):
|
||||
if self.doctype not in ['Purchase Order', 'Purchase Invoice', 'Purchase Receipt']:
|
||||
@ -298,10 +298,10 @@ class BuyingController(StockController):
|
||||
title=_("Limit Crossed"))
|
||||
|
||||
transferred_batch_qty_map = get_transferred_batch_qty_map(item.purchase_order, item.item_code)
|
||||
backflushed_batch_qty_map = get_backflushed_batch_qty_map(item.purchase_order, item.item_code)
|
||||
# backflushed_batch_qty_map = get_backflushed_batch_qty_map(item.purchase_order, item.item_code)
|
||||
|
||||
for raw_material in transferred_raw_materials + non_stock_items:
|
||||
rm_item_key = '{}{}'.format(raw_material.rm_item_code, item.purchase_order)
|
||||
rm_item_key = (raw_material.rm_item_code, item.purchase_order)
|
||||
raw_material_data = backflushed_raw_materials_map.get(rm_item_key, {})
|
||||
|
||||
consumed_qty = raw_material_data.get('qty', 0)
|
||||
@ -330,8 +330,10 @@ class BuyingController(StockController):
|
||||
set_serial_nos(raw_material, consumed_serial_nos, qty)
|
||||
|
||||
if raw_material.batch_nos:
|
||||
backflushed_batch_qty_map = raw_material_data.get('consumed_batch', {})
|
||||
|
||||
batches_qty = get_batches_with_qty(raw_material.rm_item_code, raw_material.main_item_code,
|
||||
qty, transferred_batch_qty_map, backflushed_batch_qty_map)
|
||||
qty, transferred_batch_qty_map, backflushed_batch_qty_map, item.purchase_order)
|
||||
for batch_data in batches_qty:
|
||||
qty = batch_data['qty']
|
||||
raw_material.batch_no = batch_data['batch']
|
||||
@ -343,6 +345,10 @@ class BuyingController(StockController):
|
||||
rm = self.append('supplied_items', {})
|
||||
rm.update(raw_material_data)
|
||||
|
||||
if not rm.main_item_code:
|
||||
rm.main_item_code = fg_item_doc.item_code
|
||||
|
||||
rm.reference_name = fg_item_doc.name
|
||||
rm.required_qty = qty
|
||||
rm.consumed_qty = qty
|
||||
|
||||
@ -792,8 +798,8 @@ class BuyingController(StockController):
|
||||
asset.set(field, None)
|
||||
asset.supplier = None
|
||||
if asset.docstatus == 1 and delete_asset:
|
||||
frappe.throw(_('Cannot cancel this document as it is linked with submitted asset {0}.\
|
||||
Please cancel the it to continue.').format(frappe.utils.get_link_to_form('Asset', asset.name)))
|
||||
frappe.throw(_('Cannot cancel this document as it is linked with submitted asset {0}. Please cancel it to continue.')
|
||||
.format(frappe.utils.get_link_to_form('Asset', asset.name)))
|
||||
|
||||
asset.flags.ignore_validate_update_after_submit = True
|
||||
asset.flags.ignore_mandatory = True
|
||||
@ -873,7 +879,7 @@ def get_subcontracted_raw_materials_from_se(purchase_order, fg_item):
|
||||
AND se.purpose='Send to Subcontractor'
|
||||
AND se.purchase_order = %s
|
||||
AND IFNULL(sed.t_warehouse, '') != ''
|
||||
AND sed.subcontracted_item = %s
|
||||
AND IFNULL(sed.subcontracted_item, '') in ('', %s)
|
||||
GROUP BY sed.item_code, sed.subcontracted_item
|
||||
"""
|
||||
raw_materials = frappe.db.multisql({
|
||||
@ -890,39 +896,49 @@ def get_subcontracted_raw_materials_from_se(purchase_order, fg_item):
|
||||
return raw_materials
|
||||
|
||||
def get_backflushed_subcontracted_raw_materials(purchase_orders):
|
||||
common_query = """
|
||||
SELECT
|
||||
CONCAT(prsi.rm_item_code, pri.purchase_order) AS item_key,
|
||||
SUM(prsi.consumed_qty) AS qty,
|
||||
{serial_no_concat_syntax} AS serial_nos,
|
||||
{batch_no_concat_syntax} AS batch_nos
|
||||
FROM `tabPurchase Receipt` pr, `tabPurchase Receipt Item` pri, `tabPurchase Receipt Item Supplied` prsi
|
||||
WHERE
|
||||
pr.name = pri.parent
|
||||
AND pr.name = prsi.parent
|
||||
AND pri.purchase_order IN %s
|
||||
AND pri.item_code = prsi.main_item_code
|
||||
AND pr.docstatus = 1
|
||||
GROUP BY prsi.rm_item_code, pri.purchase_order
|
||||
"""
|
||||
purchase_receipts = frappe.get_all("Purchase Receipt Item",
|
||||
fields = ["purchase_order", "item_code", "name", "parent"],
|
||||
filters={"docstatus": 1, "purchase_order": ("in", list(purchase_orders))})
|
||||
|
||||
backflushed_raw_materials = frappe.db.multisql({
|
||||
'mariadb': common_query.format(
|
||||
serial_no_concat_syntax="GROUP_CONCAT(prsi.serial_no)",
|
||||
batch_no_concat_syntax="GROUP_CONCAT(prsi.batch_no)"
|
||||
),
|
||||
'postgres': common_query.format(
|
||||
serial_no_concat_syntax="STRING_AGG(prsi.serial_no, ',')",
|
||||
batch_no_concat_syntax="STRING_AGG(prsi.batch_no, ',')"
|
||||
)
|
||||
}, (purchase_orders, ), as_dict=1)
|
||||
distinct_purchase_receipts = {}
|
||||
for pr in purchase_receipts:
|
||||
key = (pr.purchase_order, pr.item_code, pr.parent)
|
||||
distinct_purchase_receipts.setdefault(key, []).append(pr.name)
|
||||
|
||||
backflushed_raw_materials_map = frappe._dict()
|
||||
for item in backflushed_raw_materials:
|
||||
backflushed_raw_materials_map.setdefault(item.item_key, item)
|
||||
for args, references in iteritems(distinct_purchase_receipts):
|
||||
purchase_receipt_supplied_items = get_supplied_items(args[1], args[2], references)
|
||||
|
||||
for data in purchase_receipt_supplied_items:
|
||||
pr_key = (data.rm_item_code, args[0])
|
||||
if pr_key not in backflushed_raw_materials_map:
|
||||
backflushed_raw_materials_map.setdefault(pr_key, frappe._dict({
|
||||
"qty": 0.0,
|
||||
"serial_no": [],
|
||||
"batch_no": [],
|
||||
"consumed_batch": {}
|
||||
}))
|
||||
|
||||
row = backflushed_raw_materials_map.get(pr_key)
|
||||
row.qty += data.consumed_qty
|
||||
|
||||
for field in ["serial_no", "batch_no"]:
|
||||
if data.get(field):
|
||||
row[field].append(data.get(field))
|
||||
|
||||
if data.get("batch_no"):
|
||||
if data.get("batch_no") in row.consumed_batch:
|
||||
row.consumed_batch[data.get("batch_no")] += data.consumed_qty
|
||||
else:
|
||||
row.consumed_batch[data.get("batch_no")] = data.consumed_qty
|
||||
|
||||
return backflushed_raw_materials_map
|
||||
|
||||
def get_supplied_items(item_code, purchase_receipt, references):
|
||||
return frappe.get_all("Purchase Receipt Item Supplied",
|
||||
fields=["rm_item_code", "consumed_qty", "serial_no", "batch_no"],
|
||||
filters={"main_item_code": item_code, "parent": purchase_receipt, "reference_name": ("in", references)})
|
||||
|
||||
def get_asset_item_details(asset_items):
|
||||
asset_items_data = {}
|
||||
for d in frappe.get_all('Item', fields = ["name", "auto_create_assets", "asset_naming_series"],
|
||||
@ -1004,14 +1020,15 @@ def get_transferred_batch_qty_map(purchase_order, fg_item):
|
||||
SELECT
|
||||
sed.batch_no,
|
||||
SUM(sed.qty) AS qty,
|
||||
sed.item_code
|
||||
sed.item_code,
|
||||
sed.subcontracted_item
|
||||
FROM `tabStock Entry` se,`tabStock Entry Detail` sed
|
||||
WHERE
|
||||
se.name = sed.parent
|
||||
AND se.docstatus=1
|
||||
AND se.purpose='Send to Subcontractor'
|
||||
AND se.purchase_order = %s
|
||||
AND sed.subcontracted_item = %s
|
||||
AND ifnull(sed.subcontracted_item, '') in ('', %s)
|
||||
AND sed.batch_no IS NOT NULL
|
||||
GROUP BY
|
||||
sed.batch_no,
|
||||
@ -1019,8 +1036,10 @@ def get_transferred_batch_qty_map(purchase_order, fg_item):
|
||||
""", (purchase_order, fg_item), as_dict=1)
|
||||
|
||||
for batch_data in transferred_batches:
|
||||
transferred_batch_qty_map.setdefault((batch_data.item_code, fg_item), {})
|
||||
transferred_batch_qty_map[(batch_data.item_code, fg_item)][batch_data.batch_no] = batch_data.qty
|
||||
key = ((batch_data.item_code, fg_item)
|
||||
if batch_data.subcontracted_item else (batch_data.item_code, purchase_order))
|
||||
transferred_batch_qty_map.setdefault(key, {})
|
||||
transferred_batch_qty_map[key][batch_data.batch_no] = batch_data.qty
|
||||
|
||||
return transferred_batch_qty_map
|
||||
|
||||
@ -1057,10 +1076,11 @@ def get_backflushed_batch_qty_map(purchase_order, fg_item):
|
||||
|
||||
return backflushed_batch_qty_map
|
||||
|
||||
def get_batches_with_qty(item_code, fg_item, required_qty, transferred_batch_qty_map, backflushed_batch_qty_map):
|
||||
def get_batches_with_qty(item_code, fg_item, required_qty, transferred_batch_qty_map, backflushed_batches, po):
|
||||
# Returns available batches to be backflushed based on requirements
|
||||
transferred_batches = transferred_batch_qty_map.get((item_code, fg_item), {})
|
||||
backflushed_batches = backflushed_batch_qty_map.get((item_code, fg_item), {})
|
||||
if not transferred_batches:
|
||||
transferred_batches = transferred_batch_qty_map.get((item_code, po), {})
|
||||
|
||||
available_batches = []
|
||||
|
||||
|
@ -52,7 +52,7 @@ class StockController(AccountsController):
|
||||
frappe.throw(_("Row #{0}: Serial No {1} does not belong to Batch {2}")
|
||||
.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")
|
||||
|
||||
if expiry_date and getdate(expiry_date) < getdate(self.posting_date):
|
||||
|
@ -241,6 +241,7 @@
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.__islocal",
|
||||
"description": "Home, Work, etc.",
|
||||
"fieldname": "address_title",
|
||||
"fieldtype": "Data",
|
||||
"label": "Address Title"
|
||||
@ -249,7 +250,8 @@
|
||||
"depends_on": "eval: doc.__islocal",
|
||||
"fieldname": "address_line1",
|
||||
"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",
|
||||
@ -261,7 +263,8 @@
|
||||
"depends_on": "eval: doc.__islocal",
|
||||
"fieldname": "city",
|
||||
"fieldtype": "Data",
|
||||
"label": "City/Town"
|
||||
"label": "City/Town",
|
||||
"mandatory_depends_on": "eval: doc.address_title && doc.address_type"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.__islocal",
|
||||
@ -280,6 +283,7 @@
|
||||
"fieldname": "country",
|
||||
"fieldtype": "Link",
|
||||
"label": "Country",
|
||||
"mandatory_depends_on": "eval: doc.address_title && doc.address_type",
|
||||
"options": "Country"
|
||||
},
|
||||
{
|
||||
@ -449,7 +453,7 @@
|
||||
"idx": 5,
|
||||
"image_field": "image",
|
||||
"links": [],
|
||||
"modified": "2020-06-18 14:39:41.835416",
|
||||
"modified": "2020-10-13 15:24:00.094811",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "Lead",
|
||||
|
@ -22,7 +22,8 @@ class Lead(SellingController):
|
||||
load_address_and_contact(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()
|
||||
|
||||
def after_insert(self):
|
||||
@ -133,15 +134,6 @@ class Lead(SellingController):
|
||||
# skipping country since the system auto-sets it from system defaults
|
||||
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({info_field: self.get(info_field) for info_field in info_fields})
|
||||
address.insert()
|
||||
@ -190,7 +182,7 @@ class Lead(SellingController):
|
||||
|
||||
def update_links(self):
|
||||
# update address links
|
||||
if self.address_doc:
|
||||
if hasattr(self, 'address_doc'):
|
||||
self.address_doc.append("links", {
|
||||
"link_doctype": "Lead",
|
||||
"link_name": self.name,
|
||||
|
@ -11,7 +11,7 @@ from erpnext.accounts.party import get_party_account_currency
|
||||
from erpnext.exceptions import InvalidCurrency
|
||||
from erpnext.stock.doctype.material_request.material_request import make_request_for_quotation
|
||||
from erpnext.buying.doctype.request_for_quotation.request_for_quotation import \
|
||||
make_supplier_quotation as make_quotation_from_rfq
|
||||
make_supplier_quotation_from_rfq
|
||||
|
||||
def work():
|
||||
frappe.set_user(frappe.db.get_global('demo_purchase_user'))
|
||||
@ -44,7 +44,7 @@ def work():
|
||||
rfq = frappe.get_doc('Request for Quotation', rfq.name)
|
||||
|
||||
for supplier in rfq.suppliers:
|
||||
supplier_quotation = make_quotation_from_rfq(rfq.name, supplier.supplier)
|
||||
supplier_quotation = make_supplier_quotation_from_rfq(rfq.name, for_supplier=supplier.supplier)
|
||||
supplier_quotation.save()
|
||||
supplier_quotation.submit()
|
||||
|
||||
|
@ -49,6 +49,22 @@ data = {
|
||||
'fieldname': 'reference_dn', 'label': 'Reference Name', 'fieldtype': 'Dynamic Link', 'options': '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'
|
||||
|
@ -7,7 +7,7 @@ import frappe
|
||||
import json
|
||||
from frappe import _
|
||||
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
|
||||
|
||||
def get_course(program):
|
||||
@ -67,6 +67,13 @@ def mark_attendance(students_present, students_absent, course_schedule=None, stu
|
||||
: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)
|
||||
absent = json.loads(students_absent)
|
||||
|
||||
|
@ -30,6 +30,23 @@ frappe.ui.form.on('Assessment Plan', {
|
||||
frappe.set_route('Form', 'Assessment Result Tool');
|
||||
}, __('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) {
|
||||
|
@ -12,8 +12,8 @@
|
||||
"assessment_group",
|
||||
"grading_scale",
|
||||
"column_break_2",
|
||||
"course",
|
||||
"program",
|
||||
"course",
|
||||
"academic_year",
|
||||
"academic_term",
|
||||
"section_break_5",
|
||||
@ -198,7 +198,7 @@
|
||||
],
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-05-09 14:56:26.746988",
|
||||
"modified": "2020-10-23 15:55:35.076251",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Education",
|
||||
"name": "Assessment Plan",
|
||||
|
@ -7,6 +7,23 @@ frappe.ui.form.on('Assessment Result', {
|
||||
frm.trigger('setup_chart');
|
||||
}
|
||||
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) {
|
||||
|
@ -6,9 +6,13 @@ from __future__ import unicode_literals
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import get_link_to_form
|
||||
from functools import reduce
|
||||
|
||||
class CourseEnrollment(Document):
|
||||
def validate(self):
|
||||
self.validate_duplication()
|
||||
|
||||
def get_progress(self, student):
|
||||
"""
|
||||
Returns Progress of given student for a particular course enrollment
|
||||
@ -27,13 +31,15 @@ class CourseEnrollment(Document):
|
||||
return []
|
||||
|
||||
def validate_duplication(self):
|
||||
enrollment = frappe.get_all("Course Enrollment", filters={
|
||||
enrollment = frappe.db.exists("Course Enrollment", {
|
||||
"student": self.student,
|
||||
"course": self.course,
|
||||
"program_enrollment": self.program_enrollment
|
||||
"program_enrollment": self.program_enrollment,
|
||||
"name": ("!=", self.name)
|
||||
})
|
||||
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):
|
||||
result = {k: ('Correct' if v else 'Wrong') for k,v in answers.items()}
|
||||
|
@ -17,8 +17,9 @@ class TestCourseEnrollment(unittest.TestCase):
|
||||
setup_program()
|
||||
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")
|
||||
course_enrollment = student.enroll_in_course("_Test Course 1", program_enrollment.name)
|
||||
make_course_activity(course_enrollment.name, "Article", "_Test Article 1-1")
|
||||
course_enrollment = frappe.db.get_value("Course Enrollment",
|
||||
{"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):
|
||||
student = get_student("_test_student_1@example.com")
|
||||
@ -30,5 +31,14 @@ class TestCourseEnrollment(unittest.TestCase):
|
||||
self.assertTrue(finished in progress)
|
||||
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,
|
||||
"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",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"actions": [],
|
||||
"creation": "2017-12-27 08:55:52.680284",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"academic_year",
|
||||
"academic_term",
|
||||
"department",
|
||||
"column_break_3",
|
||||
"program",
|
||||
"course",
|
||||
"student_group",
|
||||
"section_break_8",
|
||||
"other_details"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "academic_year",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Academic Year",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Academic Year",
|
||||
"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
|
||||
},
|
||||
"fieldname": "academic_year",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Academic Year",
|
||||
"options": "Academic Year",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "academic_term",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Academic Term",
|
||||
"length": 0,
|
||||
"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
|
||||
},
|
||||
"fieldname": "academic_term",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Academic Term",
|
||||
"options": "Academic Term"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "department",
|
||||
"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",
|
||||
"length": 0,
|
||||
"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
|
||||
},
|
||||
"fieldname": "department",
|
||||
"fieldtype": "Link",
|
||||
"label": "Department",
|
||||
"options": "Department"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "column_break_3",
|
||||
"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
|
||||
},
|
||||
"fieldname": "column_break_3",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "program",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Program",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Program",
|
||||
"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
|
||||
},
|
||||
"fieldname": "program",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Program",
|
||||
"options": "Program",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "course",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Course",
|
||||
"length": 0,
|
||||
"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
|
||||
},
|
||||
"fieldname": "course",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Course",
|
||||
"options": "Course"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "student_group",
|
||||
"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",
|
||||
"length": 0,
|
||||
"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
|
||||
},
|
||||
"fieldname": "student_group",
|
||||
"fieldtype": "Link",
|
||||
"label": "Student Group",
|
||||
"options": "Student Group"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "section_break_8",
|
||||
"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
|
||||
},
|
||||
"fieldname": "section_break_8",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "other_details",
|
||||
"fieldtype": "Small Text",
|
||||
"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": "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
|
||||
"fieldname": "other_details",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Other details"
|
||||
}
|
||||
],
|
||||
"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,
|
||||
"max_attachments": 0,
|
||||
"modified": "2018-11-04 03:38:30.902942",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Education",
|
||||
"name": "Instructor Log",
|
||||
"name_case": "",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"restrict_to_domain": "Education",
|
||||
"show_name_in_global_search": 0,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1,
|
||||
"track_seen": 0,
|
||||
"track_views": 0
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-10-23 15:15:50.759657",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Education",
|
||||
"name": "Instructor Log",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"restrict_to_domain": "Education",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
@ -49,6 +49,11 @@ class TestProgram(unittest.TestCase):
|
||||
self.assertEqual(course[1].name, "_Test Course 2")
|
||||
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):
|
||||
program = frappe.get_doc({
|
||||
"doctype": "Program",
|
||||
@ -68,7 +73,7 @@ def make_program_and_linked_courses(program_name, course_name_list):
|
||||
program = frappe.get_doc("Program", program_name)
|
||||
course_list = [make_course(course_name) for course_name in course_name_list]
|
||||
for course in course_list:
|
||||
program.append("courses", {"course": course})
|
||||
program.append("courses", {"course": course, "required": 1})
|
||||
program.save()
|
||||
return program
|
||||
|
||||
|
@ -17,9 +17,7 @@
|
||||
"in_list_view": 1,
|
||||
"label": "Course",
|
||||
"options": "Course",
|
||||
"reqd": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "course.course_name",
|
||||
@ -27,23 +25,19 @@
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Course Name",
|
||||
"read_only": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"default": "1",
|
||||
"fieldname": "required",
|
||||
"fieldtype": "Check",
|
||||
"in_list_view": 1,
|
||||
"label": "Mandatory",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"label": "Mandatory"
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-06-09 18:56:10.213241",
|
||||
"modified": "2020-09-15 18:14:22.816795",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Education",
|
||||
"name": "Program Course",
|
||||
|
@ -2,16 +2,24 @@
|
||||
// For license information, please see license.txt
|
||||
|
||||
|
||||
frappe.ui.form.on("Program Enrollment", {
|
||||
frappe.ui.form.on('Program Enrollment', {
|
||||
setup: function(frm) {
|
||||
frm.add_fetch('fee_structure', 'total_amount', 'amount');
|
||||
},
|
||||
|
||||
onload: function(frm, cdt, cdn){
|
||||
frm.set_query("academic_term", "fees", function(){
|
||||
return{
|
||||
"filters":{
|
||||
"academic_year": (frm.doc.academic_year)
|
||||
onload: function(frm) {
|
||||
frm.set_query('academic_term', function() {
|
||||
return {
|
||||
'filters':{
|
||||
'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) {
|
||||
frm.set_query("course", "courses", function(doc, cdt, cdn) {
|
||||
return{
|
||||
query: "erpnext.education.doctype.program_enrollment.program_enrollment.get_program_courses",
|
||||
frm.set_query('course', 'courses', function() {
|
||||
return {
|
||||
query: 'erpnext.education.doctype.program_enrollment.program_enrollment.get_program_courses',
|
||||
filters: {
|
||||
'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{
|
||||
query: "erpnext.education.doctype.program_enrollment.program_enrollment.get_students",
|
||||
query: 'erpnext.education.doctype.program_enrollment.program_enrollment.get_students',
|
||||
filters: {
|
||||
'academic_year': frm.doc.academic_year,
|
||||
'academic_term': frm.doc.academic_term
|
||||
@ -49,14 +57,14 @@ frappe.ui.form.on("Program Enrollment", {
|
||||
frm.events.get_courses(frm);
|
||||
if (frm.doc.program) {
|
||||
frappe.call({
|
||||
method: "erpnext.education.api.get_fee_schedule",
|
||||
method: 'erpnext.education.api.get_fee_schedule',
|
||||
args: {
|
||||
"program": frm.doc.program,
|
||||
"student_category": frm.doc.student_category
|
||||
'program': frm.doc.program,
|
||||
'student_category': frm.doc.student_category
|
||||
},
|
||||
callback: function(r) {
|
||||
if(r.message) {
|
||||
frm.set_value("fees" ,r.message);
|
||||
if (r.message) {
|
||||
frm.set_value('fees' ,r.message);
|
||||
frm.events.get_courses(frm);
|
||||
}
|
||||
}
|
||||
@ -65,17 +73,17 @@ frappe.ui.form.on("Program Enrollment", {
|
||||
},
|
||||
|
||||
student_category: function() {
|
||||
frappe.ui.form.trigger("Program Enrollment", "program");
|
||||
frappe.ui.form.trigger('Program Enrollment', 'program');
|
||||
},
|
||||
|
||||
get_courses: function(frm) {
|
||||
frm.set_value("courses",[]);
|
||||
frm.set_value('courses',[]);
|
||||
frappe.call({
|
||||
method: "get_courses",
|
||||
method: 'get_courses',
|
||||
doc:frm.doc,
|
||||
callback: function(r) {
|
||||
if(r.message) {
|
||||
frm.set_value("courses", r.message);
|
||||
if (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', {
|
||||
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 = [];
|
||||
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);
|
||||
});
|
||||
return { filters: [['Course', 'name', 'not in', course_list]] };
|
||||
|
@ -1,775 +1,218 @@
|
||||
{
|
||||
"allow_copy": 0,
|
||||
"allow_events_in_timeline": 0,
|
||||
"allow_guest_to_view": 0,
|
||||
"allow_import": 1,
|
||||
"allow_rename": 0,
|
||||
"autoname": "EDU-ENR-.YYYY.-.#####",
|
||||
"beta": 0,
|
||||
"creation": "2015-12-02 12:58:32.916080",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "Document",
|
||||
"editable_grid": 0,
|
||||
"engine": "InnoDB",
|
||||
"actions": [],
|
||||
"allow_import": 1,
|
||||
"autoname": "EDU-ENR-.YYYY.-.#####",
|
||||
"creation": "2015-12-02 12:58:32.916080",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Document",
|
||||
"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": [
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "student",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 1,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Student",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Student",
|
||||
"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
|
||||
},
|
||||
"fieldname": "student",
|
||||
"fieldtype": "Link",
|
||||
"in_global_search": 1,
|
||||
"label": "Student",
|
||||
"options": "Student",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fetch_from": "student.title",
|
||||
"fieldname": "student_name",
|
||||
"fieldtype": "Read Only",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 1,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Student Name",
|
||||
"length": 0,
|
||||
"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
|
||||
},
|
||||
"fetch_from": "student.title",
|
||||
"fieldname": "student_name",
|
||||
"fieldtype": "Read Only",
|
||||
"in_global_search": 1,
|
||||
"label": "Student Name",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "student_category",
|
||||
"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",
|
||||
"length": 0,
|
||||
"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
|
||||
},
|
||||
"fieldname": "student_category",
|
||||
"fieldtype": "Link",
|
||||
"label": "Student Category",
|
||||
"options": "Student Category"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 1,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "student_batch_name",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 1,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Student Batch",
|
||||
"length": 0,
|
||||
"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_on_submit": 1,
|
||||
"fieldname": "student_batch_name",
|
||||
"fieldtype": "Link",
|
||||
"in_global_search": 1,
|
||||
"label": "Student Batch",
|
||||
"options": "Student Batch Name"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 1,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "school_house",
|
||||
"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",
|
||||
"length": 0,
|
||||
"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_on_submit": 1,
|
||||
"fieldname": "school_house",
|
||||
"fieldtype": "Link",
|
||||
"label": "School House",
|
||||
"options": "School House"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "column_break_4",
|
||||
"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
|
||||
},
|
||||
"fieldname": "column_break_4",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "program",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Program",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Program",
|
||||
"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
|
||||
},
|
||||
"fieldname": "program",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Program",
|
||||
"options": "Program",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "academic_year",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Academic Year",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Academic Year",
|
||||
"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
|
||||
},
|
||||
"fieldname": "academic_year",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Academic Year",
|
||||
"options": "Academic Year",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "academic_term",
|
||||
"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",
|
||||
"length": 0,
|
||||
"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
|
||||
},
|
||||
"fieldname": "academic_term",
|
||||
"fieldtype": "Link",
|
||||
"label": "Academic Term",
|
||||
"options": "Academic Term"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "Today",
|
||||
"fieldname": "enrollment_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",
|
||||
"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": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"default": "Today",
|
||||
"fieldname": "enrollment_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Enrollment Date",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "0",
|
||||
"description": "Check this if the Student is residing at the Institute's Hostel.",
|
||||
"fieldname": "boarding_student",
|
||||
"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": "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
|
||||
},
|
||||
"default": "0",
|
||||
"description": "Check this if the Student is residing at the Institute's Hostel.",
|
||||
"fieldname": "boarding_student",
|
||||
"fieldtype": "Check",
|
||||
"label": "Boarding Student"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "vehicle_no",
|
||||
"columns": 0,
|
||||
"fieldname": "transportation",
|
||||
"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": "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
|
||||
},
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "vehicle_no",
|
||||
"fieldname": "transportation",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Transportation"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 1,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "mode_of_transportation",
|
||||
"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",
|
||||
"length": 0,
|
||||
"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_on_submit": 1,
|
||||
"fieldname": "mode_of_transportation",
|
||||
"fieldtype": "Select",
|
||||
"label": "Mode of Transportation",
|
||||
"options": "\nWalking\nInstitute's Bus\nPublic Transport\nSelf-Driving Vehicle\nPick/Drop by Guardian"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "column_break_13",
|
||||
"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
|
||||
},
|
||||
"fieldname": "column_break_13",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 1,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "vehicle_no",
|
||||
"fieldtype": "Data",
|
||||
"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": "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_on_submit": 1,
|
||||
"fieldname": "vehicle_no",
|
||||
"fieldtype": "Data",
|
||||
"label": "Vehicle/Bus Number"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 1,
|
||||
"columns": 0,
|
||||
"fieldname": "enrolled_courses",
|
||||
"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": "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
|
||||
},
|
||||
"fieldname": "enrolled_courses",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Enrolled courses"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 1,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "courses",
|
||||
"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",
|
||||
"length": 0,
|
||||
"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_on_submit": 1,
|
||||
"fieldname": "courses",
|
||||
"fieldtype": "Table",
|
||||
"label": "Courses",
|
||||
"options": "Program Enrollment Course"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 1,
|
||||
"columns": 0,
|
||||
"fieldname": "section_break_7",
|
||||
"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": "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
|
||||
},
|
||||
"collapsible": 1,
|
||||
"fieldname": "section_break_7",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Fees"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "fees",
|
||||
"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",
|
||||
"length": 0,
|
||||
"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": ""
|
||||
},
|
||||
"fieldname": "fees",
|
||||
"fieldtype": "Table",
|
||||
"label": "Fees",
|
||||
"options": "Program Fee"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "amended_from",
|
||||
"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",
|
||||
"length": 0,
|
||||
"no_copy": 1,
|
||||
"options": "Program Enrollment",
|
||||
"permlevel": 0,
|
||||
"print_hide": 1,
|
||||
"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
|
||||
},
|
||||
"fieldname": "amended_from",
|
||||
"fieldtype": "Link",
|
||||
"label": "Amended From",
|
||||
"no_copy": 1,
|
||||
"options": "Program Enrollment",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "image",
|
||||
"fieldtype": "Attach Image",
|
||||
"hidden": 1,
|
||||
"ignore_user_permissions": 0,
|
||||
"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
|
||||
"fieldname": "image",
|
||||
"fieldtype": "Attach Image",
|
||||
"hidden": 1,
|
||||
"label": "Image"
|
||||
}
|
||||
],
|
||||
"has_web_view": 0,
|
||||
"hide_heading": 0,
|
||||
"hide_toolbar": 0,
|
||||
"idx": 0,
|
||||
"image_field": "image",
|
||||
"image_view": 0,
|
||||
"in_create": 0,
|
||||
"is_submittable": 1,
|
||||
"issingle": 0,
|
||||
"istable": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": 0,
|
||||
"modified": "2018-11-07 21:13:06.502279",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Education",
|
||||
"name": "Program Enrollment",
|
||||
"name_case": "",
|
||||
"owner": "Administrator",
|
||||
],
|
||||
"image_field": "image",
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-09-15 18:12:11.988565",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Education",
|
||||
"name": "Program Enrollment",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"amend": 1,
|
||||
"cancel": 1,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"if_owner": 0,
|
||||
"import": 0,
|
||||
"permlevel": 0,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Academics User",
|
||||
"set_user_permissions": 0,
|
||||
"share": 1,
|
||||
"submit": 1,
|
||||
"amend": 1,
|
||||
"cancel": 1,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Academics User",
|
||||
"share": 1,
|
||||
"submit": 1,
|
||||
"write": 1
|
||||
},
|
||||
},
|
||||
{
|
||||
"amend": 0,
|
||||
"cancel": 0,
|
||||
"create": 1,
|
||||
"delete": 0,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"if_owner": 0,
|
||||
"import": 0,
|
||||
"permlevel": 0,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "LMS User",
|
||||
"set_user_permissions": 0,
|
||||
"share": 1,
|
||||
"submit": 1,
|
||||
"create": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "LMS User",
|
||||
"share": 1,
|
||||
"submit": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 0,
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"restrict_to_domain": "Education",
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"title_field": "student_name",
|
||||
"track_changes": 0,
|
||||
"track_seen": 0,
|
||||
"track_views": 0
|
||||
],
|
||||
"restrict_to_domain": "Education",
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"title_field": "student_name"
|
||||
}
|
@ -7,12 +7,15 @@ import frappe
|
||||
from frappe import msgprint, _
|
||||
from frappe.model.document import Document
|
||||
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
|
||||
|
||||
class ProgramEnrollment(Document):
|
||||
def validate(self):
|
||||
self.validate_duplication()
|
||||
self.validate_academic_year()
|
||||
if self.academic_term:
|
||||
self.validate_academic_term()
|
||||
if not self.student_name:
|
||||
self.student_name = frappe.db.get_value("Student", self.student, "title")
|
||||
if not self.courses:
|
||||
@ -23,11 +26,34 @@ class ProgramEnrollment(Document):
|
||||
self.make_fee_records()
|
||||
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):
|
||||
enrollment = frappe.get_all("Program Enrollment", filters={
|
||||
"student": self.student,
|
||||
"program": self.program,
|
||||
"academic_year": self.academic_year,
|
||||
"academic_term": self.academic_term,
|
||||
"docstatus": ("<", 2),
|
||||
"name": ("!=", self.name)
|
||||
})
|
||||
@ -70,10 +96,9 @@ class ProgramEnrollment(Document):
|
||||
|
||||
def create_course_enrollments(self):
|
||||
student = frappe.get_doc("Student", self.student)
|
||||
program = frappe.get_doc("Program", self.program)
|
||||
course_list = [course.course for course in program.courses]
|
||||
course_list = [course.course for course in self.courses]
|
||||
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):
|
||||
course_enrollment_names = frappe.get_list("Course Enrollment", filters={'program_enrollment': self.name})
|
||||
|
@ -23,4 +23,13 @@ class TestProgramEnrollment(unittest.TestCase):
|
||||
course_enrollments = student.get_all_course_enrollments()
|
||||
self.assertTrue("_Test Course 1" 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)
|
||||
except frappe.exceptions.ValidationError:
|
||||
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:
|
||||
return enrollment
|
||||
|
||||
|
@ -42,6 +42,16 @@ class TestStudent(unittest.TestCase):
|
||||
self.assertTrue("_Test Course 2" in course_enrollments.keys())
|
||||
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):
|
||||
student = get_student(student_dict['email'])
|
||||
if not student:
|
||||
|
@ -51,12 +51,14 @@
|
||||
"fieldname": "admission_start_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Admission Start Date",
|
||||
"mandatory_depends_on": "enable_admission_application",
|
||||
"no_copy": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "admission_end_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Admission End Date",
|
||||
"mandatory_depends_on": "enable_admission_application",
|
||||
"no_copy": 1
|
||||
},
|
||||
{
|
||||
@ -83,6 +85,7 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "published",
|
||||
"fieldname": "enable_admission_application",
|
||||
"fieldtype": "Check",
|
||||
"label": "Enable Admission Application"
|
||||
@ -91,7 +94,7 @@
|
||||
"has_web_view": 1,
|
||||
"is_published_field": "published",
|
||||
"links": [],
|
||||
"modified": "2020-06-15 20:18:38.591626",
|
||||
"modified": "2020-09-18 00:14:54.615321",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Education",
|
||||
"name": "Student Admission",
|
||||
|
@ -19,6 +19,9 @@ class StudentAdmission(WebsiteGenerator):
|
||||
if not self.route: #pylint: disable=E0203
|
||||
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):
|
||||
context.no_cache = 1
|
||||
context.show_sidebar = True
|
||||
|
@ -43,31 +43,35 @@
|
||||
<thead>
|
||||
<tr class="active">
|
||||
<th style="width: 90px">Program/Std.</th>
|
||||
<th style="width: 170px">Minumum Age</th>
|
||||
<th style="width: 170px">Maximum Age</th>
|
||||
<th style="width: 180px">Description</th>
|
||||
<th style="width: 100px">Minumum Age</th>
|
||||
<th style="width: 100px">Maximum Age</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>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for row in program_details %}
|
||||
<tr>
|
||||
<td>{{ row.program }}</td>
|
||||
<td><div class="text-muted">{{ row.description if row.description else '' }}</div></td>
|
||||
<td>{{ row.min_age }}</td>
|
||||
<td>{{ row.max_age }}</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>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% 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 %}
|
||||
|
@ -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()) %}
|
||||
<a href = "{{ doc.route }}/">
|
||||
<a href = "{{ doc.route }}/" class="no-underline">
|
||||
<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
|
||||
{% if frappe.utils.getdate(doc.admission_end_date) == today %}
|
||||
red
|
||||
@ -15,6 +15,14 @@
|
||||
{% endif %}
|
||||
">{{ doc.title }}</span>
|
||||
</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">
|
||||
<span class="text-muted">
|
||||
Starts on
|
||||
@ -27,7 +35,7 @@
|
||||
<span class="text-muted">
|
||||
Ends on
|
||||
</span>
|
||||
<div class="bold">
|
||||
<div class=" text-muted bold">
|
||||
{{ frappe.format_date(doc.admission_end_date) }}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -8,6 +8,7 @@
|
||||
"program",
|
||||
"min_age",
|
||||
"max_age",
|
||||
"description",
|
||||
"column_break_4",
|
||||
"application_fee",
|
||||
"applicant_naming_series"
|
||||
@ -18,52 +19,47 @@
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Program",
|
||||
"options": "Program",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"options": "Program"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_4",
|
||||
"fieldtype": "Column Break",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "application_fee",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Application Fee",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"label": "Application Fee"
|
||||
},
|
||||
{
|
||||
"fieldname": "applicant_naming_series",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Naming Series (for Student Applicant)",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"label": "Naming Series (for Student Applicant)"
|
||||
},
|
||||
{
|
||||
"fieldname": "min_age",
|
||||
"fieldtype": "Int",
|
||||
"in_list_view": 1,
|
||||
"label": "Minimum Age",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"label": "Minimum Age"
|
||||
},
|
||||
{
|
||||
"fieldname": "max_age",
|
||||
"fieldtype": "Int",
|
||||
"in_list_view": 1,
|
||||
"label": "Maximum Age",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"label": "Maximum Age"
|
||||
},
|
||||
{
|
||||
"fetch_from": "program.description",
|
||||
"fetch_if_empty": 1,
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Description"
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-06-10 23:06:30.037404",
|
||||
"modified": "2020-10-05 13:03:42.005985",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Education",
|
||||
"name": "Student Admission Program",
|
||||
|
@ -168,6 +168,7 @@
|
||||
"fieldname": "student_email_id",
|
||||
"fieldtype": "Data",
|
||||
"label": "Student Email Address",
|
||||
"options": "Email",
|
||||
"unique": 1
|
||||
},
|
||||
{
|
||||
@ -261,7 +262,7 @@
|
||||
"image_field": "image",
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-09-07 19:31:30.063563",
|
||||
"modified": "2020-10-05 13:59:45.631647",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Education",
|
||||
"name": "Student Applicant",
|
||||
|
@ -6,13 +6,13 @@ from __future__ import unicode_literals
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
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
|
||||
|
||||
|
||||
class StudentAttendance(Document):
|
||||
def validate(self):
|
||||
self.validate_mandatory()
|
||||
self.validate_date()
|
||||
self.set_date()
|
||||
self.set_student_group()
|
||||
self.validate_student()
|
||||
@ -27,6 +27,18 @@ class StudentAttendance(Document):
|
||||
frappe.throw(_('{0} or {1} is mandatory').format(frappe.bold('Student Group'),
|
||||
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):
|
||||
if self.course_schedule:
|
||||
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) {
|
||||
if (frm.doc.date > frappe.datetime.get_today())
|
||||
frappe.throw(__("Cannot mark attendance for future dates."));
|
||||
frm.trigger("student_group");
|
||||
},
|
||||
|
||||
@ -133,8 +135,8 @@ education.StudentsEditor = Class.extend({
|
||||
return !stud.disabled && !stud.checked;
|
||||
});
|
||||
|
||||
frappe.confirm(__("Do you want to update attendance?<br>Present: {0}\
|
||||
<br>Absent: {1}", [students_present.length, students_absent.length]),
|
||||
frappe.confirm(__("Do you want to update attendance? <br> Present: {0} <br> Absent: {1}",
|
||||
[students_present.length, students_absent.length]),
|
||||
function() { //ifyes
|
||||
if(!frappe.request.ajax_count) {
|
||||
frappe.call({
|
||||
|
@ -1,333 +1,118 @@
|
||||
{
|
||||
"allow_copy": 1,
|
||||
"allow_guest_to_view": 0,
|
||||
"allow_import": 0,
|
||||
"allow_rename": 0,
|
||||
"beta": 0,
|
||||
"creation": "2016-11-16 17:12:46.437539",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"actions": [],
|
||||
"allow_copy": 1,
|
||||
"creation": "2016-11-16 17:12:46.437539",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"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": [
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "",
|
||||
"fieldname": "based_on",
|
||||
"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",
|
||||
"length": 0,
|
||||
"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
|
||||
},
|
||||
"fieldname": "based_on",
|
||||
"fieldtype": "Select",
|
||||
"label": "Based On",
|
||||
"options": "Student Group\nCourse Schedule"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "Batch",
|
||||
"depends_on": "eval:doc.based_on == \"Student Group\"",
|
||||
"fieldname": "group_based_on",
|
||||
"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",
|
||||
"length": 0,
|
||||
"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
|
||||
},
|
||||
"default": "Batch",
|
||||
"depends_on": "eval:doc.based_on == \"Student Group\"",
|
||||
"fieldname": "group_based_on",
|
||||
"fieldtype": "Select",
|
||||
"label": "Group Based On",
|
||||
"options": "Batch\nCourse\nActivity"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "column_break_2",
|
||||
"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
|
||||
},
|
||||
"fieldname": "column_break_2",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"depends_on": "eval:doc.based_on ==\"Student Group\"",
|
||||
"fieldname": "student_group",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Student Group",
|
||||
"length": 0,
|
||||
"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": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"depends_on": "eval:doc.based_on ==\"Student Group\"",
|
||||
"fieldname": "student_group",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Student Group",
|
||||
"options": "Student Group",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"depends_on": "eval:doc.based_on ==\"Course Schedule\"",
|
||||
"fieldname": "course_schedule",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Course Schedule",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Course 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": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"depends_on": "eval:doc.based_on ==\"Course Schedule\"",
|
||||
"fieldname": "course_schedule",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Course Schedule",
|
||||
"options": "Course Schedule",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"depends_on": "eval:doc.based_on ==\"Student Group\"",
|
||||
"fieldname": "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_standard_filter": 0,
|
||||
"label": "Date",
|
||||
"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": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"depends_on": "eval:doc.based_on ==\"Student Group\"",
|
||||
"fieldname": "date",
|
||||
"fieldtype": "Date",
|
||||
"in_list_view": 1,
|
||||
"label": "Date",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"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))",
|
||||
"fieldname": "attendance",
|
||||
"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": "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
|
||||
},
|
||||
"depends_on": "eval: (doc.course_schedule \n|| (doc.student_group && doc.date))",
|
||||
"fieldname": "attendance",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Attendance"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "students_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": "Students HTML",
|
||||
"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
|
||||
"fieldname": "students_html",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Students HTML"
|
||||
},
|
||||
{
|
||||
"fetch_from": "student_group.academic_year",
|
||||
"fieldname": "academic_year",
|
||||
"fieldtype": "Link",
|
||||
"label": "Academic Year",
|
||||
"options": "Academic Year",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "student_group.academic_term",
|
||||
"fieldname": "academic_term",
|
||||
"fieldtype": "Link",
|
||||
"label": "Academic Term",
|
||||
"options": "Academic Term",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"has_web_view": 0,
|
||||
"hide_heading": 1,
|
||||
"hide_toolbar": 1,
|
||||
"idx": 0,
|
||||
"image_view": 0,
|
||||
"in_create": 0,
|
||||
"is_submittable": 0,
|
||||
"issingle": 1,
|
||||
"istable": 0,
|
||||
"max_attachments": 0,
|
||||
"modified": "2017-11-10 18:55:36.168044",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Education",
|
||||
"name": "Student Attendance Tool",
|
||||
"name_case": "",
|
||||
"owner": "Administrator",
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2020-10-23 17:52:28.078971",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Education",
|
||||
"name": "Student Attendance Tool",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"amend": 0,
|
||||
"apply_user_permissions": 0,
|
||||
"cancel": 0,
|
||||
"create": 1,
|
||||
"delete": 0,
|
||||
"email": 0,
|
||||
"export": 0,
|
||||
"if_owner": 0,
|
||||
"import": 0,
|
||||
"permlevel": 0,
|
||||
"print": 0,
|
||||
"read": 1,
|
||||
"report": 0,
|
||||
"role": "Instructor",
|
||||
"set_user_permissions": 0,
|
||||
"share": 0,
|
||||
"submit": 0,
|
||||
"create": 1,
|
||||
"read": 1,
|
||||
"role": "Instructor",
|
||||
"write": 1
|
||||
},
|
||||
},
|
||||
{
|
||||
"amend": 0,
|
||||
"apply_user_permissions": 0,
|
||||
"cancel": 0,
|
||||
"create": 1,
|
||||
"delete": 0,
|
||||
"email": 0,
|
||||
"export": 0,
|
||||
"if_owner": 0,
|
||||
"import": 0,
|
||||
"permlevel": 0,
|
||||
"print": 0,
|
||||
"read": 1,
|
||||
"report": 0,
|
||||
"role": "Academics User",
|
||||
"set_user_permissions": 0,
|
||||
"share": 0,
|
||||
"submit": 0,
|
||||
"create": 1,
|
||||
"read": 1,
|
||||
"role": "Academics User",
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 0,
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"restrict_to_domain": "Education",
|
||||
"show_name_in_global_search": 0,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 0,
|
||||
"track_seen": 0
|
||||
],
|
||||
"restrict_to_domain": "Education",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC"
|
||||
}
|
@ -70,19 +70,7 @@
|
||||
"show_in_filter": 0
|
||||
},
|
||||
{
|
||||
"allow_read_on_all_link_options": 0,
|
||||
"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,
|
||||
"allow_read_on_all_link_options": 1,
|
||||
"fieldname": "program",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
@ -95,7 +83,7 @@
|
||||
"show_in_filter": 0
|
||||
},
|
||||
{
|
||||
"allow_read_on_all_link_options": 0,
|
||||
"allow_read_on_all_link_options": 1,
|
||||
"fieldname": "academic_year",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
@ -107,6 +95,19 @@
|
||||
"reqd": 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,
|
||||
"fieldname": "date_of_birth",
|
||||
@ -119,6 +120,19 @@
|
||||
"reqd": 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,
|
||||
"fieldname": "blood_group",
|
||||
@ -141,7 +155,7 @@
|
||||
"max_length": 0,
|
||||
"max_value": 0,
|
||||
"read_only": 0,
|
||||
"reqd": 0,
|
||||
"reqd": 1,
|
||||
"show_in_filter": 0
|
||||
},
|
||||
{
|
||||
@ -206,19 +220,6 @@
|
||||
"reqd": 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,
|
||||
"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()
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user