Merge branch 'develop' of https://github.com/frappe/erpnext into mpesa-integration

This commit is contained in:
Mangesh-Khairnar 2020-10-19 18:29:46 +05:30
commit 5c29eb08c4
252 changed files with 108284 additions and 19990 deletions

48
.github/helper/documentation.py vendored Normal file
View 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
View 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
View 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

View 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

View File

@ -5,6 +5,7 @@
"_comments": null,
"_liked_by": null,
"_user_tags": null,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@ -17,17 +18,24 @@
"docstatus": 0,
"dt": "Address",
"fetch_from": null,
"fetch_if_empty": 0,
"fieldname": "tax_category",
"fieldtype": "Link",
"hidden": 0,
"idx": 14,
"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",
@ -43,6 +51,66 @@
"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,

View 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)

View File

@ -53,7 +53,7 @@
{
"hidden": 0,
"label": "Goods and Services Tax (GST India)",
"links": "[\n {\n \"label\": \"GST Settings\",\n \"name\": \"GST Settings\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"GST HSN Code\",\n \"name\": \"GST HSN Code\",\n \"type\": \"doctype\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"GSTR-1\",\n \"name\": \"GSTR-1\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"GSTR-2\",\n \"name\": \"GSTR-2\",\n \"type\": \"report\"\n },\n {\n \"label\": \"GSTR 3B Report\",\n \"name\": \"GSTR 3B Report\",\n \"type\": \"doctype\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"GST Sales Register\",\n \"name\": \"GST Sales Register\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"GST Purchase Register\",\n \"name\": \"GST Purchase Register\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"GST Itemised Sales Register\",\n \"name\": \"GST Itemised Sales Register\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"GST Itemised Purchase Register\",\n \"name\": \"GST Itemised Purchase Register\",\n \"type\": \"report\"\n },\n {\n \"country\": \"India\",\n \"description\": \"C-Form records\",\n \"label\": \"C-Form\",\n \"name\": \"C-Form\",\n \"type\": \"doctype\"\n }\n]"
"links": "[\n {\n \"label\": \"GST Settings\",\n \"name\": \"GST Settings\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"GST HSN Code\",\n \"name\": \"GST HSN Code\",\n \"type\": \"doctype\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"GSTR-1\",\n \"name\": \"GSTR-1\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"GSTR-2\",\n \"name\": \"GSTR-2\",\n \"type\": \"report\"\n },\n {\n \"label\": \"GSTR 3B Report\",\n \"name\": \"GSTR 3B Report\",\n \"type\": \"doctype\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"GST Sales Register\",\n \"name\": \"GST Sales Register\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"GST Purchase Register\",\n \"name\": \"GST Purchase Register\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"GST Itemised Sales Register\",\n \"name\": \"GST Itemised Sales Register\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"GST Itemised Purchase Register\",\n \"name\": \"GST Itemised Purchase Register\",\n \"type\": \"report\"\n },\n {\n \"country\": \"India\",\n \"description\": \"C-Form records\",\n \"label\": \"C-Form\",\n \"name\": \"C-Form\",\n \"type\": \"doctype\"\n },\n {\n \"country\": \"India\",\n \"label\": \"Lower Deduction Certificate\",\n \"name\": \"Lower Deduction Certificate\",\n \"type\": \"doctype\"\n }\n]"
},
{
"hidden": 0,
@ -98,7 +98,7 @@
"idx": 0,
"is_standard": 1,
"label": "Accounting",
"modified": "2020-09-09 11:45:33.766400",
"modified": "2020-10-08 20:31:46.022470",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounting",
@ -108,7 +108,7 @@
"pin_to_top": 0,
"shortcuts": [
{
"label": "Chart Of Accounts",
"label": "Chart of Accounts",
"link_to": "Account",
"type": "DocType"
},

View File

@ -117,7 +117,9 @@ class Account(NestedSet):
for d in frappe.db.get_values('Account', filters=filters, fieldname=["company", "name"], as_dict=True):
parent_acc_name_map[d["company"]] = d["name"]
if not parent_acc_name_map: return
self.create_account_for_child_company(parent_acc_name_map, descendants, parent_acc_name)
def validate_group_or_ledger(self):
@ -289,10 +291,30 @@ def validate_account_number(name, account_number, company):
.format(account_number, account_with_same_number))
@frappe.whitelist()
def update_account_number(name, account_name, account_number=None):
def update_account_number(name, account_name, account_number=None, from_descendant=False):
account = frappe.db.get_value("Account", name, "company", as_dict=True)
if not account: return
old_acc_name, old_acc_number = frappe.db.get_value('Account', name, \
["account_name", "account_number"])
# check if account exists in parent company
ancestors = get_ancestors_of("Company", account.company)
allow_independent_account_creation = frappe.get_value("Company", account.company, "allow_account_creation_against_child_company")
if ancestors and not allow_independent_account_creation:
for ancestor in ancestors:
if frappe.db.get_value("Account", {'account_name': old_acc_name, 'company': ancestor}, 'name'):
# same account in parent company exists
allow_child_account_creation = _("Allow Account Creation Against Child Company")
message = _("Account {0} exists in parent company {1}.").format(frappe.bold(old_acc_name), frappe.bold(ancestor))
message += "<br>" + _("Renaming it is only allowed via parent company {0}, \
to avoid mismatch.").format(frappe.bold(ancestor)) + "<br><br>"
message += _("To overrule this, enable '{0}' in company {1}").format(allow_child_account_creation, frappe.bold(account.company))
frappe.throw(message, title=_("Rename Not Allowed"))
validate_account_number(name, account_number, account.company)
if account_number:
frappe.db.set_value("Account", name, "account_number", account_number.strip())
@ -300,6 +322,12 @@ def update_account_number(name, account_name, account_number=None):
frappe.db.set_value("Account", name, "account_number", "")
frappe.db.set_value("Account", name, "account_name", account_name.strip())
if not from_descendant:
# Update and rename in child company accounts as well
descendants = get_descendants_of('Company', account.company)
if descendants:
sync_update_account_number_in_child(descendants, old_acc_name, account_name, account_number, old_acc_number)
new_name = get_account_autoname(account_number, account_name, account.company)
if name != new_name:
frappe.rename_doc("Account", name, new_name, force=1)
@ -330,3 +358,14 @@ def get_root_company(company):
# return the topmost company in the hierarchy
ancestors = get_ancestors_of('Company', company, "lft asc")
return [ancestors[0]] if ancestors else []
def sync_update_account_number_in_child(descendants, old_acc_name, account_name, account_number=None, old_acc_number=None):
filters = {
"company": ["in", descendants],
"account_name": old_acc_name,
}
if old_acc_number:
filters["account_number"] = old_acc_number
for d in frappe.db.get_values('Account', filters=filters, fieldname=["company", "name"], as_dict=True):
update_account_number(d["name"], account_name, account_number, from_descendant=True)

View File

@ -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'));

View File

@ -5,8 +5,7 @@ from __future__ import unicode_literals
import unittest
import frappe
from erpnext.stock import get_warehouse_account, get_company_default_inventory_account
from erpnext.accounts.doctype.account.account import update_account_number
from erpnext.accounts.doctype.account.account import merge_account
from erpnext.accounts.doctype.account.account import update_account_number, merge_account
class TestAccount(unittest.TestCase):
def test_rename_account(self):
@ -99,7 +98,8 @@ class TestAccount(unittest.TestCase):
"Softwares - _TC", doc.is_group, doc.root_type, doc.company)
def test_account_sync(self):
del frappe.local.flags["ignore_root_company_validation"]
frappe.local.flags.pop("ignore_root_company_validation", None)
acc = frappe.new_doc("Account")
acc.account_name = "Test Sync Account"
acc.parent_account = "Temporary Accounts - _TC3"
@ -111,6 +111,55 @@ class TestAccount(unittest.TestCase):
self.assertEqual(acc_tc_4, "Test Sync Account - _TC4")
self.assertEqual(acc_tc_5, "Test Sync Account - _TC5")
def test_account_rename_sync(self):
frappe.local.flags.pop("ignore_root_company_validation", None)
acc = frappe.new_doc("Account")
acc.account_name = "Test Rename Account"
acc.parent_account = "Temporary Accounts - _TC3"
acc.company = "_Test Company 3"
acc.insert()
# Rename account in parent company
update_account_number(acc.name, "Test Rename Sync Account", "1234")
# Check if renamed in children
self.assertTrue(frappe.db.exists("Account", {'account_name': "Test Rename Sync Account", "company": "_Test Company 4", "account_number": "1234"}))
self.assertTrue(frappe.db.exists("Account", {'account_name': "Test Rename Sync Account", "company": "_Test Company 5", "account_number": "1234"}))
frappe.delete_doc("Account", "1234 - Test Rename Sync Account - _TC3")
frappe.delete_doc("Account", "1234 - Test Rename Sync Account - _TC4")
frappe.delete_doc("Account", "1234 - Test Rename Sync Account - _TC5")
def test_child_company_account_rename_sync(self):
frappe.local.flags.pop("ignore_root_company_validation", None)
acc = frappe.new_doc("Account")
acc.account_name = "Test Group Account"
acc.parent_account = "Temporary Accounts - _TC3"
acc.is_group = 1
acc.company = "_Test Company 3"
acc.insert()
self.assertTrue(frappe.db.exists("Account", {'account_name': "Test Group Account", "company": "_Test Company 4"}))
self.assertTrue(frappe.db.exists("Account", {'account_name': "Test Group Account", "company": "_Test Company 5"}))
# Try renaming child company account
acc_tc_5 = frappe.db.get_value('Account', {'account_name': "Test Group Account", "company": "_Test Company 5"})
self.assertRaises(frappe.ValidationError, update_account_number, acc_tc_5, "Test Modified Account")
# Rename child company account with allow_account_creation_against_child_company enabled
frappe.db.set_value("Company", "_Test Company 5", "allow_account_creation_against_child_company", 1)
update_account_number(acc_tc_5, "Test Modified Account")
self.assertTrue(frappe.db.exists("Account", {'name': "Test Modified Account - _TC5", "company": "_Test Company 5"}))
frappe.db.set_value("Company", "_Test Company 5", "allow_account_creation_against_child_company", 0)
to_delete = ["Test Group Account - _TC3", "Test Group Account - _TC4", "Test Modified Account - _TC5"]
for doc in to_delete:
frappe.delete_doc("Account", doc)
def _make_test_records(verbose):
from frappe.test_runner import make_test_objects

View File

@ -104,7 +104,7 @@
"default": "1",
"fieldname": "unlink_advance_payment_on_cancelation_of_order",
"fieldtype": "Check",
"label": "Unlink Advance Payment on Cancelation of Order"
"label": "Unlink Advance Payment on Cancellation of Order"
},
{
"default": "1",
@ -223,9 +223,10 @@
],
"icon": "icon-cog",
"idx": 1,
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2020-08-03 20:13:26.043092",
"modified": "2020-10-07 14:58:50.325577",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Settings",

View File

@ -1,785 +1,204 @@
{
"allow_copy": 0,
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"actions": [],
"allow_import": 1,
"allow_rename": 0,
"beta": 0,
"creation": "2016-05-16 11:42:29.632528",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"budget_against",
"company",
"cost_center",
"project",
"fiscal_year",
"column_break_3",
"monthly_distribution",
"amended_from",
"section_break_6",
"applicable_on_material_request",
"action_if_annual_budget_exceeded_on_mr",
"action_if_accumulated_monthly_budget_exceeded_on_mr",
"column_break_13",
"applicable_on_purchase_order",
"action_if_annual_budget_exceeded_on_po",
"action_if_accumulated_monthly_budget_exceeded_on_po",
"section_break_16",
"applicable_on_booking_actual_expenses",
"action_if_annual_budget_exceeded",
"action_if_accumulated_monthly_budget_exceeded",
"section_break_21",
"accounts"
],
"fields": [
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "Cost Center",
"fetch_if_empty": 0,
"fieldname": "budget_against",
"fieldtype": "Select",
"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": "Budget Against",
"length": 0,
"no_copy": 0,
"options": "\nCost Center\nProject",
"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
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "company",
"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": "Company",
"length": 0,
"no_copy": 0,
"options": "Company",
"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
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "eval:doc.budget_against == 'Cost Center'",
"fetch_if_empty": 0,
"fieldname": "cost_center",
"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": 1,
"label": "Cost Center",
"length": 0,
"no_copy": 0,
"options": "Cost Center",
"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
"options": "Cost Center"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "eval:doc.budget_against == 'Project'",
"fetch_if_empty": 0,
"fieldname": "project",
"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": 1,
"label": "Project",
"length": 0,
"no_copy": 0,
"options": "Project",
"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
"options": "Project"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "fiscal_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": "Fiscal Year",
"length": 0,
"no_copy": 0,
"options": "Fiscal 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
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 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
"fieldtype": "Column Break"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "eval:in_list([\"Stop\", \"Warn\"], doc.action_if_accumulated_monthly_budget_exceeded_on_po || doc.action_if_accumulated_monthly_budget_exceeded_on_mr || doc.action_if_accumulated_monthly_budget_exceeded_on_actual)",
"fetch_if_empty": 0,
"fieldname": "monthly_distribution",
"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": "Monthly Distribution",
"length": 0,
"no_copy": 0,
"options": "Monthly Distribution",
"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
"options": "Monthly Distribution"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 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": "Budget",
"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
"read_only": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "section_break_6",
"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": "Control Action",
"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
"label": "Control Action"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"default": "0",
"fieldname": "applicable_on_material_request",
"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": "Applicable on Material Request",
"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
"label": "Applicable on Material Request"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 1,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "Stop",
"depends_on": "eval:doc.applicable_on_material_request == 1",
"fetch_if_empty": 0,
"fieldname": "action_if_annual_budget_exceeded_on_mr",
"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": "Action if Annual Budget Exceeded on MR",
"length": 0,
"no_copy": 0,
"options": "\nStop\nWarn\nIgnore",
"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
"options": "\nStop\nWarn\nIgnore"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 1,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "Warn",
"depends_on": "eval:doc.applicable_on_material_request == 1",
"fetch_if_empty": 0,
"fieldname": "action_if_accumulated_monthly_budget_exceeded_on_mr",
"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": "Action if Accumulated Monthly Budget Exceeded on MR",
"length": 0,
"no_copy": 0,
"options": "\nStop\nWarn\nIgnore",
"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
"options": "\nStop\nWarn\nIgnore"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 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
"fieldtype": "Column Break"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"default": "0",
"fieldname": "applicable_on_purchase_order",
"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": "Applicable on Purchase Order",
"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
"label": "Applicable on Purchase Order"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 1,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "Stop",
"depends_on": "eval:doc.applicable_on_purchase_order == 1",
"fetch_if_empty": 0,
"fieldname": "action_if_annual_budget_exceeded_on_po",
"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": "Action if Annual Budget Exceeded on PO",
"length": 0,
"no_copy": 0,
"options": "\nStop\nWarn\nIgnore",
"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
"options": "\nStop\nWarn\nIgnore"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 1,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "Warn",
"depends_on": "eval:doc.applicable_on_purchase_order == 1",
"fetch_if_empty": 0,
"fieldname": "action_if_accumulated_monthly_budget_exceeded_on_po",
"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": "Action if Accumulated Monthly Budget Exceeded on PO",
"length": 0,
"no_copy": 0,
"options": "\nStop\nWarn\nIgnore",
"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
"options": "\nStop\nWarn\nIgnore"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "section_break_16",
"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
"fieldtype": "Section Break"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"default": "0",
"fieldname": "applicable_on_booking_actual_expenses",
"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": "Applicable on booking actual expenses",
"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
"label": "Applicable on booking actual expenses"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 1,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "Stop",
"depends_on": "eval:doc.applicable_on_booking_actual_expenses == 1",
"fetch_if_empty": 0,
"fieldname": "action_if_annual_budget_exceeded",
"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": "Action if Annual Budget Exceeded on Actual",
"length": 0,
"no_copy": 0,
"options": "\nStop\nWarn\nIgnore",
"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
"options": "\nStop\nWarn\nIgnore"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 1,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "Warn",
"depends_on": "eval:doc.applicable_on_booking_actual_expenses == 1",
"fetch_if_empty": 0,
"fieldname": "action_if_accumulated_monthly_budget_exceeded",
"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": "Action if Accumulated Monthly Budget Exceeded on Actual",
"length": 0,
"no_copy": 0,
"options": "\nStop\nWarn\nIgnore",
"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
"options": "\nStop\nWarn\nIgnore"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "section_break_21",
"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
"fieldtype": "Section Break"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "",
"fetch_if_empty": 0,
"fieldname": "accounts",
"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": "Budget Accounts",
"length": 0,
"no_copy": 0,
"options": "Budget 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
"reqd": 1
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"index_web_pages_for_search": 1,
"is_submittable": 1,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2019-03-22 12:06:02.323099",
"links": [],
"modified": "2020-10-06 15:13:54.055854",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Budget",
"name_case": "",
"owner": "Administrator",
"permissions": [
{
@ -789,26 +208,17 @@
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 1,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 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": 1,
"track_seen": 0,
"track_views": 0
"track_changes": 1
}

View File

@ -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)

View File

@ -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());
}
},

View File

@ -22,8 +22,12 @@ class JournalEntry(AccountsController):
return self.voucher_type
def validate(self):
if self.voucher_type == 'Opening Entry':
self.is_opening = 'Yes'
if not self.is_opening:
self.is_opening='No'
self.clearance_date = None
self.validate_party()

View File

@ -195,9 +195,7 @@ def create_sales_invoice_record(qty=1):
def create_records():
# create a new loyalty Account
if frappe.db.exists("Account", "Loyalty - _TC"):
return
if not frappe.db.exists("Account", "Loyalty - _TC"):
frappe.get_doc({
"doctype": "Account",
"account_name": "Loyalty",
@ -208,6 +206,7 @@ def create_records():
}).insert()
# create a new loyalty program Single tier
if not frappe.db.exists("Loyalty Program","Test Single Loyalty"):
frappe.get_doc({
"doctype": "Loyalty Program",
"loyalty_program_name": "Test Single Loyalty",
@ -227,6 +226,7 @@ def create_records():
}).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",
@ -236,6 +236,7 @@ def create_records():
}).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",
@ -262,7 +263,8 @@ def create_records():
}).insert()
# create an item
item = frappe.get_doc({
if not frappe.db.exists("Item", "Loyal Item"):
frappe.get_doc({
"doctype": "Item",
"item_code": "Loyal Item",
"item_name": "Loyal Item",
@ -274,9 +276,10 @@ def create_records():
}).insert()
# create item price
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": item.item_code,
"item_code": "Loyal Item",
"price_list_rate": 10000
}).insert()

View File

@ -1,6 +1,7 @@
frappe.listview_settings['Payment Entry'] = {
onload: function(listview) {
if (listview.page.fields_dict.party_type) {
listview.page.fields_dict.party_type.get_query = function() {
return {
"filters": {
@ -9,4 +10,5 @@ frappe.listview_settings['Payment Entry'] = {
};
};
}
}
};

View File

@ -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{
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{
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);
}
}

View File

@ -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

View File

@ -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",

View File

@ -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

View File

@ -106,21 +106,19 @@ class POSInvoice(SalesInvoice):
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"))
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"))
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"))
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"))
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"))
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"))
def validate_serialised_or_batched_item(self):
for d in self.get("items"):
@ -131,14 +129,14 @@ class POSInvoice(SalesInvoice):
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"))
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"))
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"))
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"))
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"))
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"))
def validate_return_items(self):
if not self.get("is_return"): return
@ -153,7 +151,8 @@ class POSInvoice(SalesInvoice):
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):
@ -164,7 +163,7 @@ 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):
for entry in self.payments:
@ -180,7 +179,7 @@ class POSInvoice(SalesInvoice):
total_amount_in_payments += payment.amount
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)))
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):

View File

@ -7,6 +7,8 @@ import frappe
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):
@ -221,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,6 +288,119 @@ 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
frappe.db.sql("delete from `tabPOS Invoice`")
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': 270
})
pos_inv.submit()
pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1)
pos_inv2.append('payments', {
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 3200
})
pos_inv2.submit()
merge_pos_invoices()
pos_inv.load_from_db()
rounded_total = frappe.db.get_value("Sales Invoice", pos_inv.consolidated_invoice, "rounded_total")
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
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)
pos_inv.append('payments', {
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 300
})
pos_inv.append('taxes', {
"charge_type": "On Net Total",
"account_head": "_Test Account Service Tax - _TC",
"cost_center": "_Test Cost Center - _TC",
"description": "Service Tax",
"rate": 14,
'included_in_print_rate': 1
})
pos_inv.submit()
pos_inv2 = create_pos_invoice(rate=300, qty=2, do_not_submit=1)
pos_inv2.additional_discount_percentage = 10
pos_inv2.append('payments', {
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 540
})
pos_inv2.append('taxes', {
"charge_type": "On Net Total",
"account_head": "_Test Account Service Tax - _TC",
"cost_center": "_Test Cost Center - _TC",
"description": "Service Tax",
"rate": 14,
'included_in_print_rate': 1
})
pos_inv2.submit()
merge_pos_invoices()
pos_inv.load_from_db()
rounded_total = frappe.db.get_value("Sales Invoice", pos_inv.consolidated_invoice, "rounded_total")
self.assertEqual(rounded_total, 840)
frappe.set_user("Administrator")
def test_merging_with_validate_selling_price(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
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_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)
pos_inv.append('payments', {
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 300
})
pos_inv.append('taxes', {
"charge_type": "On Net Total",
"account_head": "_Test Account Service Tax - _TC",
"cost_center": "_Test Cost Center - _TC",
"description": "Service Tax",
"rate": 14,
'included_in_print_rate': 1
})
self.assertRaises(frappe.ValidationError, pos_inv.submit)
pos_inv2 = create_pos_invoice(rate=400, do_not_submit=1)
pos_inv2.append('payments', {
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 400
})
pos_inv2.append('taxes', {
"charge_type": "On Net Total",
"account_head": "_Test Account Service Tax - _TC",
"cost_center": "_Test Cost Center - _TC",
"description": "Service Tax",
"rate": 14,
'included_in_print_rate': 1
})
pos_inv2.submit()
merge_pos_invoices()
pos_inv2.load_from_db()
rounded_total = frappe.db.get_value("Sales Invoice", pos_inv2.consolidated_invoice, "rounded_total")
self.assertEqual(rounded_total, 400)
frappe.set_user("Administrator")
frappe.db.set_value("Selling Settings", "Selling Settings", "validate_selling_price", 0)
def create_pos_invoice(**args):
args = frappe._dict(args)
pos_profile = None
@ -294,12 +409,11 @@ def create_pos_invoice(**args):
pos_profile.save()
pos_inv = frappe.new_doc("POS Invoice")
pos_inv.update(args)
pos_inv.update_stock = 1
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()
@ -313,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",

View File

@ -96,17 +96,28 @@ class POSInvoiceMergeLog(Document):
loyalty_amount_sum += doc.loyalty_amount
for item in doc.get('items'):
found = False
for i in items:
if (i.item_code == item.item_code and not i.serial_no and not i.batch_no and
i.uom == item.uom and i.net_rate == item.net_rate):
found = True
i.qty = i.qty + item.qty
if not found:
item.rate = item.net_rate
items.append(item)
for tax in doc.get('taxes'):
found = False
for t in taxes:
if t.account_head == tax.account_head and t.cost_center == tax.cost_center and t.rate == tax.rate:
t.tax_amount = flt(t.tax_amount) + flt(tax.tax_amount)
t.base_tax_amount = flt(t.base_tax_amount) + flt(tax.base_tax_amount)
if t.account_head == tax.account_head and t.cost_center == tax.cost_center:
t.tax_amount = flt(t.tax_amount) + flt(tax.tax_amount_after_discount_amount)
t.base_tax_amount = flt(t.base_tax_amount) + flt(tax.base_tax_amount_after_discount_amount)
found = True
if not found:
tax.charge_type = 'Actual'
tax.included_in_print_rate = 0
tax.tax_amount = tax.tax_amount_after_discount_amount
tax.base_tax_amount = tax.base_tax_amount_after_discount_amount
taxes.append(tax)
for payment in doc.get('payments'):
@ -127,6 +138,8 @@ class POSInvoiceMergeLog(Document):
invoice.set('items', items)
invoice.set('payments', payments)
invoice.set('taxes', taxes)
invoice.additional_discount_percentage = 0
invoice.discount_amount = 0.0
return invoice

View File

@ -15,15 +15,6 @@ frappe.ui.form.on("POS Profile", "onload", function(frm) {
erpnext.queries.setup_queries(frm, "Warehouse", function() {
return erpnext.queries.warehouse(frm.doc);
});
frm.call({
method: "erpnext.accounts.doctype.pos_profile.pos_profile.get_series",
callback: function(r) {
if(!r.exc) {
set_field_options("naming_series", r.message);
}
}
});
});
frappe.ui.form.on('POS Profile', {

View File

@ -8,7 +8,6 @@
"field_order": [
"disabled",
"section_break_2",
"naming_series",
"customer",
"company",
"country",
@ -59,17 +58,6 @@
"fieldname": "section_break_2",
"fieldtype": "Section Break"
},
{
"fieldname": "naming_series",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Series",
"no_copy": 1,
"oldfieldname": "naming_series",
"oldfieldtype": "Select",
"options": "[Select]",
"reqd": 1
},
{
"fieldname": "customer",
"fieldtype": "Link",
@ -323,7 +311,7 @@
"icon": "icon-cog",
"idx": 1,
"links": [],
"modified": "2020-06-29 12:20:30.977272",
"modified": "2020-10-01 17:29:27.759088",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Profile",

View File

@ -109,10 +109,6 @@ def get_child_nodes(group_type, root):
return frappe.db.sql(""" Select name, lft, rgt from `tab{tab}` where
lft >= {lft} and rgt <= {rgt} order by lft""".format(tab=group_type, lft=lft, rgt=rgt), as_dict=1)
@frappe.whitelist()
def get_series():
return frappe.get_meta("POS Invoice").get_field("naming_series").options or "s"
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def pos_profile_query(doctype, txt, searchfield, start, page_len, filters):

View File

@ -1,4 +1,5 @@
{
"actions": [],
"allow_import": 1,
"allow_rename": 1,
"autoname": "field:title",
@ -71,6 +72,7 @@
"section_break_13",
"threshold_percentage",
"priority",
"condition",
"column_break_66",
"apply_multiple_pricing_rules",
"apply_discount_on_rate",
@ -550,11 +552,18 @@
"fieldtype": "Link",
"label": "Promotional Scheme",
"options": "Promotional Scheme"
},
{
"description": "Simple Python Expression, Example: territory != 'All Territories'",
"fieldname": "condition",
"fieldtype": "Code",
"label": "Condition"
}
],
"icon": "fa fa-gift",
"idx": 1,
"modified": "2019-12-18 17:29:22.957077",
"links": [],
"modified": "2020-08-26 12:24:44.740734",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Pricing Rule",

View File

@ -6,9 +6,10 @@ from __future__ import unicode_literals
import frappe
import json
import copy
import re
from frappe import throw, _
from frappe.utils import flt, cint, getdate
from frappe.model.document import Document
from six import string_types
@ -30,6 +31,7 @@ class PricingRule(Document):
self.validate_max_discount()
self.validate_price_list_with_currency()
self.validate_dates()
self.validate_condition()
if not self.margin_type: self.margin_rate_or_amount = 0.0
@ -140,6 +142,10 @@ class PricingRule(Document):
if self.valid_from and self.valid_upto and getdate(self.valid_from) > getdate(self.valid_upto):
frappe.throw(_("Valid from date must be less than valid upto date"))
def validate_condition(self):
if self.condition and ("=" in self.condition) and re.match("""[\w\.:_]+\s*={1}\s*[\w\.@'"]+""", self.condition):
frappe.throw(_("Invalid condition expression"))
#--------------------------------------------------------------------------------
@frappe.whitelist()

View File

@ -430,6 +430,33 @@ class TestPricingRule(unittest.TestCase):
self.assertTrue(details)
def test_pricing_rule_for_condition(self):
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule")
make_pricing_rule(selling=1, margin_type="Percentage", \
condition="customer=='_Test Customer 1' and is_return==0", discount_percentage=10)
# Incorrect Customer and Correct is_return value
si = create_sales_invoice(do_not_submit=True, customer="_Test Customer 2", is_return=0)
si.items[0].price_list_rate = 1000
si.submit()
item = si.items[0]
self.assertEquals(item.rate, 100)
# Correct Customer and Incorrect is_return value
si = create_sales_invoice(do_not_submit=True, customer="_Test Customer 1", is_return=1, qty=-1)
si.items[0].price_list_rate = 1000
si.submit()
item = si.items[0]
self.assertEquals(item.rate, 100)
# Correct Customer and correct is_return value
si = create_sales_invoice(do_not_submit=True, customer="_Test Customer 1", is_return=0)
si.items[0].price_list_rate = 1000
si.submit()
item = si.items[0]
self.assertEquals(item.rate, 900)
def make_pricing_rule(**args):
args = frappe._dict(args)
@ -448,7 +475,8 @@ def make_pricing_rule(**args):
"discount_percentage": args.discount_percentage or 0.0,
"rate": args.rate or 0.0,
"margin_type": args.margin_type,
"margin_rate_or_amount": args.margin_rate_or_amount or 0.0
"margin_rate_or_amount": args.margin_rate_or_amount or 0.0,
"condition": args.condition or ''
})
apply_on = doc.apply_on.replace(' ', '_').lower()

View File

@ -37,6 +37,8 @@ def get_pricing_rules(args, doc=None):
rules = []
pricing_rules = filter_pricing_rule_based_on_condition(pricing_rules, doc)
if not pricing_rules: return []
if apply_multiple_pricing_rules(pricing_rules):
@ -51,6 +53,23 @@ def get_pricing_rules(args, doc=None):
return rules
def filter_pricing_rule_based_on_condition(pricing_rules, doc=None):
filtered_pricing_rules = []
if doc:
for pricing_rule in pricing_rules:
if pricing_rule.condition:
try:
if frappe.safe_eval(pricing_rule.condition, None, doc.as_dict()):
filtered_pricing_rules.append(pricing_rule)
except:
pass
else:
filtered_pricing_rules.append(pricing_rule)
else:
filtered_pricing_rules = pricing_rules
return filtered_pricing_rules
def _get_pricing_rules(apply_on, args, values):
apply_on_field = frappe.scrub(apply_on)

View File

@ -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",

View File

@ -711,7 +711,8 @@ class PurchaseInvoice(BuyingController):
item.item_tax_amount / self.conversion_rate)
}, item=item))
else:
cwip_account = get_asset_account("capital_work_in_progress_account", company = self.company)
cwip_account = get_asset_account("capital_work_in_progress_account",
asset_category=item.asset_category,company=self.company)
cwip_account_currency = get_account_currency(cwip_account)
gl_entries.append(self.get_gl_dict({

View File

@ -1002,7 +1002,8 @@ def make_purchase_invoice(**args):
"cost_center": args.cost_center or "_Test Cost Center - _TC",
"project": args.project,
"rejected_warehouse": args.rejected_warehouse or "",
"rejected_serial_no": args.rejected_serial_no or ""
"rejected_serial_no": args.rejected_serial_no or "",
"asset_location": args.location or ""
})
if args.get_taxes_and_charges:

View File

@ -19,6 +19,7 @@
"is_return",
"column_break1",
"company",
"company_tax_id",
"posting_date",
"posting_time",
"set_posting_time",
@ -1825,7 +1826,7 @@
"fieldtype": "Table",
"hide_days": 1,
"hide_seconds": 1,
"label": "Sales Team1",
"label": "Sales Contributions and Incentives",
"oldfieldname": "sales_team",
"oldfieldtype": "Table",
"options": "Sales Team",
@ -1926,6 +1927,7 @@
},
{
"default": "0",
"depends_on": "eval:(doc.is_pos && doc.is_consolidated)",
"fieldname": "is_consolidated",
"fieldtype": "Check",
"label": "Is Consolidated",
@ -1940,13 +1942,20 @@
"hide_seconds": 1,
"label": "Is Internal Customer",
"read_only": 1
},
{
"fetch_from": "company.tax_id",
"fieldname": "company_tax_id",
"fieldtype": "Data",
"label": "Company Tax ID",
"read_only": 1
}
],
"icon": "fa fa-file-text",
"idx": 181,
"is_submittable": 1,
"links": [],
"modified": "2020-08-27 01:56:28.532140",
"modified": "2020-10-09 15:59:57.544736",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",

View File

@ -428,7 +428,7 @@ class SalesInvoice(SellingController):
if pos.get('account_for_change_amount'):
self.account_for_change_amount = pos.get('account_for_change_amount')
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)):
@ -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"))

View File

@ -13,8 +13,7 @@ def get_data():
'Auto Repeat': 'reference_document',
},
'internal_links': {
'Sales Order': ['items', 'sales_order'],
'Delivery Note': ['items', 'delivery_note']
'Sales Order': ['items', 'sales_order']
},
'transactions': [
{

View File

@ -345,6 +345,7 @@ class Subscription(Document):
invoice.set_taxes()
# Due date
if self.days_until_due:
invoice.append(
'payment_schedule',
{

View File

@ -6,6 +6,8 @@ from __future__ import unicode_literals
import frappe
import unittest
from erpnext.accounts.doctype.tax_rule.tax_rule import IncorrectCustomerGroup, IncorrectSupplierType, ConflictingTaxRule, get_tax_template
from erpnext.crm.doctype.opportunity.test_opportunity import make_opportunity
from erpnext.crm.doctype.opportunity.opportunity import make_quotation
test_records = frappe.get_test_records('Tax Rule')
@ -144,6 +146,23 @@ class TestTaxRule(unittest.TestCase):
self.assertEqual(get_tax_template("2015-01-01", {"customer":"_Test Customer", "billing_city": "Test City 1"}),
"_Test Sales Taxes and Charges Template 1 - _TC")
def test_taxes_fetch_via_tax_rule(self):
make_tax_rule(customer= "_Test Customer", billing_city = "_Test City",
sales_tax_template = "_Test Sales Taxes and Charges Template - _TC", save=1)
# create opportunity for customer
opportunity = make_opportunity(with_items=1)
# make quotation from opportunity
quotation = make_quotation(opportunity.name)
quotation.save()
self.assertEqual(quotation.taxes_and_charges, "_Test Sales Taxes and Charges Template - _TC")
# Check if accounts heads and rate fetched are also fetched from tax template or not
self.assertTrue(len(quotation.taxes) > 0)
def make_tax_rule(**args):
args = frappe._dict(args)

View File

@ -106,6 +106,7 @@ def get_tds_amount(suppliers, net_total, company, tax_details, fiscal_year_detai
from `tabGL Entry`
where company = %s and
party in %s and fiscal_year=%s and credit > 0
and is_opening = 'No'
""", (company, tuple(suppliers), fiscal_year), as_dict=1)
vouchers = [d.voucher_no for d in entries]
@ -192,6 +193,7 @@ def get_advance_vouchers(suppliers, fiscal_year=None, company=None, from_date=No
select distinct voucher_no
from `tabGL Entry`
where party in %s and %s and debit > 0
and is_opening = 'No'
""", (tuple(suppliers), condition)) or []
def get_debit_note_amount(suppliers, year_start_date, year_end_date, company=None):

View File

@ -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"))

View File

@ -20,7 +20,7 @@
"owner": "Administrator",
"steps": [
{
"step": "Chart Of Accounts"
"step": "Chart of Accounts"
},
{
"step": "Setup Taxes"

View File

@ -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
}

View File

@ -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,32 +82,12 @@ 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("""
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.credit_in_account_currency as paid_amount
jea.{jea_side}_in_account_currency as paid_amount
FROM
`tabJournal Entry Account` as jea
JOIN
@ -120,7 +99,7 @@ def check_matching_amount(bank_account, company, transaction):
AND
jea.account = %(bank_account)s
AND
jea.credit_in_account_currency like %(txt)s
jea.{jea_side}_in_account_currency like %(txt)s
AND
je.docstatus = 1
""", {

View File

@ -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),

View File

@ -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],
]
}
}

View File

@ -0,0 +1,76 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
/* eslint-disable */
frappe.query_reports["POS Register"] = {
"filters": [
{
"fieldname":"company",
"label": __("Company"),
"fieldtype": "Link",
"options": "Company",
"default": frappe.defaults.get_user_default("Company"),
"reqd": 1
},
{
"fieldname":"from_date",
"label": __("From Date"),
"fieldtype": "Date",
"default": frappe.datetime.add_months(frappe.datetime.get_today(), -1),
"reqd": 1,
"width": "60px"
},
{
"fieldname":"to_date",
"label": __("To Date"),
"fieldtype": "Date",
"default": frappe.datetime.get_today(),
"reqd": 1,
"width": "60px"
},
{
"fieldname":"pos_profile",
"label": __("POS Profile"),
"fieldtype": "Link",
"options": "POS Profile"
},
{
"fieldname":"cashier",
"label": __("Cashier"),
"fieldtype": "Link",
"options": "User"
},
{
"fieldname":"customer",
"label": __("Customer"),
"fieldtype": "Link",
"options": "Customer"
},
{
"fieldname":"mode_of_payment",
"label": __("Payment Method"),
"fieldtype": "Link",
"options": "Mode of Payment"
},
{
"fieldname":"group_by",
"label": __("Group by"),
"fieldtype": "Select",
"options": ["", "POS Profile", "Cashier", "Payment Method", "Customer"],
"default": "POS Profile"
},
{
"fieldname":"is_return",
"label": __("Is Return"),
"fieldtype": "Check"
},
],
"formatter": function(value, row, column, data, default_formatter) {
value = default_formatter(value, row, column, data);
if (data && data.bold) {
value = value.bold();
}
return value;
}
};

View File

@ -0,0 +1,30 @@
{
"add_total_row": 0,
"columns": [],
"creation": "2020-09-10 19:25:03.766871",
"disable_prepared_report": 0,
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 0,
"is_standard": "Yes",
"json": "{}",
"modified": "2020-09-10 19:25:15.851331",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Register",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "POS Invoice",
"report_name": "POS Register",
"report_type": "Script Report",
"roles": [
{
"role": "Accounts Manager"
},
{
"role": "Accounts User"
}
]
}

View File

@ -0,0 +1,222 @@
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe import _, _dict
from erpnext import get_company_currency, get_default_company
from erpnext.accounts.report.sales_register.sales_register import get_mode_of_payments
def execute(filters=None):
if not filters:
return [], []
validate_filters(filters)
columns = get_columns(filters)
group_by_field = get_group_by_field(filters.get("group_by"))
pos_entries = get_pos_entries(filters, group_by_field)
if group_by_field != "mode_of_payment":
concat_mode_of_payments(pos_entries)
# return only entries if group by is unselected
if not group_by_field:
return columns, pos_entries
# handle grouping
invoice_map, grouped_data = {}, []
for d in pos_entries:
invoice_map.setdefault(d[group_by_field], []).append(d)
for key in invoice_map:
invoices = invoice_map[key]
grouped_data += invoices
add_subtotal_row(grouped_data, invoices, group_by_field, key)
# move group by column to first position
column_index = next((index for (index, d) in enumerate(columns) if d["fieldname"] == group_by_field), None)
columns.insert(0, columns.pop(column_index))
return columns, grouped_data
def get_pos_entries(filters, group_by_field):
conditions = get_conditions(filters)
order_by = "p.posting_date"
select_mop_field, from_sales_invoice_payment, group_by_mop_condition = "", "", ""
if group_by_field == "mode_of_payment":
select_mop_field = ", sip.mode_of_payment"
from_sales_invoice_payment = ", `tabSales Invoice Payment` sip"
group_by_mop_condition = "sip.parent = p.name AND ifnull(sip.base_amount, 0) != 0 AND"
order_by += ", sip.mode_of_payment"
elif group_by_field:
order_by += ", p.{}".format(group_by_field)
return frappe.db.sql(
"""
SELECT
p.posting_date, p.name as pos_invoice, p.pos_profile,
p.owner, p.base_grand_total as grand_total, p.base_paid_amount as paid_amount,
p.customer, p.is_return {select_mop_field}
FROM
`tabPOS Invoice` p {from_sales_invoice_payment}
WHERE
{group_by_mop_condition}
{conditions}
ORDER BY
{order_by}
""".format(
select_mop_field=select_mop_field,
from_sales_invoice_payment=from_sales_invoice_payment,
group_by_mop_condition=group_by_mop_condition,
conditions=conditions,
order_by=order_by
), filters, as_dict=1)
def concat_mode_of_payments(pos_entries):
mode_of_payments = get_mode_of_payments(set([d.pos_invoice for d in pos_entries]))
for entry in pos_entries:
if mode_of_payments.get(entry.pos_invoice):
entry.mode_of_payment = ", ".join(mode_of_payments.get(entry.pos_invoice, []))
def add_subtotal_row(data, group_invoices, group_by_field, group_by_value):
grand_total = sum([d.grand_total for d in group_invoices])
paid_amount = sum([d.paid_amount for d in group_invoices])
data.append({
group_by_field: group_by_value,
"grand_total": grand_total,
"paid_amount": paid_amount,
"bold": 1
})
data.append({})
def validate_filters(filters):
if not filters.get("company"):
frappe.throw(_("{0} is mandatory").format(_("Company")))
if not filters.get("from_date") and not filters.get("to_date"):
frappe.throw(_("{0} and {1} are mandatory").format(frappe.bold(_("From Date")), frappe.bold(_("To Date"))))
if filters.from_date > filters.to_date:
frappe.throw(_("From Date must be before To Date"))
if (filters.get("pos_profile") and filters.get("group_by") == _('POS Profile')):
frappe.throw(_("Can not filter based on POS Profile, if grouped by POS Profile"))
if (filters.get("customer") and filters.get("group_by") == _('Customer')):
frappe.throw(_("Can not filter based on Customer, if grouped by Customer"))
if (filters.get("owner") and filters.get("group_by") == _('Cashier')):
frappe.throw(_("Can not filter based on Cashier, if grouped by Cashier"))
if (filters.get("mode_of_payment") and filters.get("group_by") == _('Payment Method')):
frappe.throw(_("Can not filter based on Payment Method, if grouped by Payment Method"))
def get_conditions(filters):
conditions = "company = %(company)s AND posting_date >= %(from_date)s AND posting_date <= %(to_date)s".format(
company=filters.get("company"),
from_date=filters.get("from_date"),
to_date=filters.get("to_date"))
if filters.get("pos_profile"):
conditions += " AND pos_profile = %(pos_profile)s".format(pos_profile=filters.get("pos_profile"))
if filters.get("owner"):
conditions += " AND owner = %(owner)s".format(owner=filters.get("owner"))
if filters.get("customer"):
conditions += " AND customer = %(customer)s".format(customer=filters.get("customer"))
if filters.get("is_return"):
conditions += " AND is_return = %(is_return)s".format(is_return=filters.get("is_return"))
if filters.get("mode_of_payment"):
conditions += """
AND EXISTS(
SELECT name FROM `tabSales Invoice Payment` sip
WHERE parent=p.name AND ifnull(sip.mode_of_payment, '') = %(mode_of_payment)s
)"""
return conditions
def get_group_by_field(group_by):
group_by_field = ""
if group_by == "POS Profile":
group_by_field = "pos_profile"
elif group_by == "Cashier":
group_by_field = "owner"
elif group_by == "Customer":
group_by_field = "customer"
elif group_by == "Payment Method":
group_by_field = "mode_of_payment"
return group_by_field
def get_columns(filters):
columns = [
{
"label": _("Posting Date"),
"fieldname": "posting_date",
"fieldtype": "Date",
"width": 90
},
{
"label": _("POS Invoice"),
"fieldname": "pos_invoice",
"fieldtype": "Link",
"options": "POS Invoice",
"width": 120
},
{
"label": _("Customer"),
"fieldname": "customer",
"fieldtype": "Link",
"options": "Customer",
"width": 120
},
{
"label": _("POS Profile"),
"fieldname": "pos_profile",
"fieldtype": "Link",
"options": "POS Profile",
"width": 160
},
{
"label": _("Cashier"),
"fieldname": "owner",
"fieldtype": "Link",
"options": "User",
"width": 140
},
{
"label": _("Grand Total"),
"fieldname": "grand_total",
"fieldtype": "Currency",
"options": "company:currency",
"width": 120
},
{
"label": _("Paid Amount"),
"fieldname": "paid_amount",
"fieldtype": "Currency",
"options": "company:currency",
"width": 120
},
{
"label": _("Payment Method"),
"fieldname": "mode_of_payment",
"fieldtype": "Data",
"width": 150
},
{
"label": _("Is Return"),
"fieldname": "is_return",
"fieldtype": "Data",
"width": 80
},
]
return columns

View File

@ -32,12 +32,12 @@ def execute(filters=None):
chart = get_chart_data(filters, columns, income, expense, net_profit_loss)
default_currency = frappe.get_cached_value('Company', filters.company, "default_currency")
report_summary = get_report_summary(period_list, filters.periodicity, income, expense, net_profit_loss, default_currency)
currency = filters.presentation_currency or frappe.get_cached_value('Company', filters.company, "default_currency")
report_summary = get_report_summary(period_list, filters.periodicity, income, expense, net_profit_loss, currency)
return columns, data, None, chart, report_summary
def get_report_summary(period_list, periodicity, income, expense, net_profit_loss, default_currency, consolidated=False):
def get_report_summary(period_list, periodicity, income, expense, net_profit_loss, currency, consolidated=False):
net_income, net_expense, net_profit = 0.0, 0.0, 0.0
for period in period_list:
@ -64,19 +64,19 @@ def get_report_summary(period_list, periodicity, income, expense, net_profit_los
"indicator": "Green" if net_profit > 0 else "Red",
"label": profit_label,
"datatype": "Currency",
"currency": net_profit_loss.get("currency") if net_profit_loss else default_currency
"currency": currency
},
{
"value": net_income,
"label": income_label,
"datatype": "Currency",
"currency": income[-1].get('currency') if income else default_currency
"currency": currency
},
{
"value": net_expense,
"label": expense_label,
"datatype": "Currency",
"currency": expense[-1].get('currency') if expense else default_currency
"currency": currency
}
]

View File

@ -72,6 +72,12 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() {
"fieldtype": "Link",
"options": "Finance Book",
},
{
"fieldname": "presentation_currency",
"label": __("Currency"),
"fieldtype": "Select",
"options": erpnext.get_presentation_currency_list()
},
{
"fieldname": "with_period_closing_entry",
"label": __("Period Closing Entry"),

View File

@ -56,7 +56,7 @@ def get_data(filters):
accounts = frappe.db.sql("""select name, account_number, parent_account, account_name, root_type, report_type, lft, rgt
from `tabAccount` where company=%s order by lft""", filters.company, as_dict=True)
company_currency = erpnext.get_company_currency(filters.company)
company_currency = filters.presentation_currency or erpnext.get_company_currency(filters.company)
if not accounts:
return None

View File

@ -683,6 +683,7 @@ def get_outstanding_invoices(party_type, party, account, condition=None, filters
where
party_type = %(party_type)s and party = %(party)s
and account = %(account)s and {dr_or_cr} > 0
and is_cancelled=0
{condition}
and ((voucher_type = 'Journal Entry'
and (against_voucher = '' or against_voucher is null))
@ -705,6 +706,7 @@ def get_outstanding_invoices(party_type, party, account, condition=None, filters
and account = %(account)s
and {payment_dr_or_cr} > 0
and against_voucher is not null and against_voucher != ''
and is_cancelled=0
group by against_voucher_type, against_voucher
""".format(payment_dr_or_cr=payment_dr_or_cr), {
"party_type": party_type,

View File

@ -466,29 +466,37 @@ class Asset(AccountsController):
def validate_make_gl_entry(self):
purchase_document = self.get_purchase_document()
asset_bought_with_invoice = purchase_document == self.purchase_invoice
fixed_asset_account, cwip_account = self.get_asset_accounts()
cwip_enabled = is_cwip_accounting_enabled(self.asset_category)
# check if expense already has been booked in case of cwip was enabled after purchasing asset
expense_booked = False
cwip_booked = False
if asset_bought_with_invoice:
expense_booked = frappe.db.sql("""SELECT name FROM `tabGL Entry` WHERE voucher_no = %s and account = %s""",
(purchase_document, fixed_asset_account), as_dict=1)
else:
cwip_booked = frappe.db.sql("""SELECT name FROM `tabGL Entry` WHERE voucher_no = %s and account = %s""",
(purchase_document, cwip_account), as_dict=1)
if cwip_enabled and (expense_booked or not cwip_booked):
# if expense has already booked from invoice or cwip is booked from receipt
if not purchase_document:
return False
elif not cwip_enabled and (not expense_booked or cwip_booked):
# if cwip is disabled but expense hasn't been booked yet
return True
elif cwip_enabled:
# default condition
asset_bought_with_invoice = (purchase_document == self.purchase_invoice)
fixed_asset_account = self.get_fixed_asset_account()
cwip_enabled = is_cwip_accounting_enabled(self.asset_category)
cwip_account = self.get_cwip_account(cwip_enabled=cwip_enabled)
query = """SELECT name FROM `tabGL Entry` WHERE voucher_no = %s and account = %s"""
if asset_bought_with_invoice:
# with invoice purchase either expense or cwip has been booked
expense_booked = frappe.db.sql(query, (purchase_document, fixed_asset_account), as_dict=1)
if expense_booked:
# if expense is already booked from invoice then do not make gl entries regardless of cwip enabled/disabled
return False
cwip_booked = frappe.db.sql(query, (purchase_document, cwip_account), as_dict=1)
if cwip_booked:
# if cwip is booked from invoice then make gl entries regardless of cwip enabled/disabled
return True
else:
# with receipt purchase either cwip has been booked or no entries have been made
if not cwip_account:
# if cwip account isn't available do not make gl entries
return False
cwip_booked = frappe.db.sql(query, (purchase_document, cwip_account), as_dict=1)
# if cwip is not booked from receipt then do not make gl entries
# if cwip is booked from receipt then make gl entries
return cwip_booked
def get_purchase_document(self):
asset_bought_with_invoice = self.purchase_invoice and frappe.db.get_value('Purchase Invoice', self.purchase_invoice, 'update_stock')
@ -496,20 +504,25 @@ class Asset(AccountsController):
return purchase_document
def get_asset_accounts(self):
fixed_asset_account = get_asset_category_account('fixed_asset_account', asset=self.name,
asset_category = self.asset_category, company = self.company)
def get_fixed_asset_account(self):
return get_asset_category_account('fixed_asset_account', None, self.name, None, self.asset_category, self.company)
cwip_account = get_asset_account("capital_work_in_progress_account",
self.name, self.asset_category, self.company)
def get_cwip_account(self, cwip_enabled=False):
cwip_account = None
try:
cwip_account = get_asset_account("capital_work_in_progress_account", self.name, self.asset_category, self.company)
except:
# if no cwip account found in category or company and "cwip is enabled" then raise else silently pass
if cwip_enabled:
raise
return fixed_asset_account, cwip_account
return cwip_account
def make_gl_entries(self):
gl_entries = []
purchase_document = self.get_purchase_document()
fixed_asset_account, cwip_account = self.get_asset_accounts()
fixed_asset_account, cwip_account = self.get_fixed_asset_account(), self.get_cwip_account()
if (purchase_document and self.purchase_receipt_amount and self.available_for_use_date <= nowdate()):
@ -561,14 +574,18 @@ class Asset(AccountsController):
return 100 * (1 - flt(depreciation_rate, float_precision))
def update_maintenance_status():
assets = frappe.get_all('Asset', filters = {'docstatus': 1, 'maintenance_required': 1})
assets = frappe.get_all(
"Asset", filters={"docstatus": 1, "maintenance_required": 1}
)
for asset in assets:
asset = frappe.get_doc("Asset", asset.name)
if frappe.db.exists('Asset Maintenance Task', {'parent': asset.name, 'next_due_date': today()}):
asset.set_status('In Maintenance')
if frappe.db.exists('Asset Repair', {'asset_name': asset.name, 'repair_status': 'Pending'}):
asset.set_status('Out of Order')
if frappe.db.exists("Asset Repair", {"asset_name": asset.name, "repair_status": "Pending"}):
asset.set_status("Out of Order")
elif frappe.db.exists("Asset Maintenance Task", {"parent": asset.name, "next_due_date": today()}):
asset.set_status("In Maintenance")
else:
asset.set_status()
def make_post_gl_entry():

View File

@ -9,6 +9,7 @@ from frappe.utils import cstr, nowdate, getdate, flt, get_last_day, add_days, ad
from erpnext.assets.doctype.asset.depreciation import post_depreciation_entries, scrap_asset, restore_asset
from erpnext.assets.doctype.asset.asset import make_sales_invoice
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_invoice as make_invoice
class TestAsset(unittest.TestCase):
@ -558,81 +559,6 @@ class TestAsset(unittest.TestCase):
self.assertEqual(gle, expected_gle)
def test_gle_with_cwip_toggling(self):
# TEST: purchase an asset with cwip enabled and then disable cwip and try submitting the asset
frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", 1)
pr = make_purchase_receipt(item_code="Macbook Pro",
qty=1, rate=5000, do_not_submit=True, location="Test Location")
pr.set('taxes', [{
'category': 'Total',
'add_deduct_tax': 'Add',
'charge_type': 'On Net Total',
'account_head': '_Test Account Service Tax - _TC',
'description': '_Test Account Service Tax',
'cost_center': 'Main - _TC',
'rate': 5.0
}, {
'category': 'Valuation and Total',
'add_deduct_tax': 'Add',
'charge_type': 'On Net Total',
'account_head': '_Test Account Shipping Charges - _TC',
'description': '_Test Account Shipping Charges',
'cost_center': 'Main - _TC',
'rate': 5.0
}])
pr.submit()
expected_gle = (
("Asset Received But Not Billed - _TC", 0.0, 5250.0),
("CWIP Account - _TC", 5250.0, 0.0)
)
pr_gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry`
where voucher_type='Purchase Receipt' and voucher_no = %s
order by account""", pr.name)
self.assertEqual(pr_gle, expected_gle)
pi = make_invoice(pr.name)
pi.submit()
expected_gle = (
("_Test Account Service Tax - _TC", 250.0, 0.0),
("_Test Account Shipping Charges - _TC", 250.0, 0.0),
("Asset Received But Not Billed - _TC", 5250.0, 0.0),
("Creditors - _TC", 0.0, 5500.0),
("Expenses Included In Asset Valuation - _TC", 0.0, 250.0),
)
pi_gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry`
where voucher_type='Purchase Invoice' and voucher_no = %s
order by account""", pi.name)
self.assertEqual(pi_gle, expected_gle)
asset = frappe.db.get_value('Asset', {'purchase_receipt': pr.name, 'docstatus': 0}, 'name')
asset_doc = frappe.get_doc('Asset', asset)
month_end_date = get_last_day(nowdate())
asset_doc.available_for_use_date = nowdate() if nowdate() != month_end_date else add_days(nowdate(), -15)
self.assertEqual(asset_doc.gross_purchase_amount, 5250.0)
asset_doc.append("finance_books", {
"expected_value_after_useful_life": 200,
"depreciation_method": "Straight Line",
"total_number_of_depreciations": 3,
"frequency_of_depreciation": 10,
"depreciation_start_date": month_end_date
})
# disable cwip and try submitting
frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", 0)
asset_doc.submit()
# asset should have gl entries even if cwip is disabled
expected_gle = (
("_Test Fixed Asset - _TC", 5250.0, 0.0),
("CWIP Account - _TC", 0.0, 5250.0)
)
gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry`
where voucher_type='Asset' and voucher_no = %s
order by account""", asset_doc.name)
self.assertEqual(gle, expected_gle)
frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", 1)
def test_expense_head(self):
pr = make_purchase_receipt(item_code="Macbook Pro",
qty=2, rate=200000.0, location="Test Location")
@ -641,6 +567,74 @@ class TestAsset(unittest.TestCase):
self.assertEquals('Asset Received But Not Billed - _TC', doc.items[0].expense_account)
def test_asset_cwip_toggling_cases(self):
cwip = frappe.db.get_value("Asset Category", "Computers", "enable_cwip_accounting")
name = frappe.db.get_value("Asset Category Account", filters={"parent": "Computers"}, fieldname=["name"])
cwip_acc = "CWIP Account - _TC"
frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", 0)
frappe.db.set_value("Asset Category Account", name, "capital_work_in_progress_account", "")
frappe.db.get_value("Company", "_Test Company", "capital_work_in_progress_account", "")
# case 0 -- PI with cwip disable, Asset with cwip disabled, No cwip account set
pi = make_purchase_invoice(item_code="Macbook Pro", qty=1, rate=200000.0, location="Test Location", update_stock=1)
asset = frappe.db.get_value('Asset', {'purchase_invoice': pi.name, 'docstatus': 0}, 'name')
asset_doc = frappe.get_doc('Asset', asset)
asset_doc.available_for_use_date = nowdate()
asset_doc.calculate_depreciation = 0
asset_doc.submit()
gle = frappe.db.sql("""select name from `tabGL Entry` where voucher_type='Asset' and voucher_no = %s""", asset_doc.name)
self.assertFalse(gle)
# case 1 -- PR with cwip disabled, Asset with cwip enabled
pr = make_purchase_receipt(item_code="Macbook Pro", qty=1, rate=200000.0, location="Test Location")
frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", 1)
frappe.db.set_value("Asset Category Account", name, "capital_work_in_progress_account", cwip_acc)
asset = frappe.db.get_value('Asset', {'purchase_receipt': pr.name, 'docstatus': 0}, 'name')
asset_doc = frappe.get_doc('Asset', asset)
asset_doc.available_for_use_date = nowdate()
asset_doc.calculate_depreciation = 0
asset_doc.submit()
gle = frappe.db.sql("""select name from `tabGL Entry` where voucher_type='Asset' and voucher_no = %s""", asset_doc.name)
self.assertFalse(gle)
# case 2 -- PR with cwip enabled, Asset with cwip disabled
pr = make_purchase_receipt(item_code="Macbook Pro", qty=1, rate=200000.0, location="Test Location")
frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", 0)
asset = frappe.db.get_value('Asset', {'purchase_receipt': pr.name, 'docstatus': 0}, 'name')
asset_doc = frappe.get_doc('Asset', asset)
asset_doc.available_for_use_date = nowdate()
asset_doc.calculate_depreciation = 0
asset_doc.submit()
gle = frappe.db.sql("""select name from `tabGL Entry` where voucher_type='Asset' and voucher_no = %s""", asset_doc.name)
self.assertTrue(gle)
# case 3 -- PI with cwip disabled, Asset with cwip enabled
pi = make_purchase_invoice(item_code="Macbook Pro", qty=1, rate=200000.0, location="Test Location", update_stock=1)
frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", 1)
asset = frappe.db.get_value('Asset', {'purchase_invoice': pi.name, 'docstatus': 0}, 'name')
asset_doc = frappe.get_doc('Asset', asset)
asset_doc.available_for_use_date = nowdate()
asset_doc.calculate_depreciation = 0
asset_doc.submit()
gle = frappe.db.sql("""select name from `tabGL Entry` where voucher_type='Asset' and voucher_no = %s""", asset_doc.name)
self.assertFalse(gle)
# case 4 -- PI with cwip enabled, Asset with cwip disabled
pi = make_purchase_invoice(item_code="Macbook Pro", qty=1, rate=200000.0, location="Test Location", update_stock=1)
frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", 0)
asset = frappe.db.get_value('Asset', {'purchase_invoice': pi.name, 'docstatus': 0}, 'name')
asset_doc = frappe.get_doc('Asset', asset)
asset_doc.available_for_use_date = nowdate()
asset_doc.calculate_depreciation = 0
asset_doc.submit()
gle = frappe.db.sql("""select name from `tabGL Entry` where voucher_type='Asset' and voucher_no = %s""", asset_doc.name)
self.assertTrue(gle)
frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", cwip)
frappe.db.set_value("Asset Category Account", name, "capital_work_in_progress_account", cwip_acc)
frappe.db.get_value("Company", "_Test Company", "capital_work_in_progress_account", cwip_acc)
def create_asset_data():
if not frappe.db.exists("Asset Category", "Computers"):
create_asset_category()

View File

@ -5,7 +5,7 @@
from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.utils import cint
from frappe.utils import cint, get_link_to_form
from frappe.model.document import Document
class AssetCategory(Document):
@ -13,6 +13,7 @@ class AssetCategory(Document):
self.validate_finance_books()
self.validate_account_types()
self.validate_account_currency()
self.valide_cwip_account()
def validate_finance_books(self):
for d in self.finance_books:
@ -59,6 +60,21 @@ class AssetCategory(Document):
.format(d.idx, frappe.unscrub(key_to_match), frappe.bold(selected_account), frappe.bold(expected_key_type)),
title=_("Invalid Account"))
def valide_cwip_account(self):
if self.enable_cwip_accounting:
missing_cwip_accounts_for_company = []
for d in self.accounts:
if (not d.capital_work_in_progress_account and
not frappe.db.get_value("Company", d.company_name, "capital_work_in_progress_account")):
missing_cwip_accounts_for_company.append(get_link_to_form("Company", d.company_name))
if missing_cwip_accounts_for_company:
msg = _("""To enable Capital Work in Progress Accounting, """)
msg += _("""you must select Capital Work in Progress Account in accounts table""")
msg += "<br><br>"
msg += _("You can also set default CWIP account in Company {}").format(", ".join(missing_cwip_accounts_for_company))
frappe.throw(msg, title=_("Missing Account"))
@frappe.whitelist()
def get_asset_category_account(fieldname, item=None, asset=None, account=None, asset_category = None, company = None):

View File

@ -27,3 +27,21 @@ class TestAssetCategory(unittest.TestCase):
except frappe.DuplicateEntryError:
pass
def test_cwip_accounting(self):
company_cwip_acc = frappe.db.get_value("Company", "_Test Company", "capital_work_in_progress_account")
frappe.db.set_value("Company", "_Test Company", "capital_work_in_progress_account", "")
asset_category = frappe.new_doc("Asset Category")
asset_category.asset_category_name = "Computers"
asset_category.enable_cwip_accounting = 1
asset_category.total_number_of_depreciations = 3
asset_category.frequency_of_depreciation = 3
asset_category.append("accounts", {
"company_name": "_Test Company",
"fixed_asset_account": "_Test Fixed Asset - _TC",
"accumulated_depreciation_account": "_Test Accumulated Depreciations - _TC",
"depreciation_expense_account": "_Test Depreciations - _TC"
})
self.assertRaises(frappe.ValidationError, asset_category.insert)

View File

@ -33,7 +33,7 @@
{
"hidden": 0,
"label": "Other Reports",
"links": "[\n {\n \"is_query_report\": true,\n \"label\": \"Items To Be Requested\",\n \"name\": \"Items To Be Requested\",\n \"onboard\": 1,\n \"reference_doctype\": \"Item\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"Item-wise Purchase History\",\n \"name\": \"Item-wise Purchase History\",\n \"onboard\": 1,\n \"reference_doctype\": \"Item\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"Purchase Receipt Trends\",\n \"name\": \"Purchase Receipt Trends\",\n \"reference_doctype\": \"Purchase Receipt\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"Purchase Invoice Trends\",\n \"name\": \"Purchase Invoice Trends\",\n \"reference_doctype\": \"Purchase Invoice\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"Subcontracted Raw Materials To Be Transferred\",\n \"name\": \"Subcontracted Raw Materials To Be Transferred\",\n \"reference_doctype\": \"Purchase Order\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"Subcontracted Item To Be Received\",\n \"name\": \"Subcontracted Item To Be Received\",\n \"reference_doctype\": \"Purchase Order\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"Quoted Item Comparison\",\n \"name\": \"Quoted Item Comparison\",\n \"onboard\": 1,\n \"reference_doctype\": \"Supplier Quotation\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"Material Requests for which Supplier Quotations are not created\",\n \"name\": \"Material Requests for which Supplier Quotations are not created\",\n \"reference_doctype\": \"Material Request\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"Supplier Addresses And Contacts\",\n \"name\": \"Address And Contacts\",\n \"reference_doctype\": \"Address\",\n \"route_options\": {\n \"party_type\": \"Supplier\"\n },\n \"type\": \"report\"\n }\n]"
"links": "[\n {\n \"is_query_report\": true,\n \"label\": \"Items To Be Requested\",\n \"name\": \"Items To Be Requested\",\n \"onboard\": 1,\n \"reference_doctype\": \"Item\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"Item-wise Purchase History\",\n \"name\": \"Item-wise Purchase History\",\n \"onboard\": 1,\n \"reference_doctype\": \"Item\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"Purchase Receipt Trends\",\n \"name\": \"Purchase Receipt Trends\",\n \"reference_doctype\": \"Purchase Receipt\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"Purchase Invoice Trends\",\n \"name\": \"Purchase Invoice Trends\",\n \"reference_doctype\": \"Purchase Invoice\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"Subcontracted Raw Materials To Be Transferred\",\n \"name\": \"Subcontracted Raw Materials To Be Transferred\",\n \"reference_doctype\": \"Purchase Order\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"Subcontracted Item To Be Received\",\n \"name\": \"Subcontracted Item To Be Received\",\n \"reference_doctype\": \"Purchase Order\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"Supplier Quotation Comparison\",\n \"name\": \"Supplier Quotation Comparison\",\n \"onboard\": 1,\n \"reference_doctype\": \"Supplier Quotation\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"Material Requests for which Supplier Quotations are not created\",\n \"name\": \"Material Requests for which Supplier Quotations are not created\",\n \"reference_doctype\": \"Material Request\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"Supplier Addresses And Contacts\",\n \"name\": \"Address And Contacts\",\n \"reference_doctype\": \"Address\",\n \"route_options\": {\n \"party_type\": \"Supplier\"\n },\n \"type\": \"report\"\n }\n]"
},
{
"hidden": 0,
@ -60,7 +60,7 @@
"idx": 0,
"is_standard": 1,
"label": "Buying",
"modified": "2020-06-29 19:30:24.983050",
"modified": "2020-09-30 14:40:55.638458",
"modified_by": "Administrator",
"module": "Buying",
"name": "Buying",

View File

@ -1084,7 +1084,7 @@
"idx": 105,
"is_submittable": 1,
"links": [],
"modified": "2020-09-14 14:36:12.418690",
"modified": "2020-10-07 14:31:57.661221",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order",
@ -1130,11 +1130,11 @@
"write": 1
}
],
"search_fields": "status, transaction_date, supplier,grand_total",
"search_fields": "status, transaction_date, supplier, grand_total",
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "DESC",
"timeline_field": "supplier",
"title_field": "supplier",
"title_field": "supplier_name",
"track_changes": 1
}

View File

@ -651,12 +651,12 @@ class TestPurchaseOrder(unittest.TestCase):
make_subcontracted_item(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,7 +664,7 @@ 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)

View File

@ -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"))
}
@ -179,7 +177,7 @@ frappe.ui.form.on("Request for Quotation",{
dialog.hide();
return frappe.call({
type: "GET",
method: "erpnext.buying.doctype.request_for_quotation.request_for_quotation.make_supplier_quotation",
method: "erpnext.buying.doctype.request_for_quotation.request_for_quotation.make_supplier_quotation_from_rfq",
args: {
"source_name": doc.name,
"for_supplier": args.supplier
@ -194,6 +192,66 @@ frappe.ui.form.on("Request for Quotation",{
});
});
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['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.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();
}
})

View File

@ -1,5 +1,5 @@
{
"actions": "",
"actions": [],
"allow_import": 1,
"autoname": "naming_series:",
"creation": "2016-02-25 01:24:07.224790",
@ -19,7 +19,12 @@
"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",
@ -126,8 +131,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 +144,8 @@
"print_hide": 1
},
{
"fetch_from": "email_template.response",
"fetch_if_empty": 1,
"fieldname": "message_for_supplier",
"fieldtype": "Text Editor",
"in_list_view": 1,
@ -230,12 +239,45 @@
"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",
"is_submittable": 1,
"links": [],
"modified": "2020-06-25 14:37:21.140194",
"modified": "2020-10-01 14:54:50.888729",
"modified_by": "Administrator",
"module": "Buying",
"name": "Request for Quotation",

View File

@ -51,7 +51,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,17 +62,31 @@ 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.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):
@ -87,18 +101,19 @@ class RequestforQuotation(BuyingController):
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 +130,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 +148,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)
@ -214,10 +247,10 @@ def get_supplier_contacts(doctype, txt, searchfield, start, page_len, filters):
and `tabDynamic Link`.link_name like %(txt)s) and `tabContact`.name = `tabDynamic Link`.parent
limit %(start)s, %(page_len)s""", {"start": start, "page_len":page_len, "txt": "%%%s%%" % txt, "name": filters.get('supplier')})
# This method is used to make supplier quotation from material request form.
@frappe.whitelist()
def make_supplier_quotation(source_name, for_supplier, target_doc=None):
def make_supplier_quotation_from_rfq(source_name, target_doc=None, for_supplier=None):
def postprocess(source, target_doc):
if for_supplier:
target_doc.supplier = for_supplier
args = get_party_details(for_supplier, party_type="Supplier", ignore_permissions=True)
target_doc.currency = args.currency or get_party_account_currency('Supplier', for_supplier, source.company)
@ -354,3 +387,32 @@ def get_supplier_tag():
frappe.cache().hset("Supplier", "Tags", tags)
return frappe.cache().hget("Supplier", "Tags")
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_rfq_containing_supplier(doctype, txt, searchfield, start, page_len, filters):
conditions = ""
if txt:
conditions += "and rfq.name like '%%"+txt+"%%' "
if filters.get("transaction_date"):
conditions += "and rfq.transaction_date = '{0}'".format(filters.get("transaction_date"))
rfq_data = frappe.db.sql("""
select
distinct rfq.name, rfq.transaction_date,
rfq.company
from
`tabRequest for Quotation` rfq, `tabRequest for Quotation Supplier` rfq_supplier
where
rfq.name = rfq_supplier.parent
and rfq_supplier.supplier = '{0}'
and rfq.docstatus = 1
and rfq.company = '{1}'
{2}
order by rfq.transaction_date ASC
limit %(page_len)s offset %(start)s """ \
.format(filters.get("supplier"), filters.get("company"), conditions),
{"page_len": page_len, "start": start}, as_dict=1)
return rfq_data

View File

@ -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,7 +22,7 @@ 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
@ -37,10 +37,10 @@ class TestRequestforQuotation(unittest.TestCase):
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 +62,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")

View File

@ -1,362 +1,112 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"actions": [],
"creation": "2016-03-29 05:59:11.896885",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"send_email",
"email_sent",
"supplier",
"contact",
"no_quote",
"quote_status",
"column_break_3",
"supplier_name",
"email_id",
"download_pdf"
],
"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
"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
"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
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"allow_on_submit": 1,
"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
"options": "Contact"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 1,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "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
"label": "No Quote"
},
{
"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
"read_only": 1
},
{
"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
"fieldtype": "Column Break"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 1,
"collapsible": 0,
"columns": 0,
"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
"label": "Supplier Name"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"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
"no_copy": 1
},
{
"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
"label": "Download PDF"
}
],
"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",
"links": [],
"modified": "2020-09-28 19:31:11.855588",
"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
"track_changes": 1
}

View File

@ -8,8 +8,7 @@ erpnext.buying.SupplierQuotationController = erpnext.buying.BuyingController.ext
setup: function() {
this.frm.custom_make_buttons = {
'Purchase Order': 'Purchase Order',
'Quotation': 'Quotation',
'Subscription': 'Subscription'
'Quotation': 'Quotation'
}
this._super();
@ -28,12 +27,6 @@ erpnext.buying.SupplierQuotationController = erpnext.buying.BuyingController.ext
cur_frm.page.set_inner_btn_group_as_primary(__('Create'));
cur_frm.add_custom_button(__("Quotation"), this.make_quotation,
__('Create'));
if(!this.frm.doc.auto_repeat) {
cur_frm.add_custom_button(__('Subscription'), function() {
erpnext.utils.make_subscription(me.frm.doc.doctype, me.frm.doc.name)
}, __('Create'))
}
}
else if (this.frm.doc.docstatus===0) {
@ -54,6 +47,27 @@ erpnext.buying.SupplierQuotationController = erpnext.buying.BuyingController.ext
}
})
}, __("Get items from"));
this.frm.add_custom_button(__("Request for Quotation"),
function() {
if (!me.frm.doc.supplier) {
frappe.throw({message:__("Please select a Supplier"), title:__("Mandatory")})
}
erpnext.utils.map_current_doc({
method: "erpnext.buying.doctype.request_for_quotation.request_for_quotation.make_supplier_quotation_from_rfq",
source_doctype: "Request for Quotation",
target: me.frm,
setters: {
company: me.frm.doc.company,
transaction_date: null
},
get_query_filters: {
supplier: me.frm.doc.supplier
},
get_query_method: "erpnext.buying.doctype.request_for_quotation.request_for_quotation.get_rfq_containing_supplier"
})
}, __("Get items from"));
}
},

View File

@ -159,6 +159,7 @@
"default": "Today",
"fieldname": "transaction_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Date",
"oldfieldname": "transaction_date",
"oldfieldtype": "Date",
@ -798,6 +799,7 @@
{
"fieldname": "valid_till",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Valid Till"
}
],
@ -805,7 +807,7 @@
"idx": 29,
"is_submittable": 1,
"links": [],
"modified": "2020-07-18 05:10:45.556792",
"modified": "2020-10-01 20:56:17.932007",
"modified_by": "Administrator",
"module": "Buying",
"name": "Supplier Quotation",

View File

@ -12,6 +12,8 @@
"item_name",
"column_break_3",
"lead_time_days",
"expected_delivery_date",
"is_free_item",
"section_break_5",
"description",
"item_group",
@ -19,20 +21,18 @@
"col_break1",
"image",
"image_view",
"manufacture_details",
"manufacturer",
"column_break_15",
"manufacturer_part_no",
"quantity_and_rate",
"qty",
"stock_uom",
"price_list_rate",
"discount_percentage",
"discount_amount",
"col_break2",
"uom",
"conversion_factor",
"stock_qty",
"sec_break_price_list",
"price_list_rate",
"discount_percentage",
"discount_amount",
"col_break_price_list",
"base_price_list_rate",
"sec_break1",
"rate",
@ -42,7 +42,6 @@
"base_rate",
"base_amount",
"pricing_rules",
"is_free_item",
"section_break_24",
"net_rate",
"net_amount",
@ -56,7 +55,6 @@
"weight_uom",
"warehouse_and_reference",
"warehouse",
"project",
"prevdoc_doctype",
"material_request",
"sales_order",
@ -65,13 +63,19 @@
"material_request_item",
"request_for_quotation_item",
"item_tax_rate",
"manufacture_details",
"manufacturer",
"column_break_15",
"manufacturer_part_no",
"ad_sec_break",
"project",
"section_break_44",
"page_break"
],
"fields": [
{
"bold": 1,
"columns": 4,
"columns": 2,
"fieldname": "item_code",
"fieldtype": "Link",
"in_list_view": 1,
@ -107,7 +111,7 @@
{
"fieldname": "lead_time_days",
"fieldtype": "Int",
"label": "Lead Time in days"
"label": "Supplier Lead Time (days)"
},
{
"collapsible": 1,
@ -162,7 +166,6 @@
{
"fieldname": "stock_uom",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Stock UOM",
"options": "UOM",
"print_hide": 1,
@ -196,6 +199,7 @@
{
"fieldname": "uom",
"fieldtype": "Link",
"in_list_view": 1,
"label": "UOM",
"options": "UOM",
"print_hide": 1,
@ -237,7 +241,7 @@
"fieldname": "rate",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Rate ",
"label": "Rate",
"oldfieldname": "import_rate",
"oldfieldtype": "Currency",
"options": "currency"
@ -289,14 +293,6 @@
"print_hide": 1,
"read_only": 1
},
{
"default": "0",
"fieldname": "is_free_item",
"fieldtype": "Check",
"label": "Is Free Item",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "section_break_24",
"fieldtype": "Section Break"
@ -528,12 +524,43 @@
{
"fieldname": "column_break_15",
"fieldtype": "Column Break"
},
{
"fieldname": "sec_break_price_list",
"fieldtype": "Section Break"
},
{
"fieldname": "col_break_price_list",
"fieldtype": "Column Break"
},
{
"collapsible": 1,
"fieldname": "ad_sec_break",
"fieldtype": "Section Break",
"label": "Accounting Dimensions"
},
{
"default": "0",
"depends_on": "is_free_item",
"fieldname": "is_free_item",
"fieldtype": "Check",
"label": "Is Free Item",
"print_hide": 1,
"read_only": 1
},
{
"allow_on_submit": 1,
"bold": 1,
"fieldname": "expected_delivery_date",
"fieldtype": "Date",
"label": "Expected Delivery Date"
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2020-04-07 18:35:51.175947",
"modified": "2020-10-19 12:36:26.913211",
"modified_by": "Administrator",
"module": "Buying",
"name": "Supplier Quotation Item",

View File

@ -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

View File

@ -1,32 +0,0 @@
{
"add_total_row": 0,
"apply_user_permissions": 1,
"creation": "2016-07-21 08:31:05.890362",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"idx": 2,
"is_standard": "Yes",
"modified": "2017-02-24 20:04:58.784351",
"modified_by": "Administrator",
"module": "Buying",
"name": "Quoted Item Comparison",
"owner": "Administrator",
"ref_doctype": "Supplier Quotation",
"report_name": "Quoted Item Comparison",
"report_type": "Script Report",
"roles": [
{
"role": "Manufacturing Manager"
},
{
"role": "Purchase Manager"
},
{
"role": "Purchase User"
},
{
"role": "Stock User"
}
]
}

View File

@ -1,7 +1,7 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.query_reports["Quoted Item Comparison"] = {
frappe.query_reports["Supplier Quotation Comparison"] = {
filters: [
{
fieldtype: "Link",
@ -78,6 +78,13 @@ frappe.query_reports["Quoted Item Comparison"] = {
return { filters: { "docstatus": ["<", 2] } }
}
},
{
"fieldname":"group_by",
"label": __("Group by"),
"fieldtype": "Select",
"options": [__("Group by Supplier"), __("Group by Item")],
"default": __("Group by Supplier")
},
{
fieldtype: "Check",
label: __("Include Expired"),
@ -98,6 +105,9 @@ frappe.query_reports["Quoted Item Comparison"] = {
}
}
if(column.fieldname === "price_per_unit" && data.price_per_unit && data.min && data.min === 1){
value = `<div style="color:green">${value}</div>`;
}
return value;
},

View File

@ -0,0 +1,32 @@
{
"add_total_row": 0,
"apply_user_permissions": 1,
"creation": "2016-07-21 08:31:05.890362",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"idx": 2,
"is_standard": "Yes",
"modified": "2017-02-24 20:04:58.784351",
"modified_by": "Administrator",
"module": "Buying",
"name": "Supplier Quotation Comparison",
"owner": "Administrator",
"ref_doctype": "Supplier Quotation",
"report_name": "Supplier Quotation Comparison",
"report_type": "Script Report",
"roles": [
{
"role": "Manufacturing Manager"
},
{
"role": "Purchase Manager"
},
{
"role": "Purchase User"
},
{
"role": "Stock User"
}
]
}

View File

@ -12,9 +12,9 @@ def execute(filters=None):
if not filters:
return [], []
columns = get_columns(filters)
conditions = get_conditions(filters)
supplier_quotation_data = get_data(filters, conditions)
columns = get_columns()
data, chart_data = prepare_data(supplier_quotation_data, filters)
message = get_message()
@ -41,9 +41,13 @@ def get_conditions(filters):
return conditions
def get_data(filters, conditions):
supplier_quotation_data = frappe.db.sql("""SELECT
sqi.parent, sqi.item_code, sqi.qty, sqi.rate, sqi.uom, sqi.request_for_quotation,
sqi.lead_time_days, sq.supplier, sq.valid_till
supplier_quotation_data = frappe.db.sql("""
SELECT
sqi.parent, sqi.item_code,
sqi.qty, sqi.stock_qty, sqi.amount,
sqi.uom, sqi.stock_uom,
sqi.request_for_quotation,
sqi.lead_time_days, sq.supplier as supplier_name, sq.valid_till
FROM
`tabSupplier Quotation Item` sqi,
`tabSupplier Quotation` sq
@ -58,16 +62,18 @@ def get_data(filters, conditions):
return supplier_quotation_data
def prepare_data(supplier_quotation_data, filters):
out, suppliers, qty_list, chart_data = [], [], [], []
supplier_wise_map = defaultdict(list)
out, groups, qty_list, suppliers, chart_data = [], [], [], [], []
group_wise_map = defaultdict(list)
supplier_qty_price_map = {}
group_by_field = "supplier_name" if filters.get("group_by") == "Group by Supplier" else "item_code"
company_currency = frappe.db.get_default("currency")
float_precision = cint(frappe.db.get_default("float_precision")) or 2
for data in supplier_quotation_data:
supplier = data.get("supplier")
supplier_currency = frappe.db.get_value("Supplier", data.get("supplier"), "default_currency")
group = data.get(group_by_field) # get item or supplier value for this row
supplier_currency = frappe.db.get_value("Supplier", data.get("supplier_name"), "default_currency")
if supplier_currency:
exchange_rate = get_exchange_rate(supplier_currency, company_currency)
@ -75,38 +81,55 @@ def prepare_data(supplier_quotation_data, filters):
exchange_rate = 1
row = {
"item_code": data.get('item_code'),
"item_code": "" if group_by_field=="item_code" else data.get("item_code"), # leave blank if group by field
"supplier_name": "" if group_by_field=="supplier_name" else data.get("supplier_name"),
"quotation": data.get("parent"),
"qty": data.get("qty"),
"price": flt(data.get("rate") * exchange_rate, float_precision),
"price": flt(data.get("amount") * exchange_rate, float_precision),
"uom": data.get("uom"),
"stock_uom": data.get('stock_uom'),
"request_for_quotation": data.get("request_for_quotation"),
"valid_till": data.get('valid_till'),
"lead_time_days": data.get('lead_time_days')
}
row["price_per_unit"] = flt(row["price"]) / (flt(data.get("stock_qty")) or 1)
# map for report view of form {'supplier1':[{},{},...]}
supplier_wise_map[supplier].append(row)
# map for report view of form {'supplier1'/'item1':[{},{},...]}
group_wise_map[group].append(row)
# map for chart preparation of the form {'supplier1': {'qty': 'price'}}
supplier = data.get("supplier_name")
if filters.get("item_code"):
if not supplier in supplier_qty_price_map:
supplier_qty_price_map[supplier] = {}
supplier_qty_price_map[supplier][row["qty"]] = row["price"]
groups.append(group)
suppliers.append(supplier)
qty_list.append(data.get("qty"))
groups = list(set(groups))
suppliers = list(set(suppliers))
qty_list = list(set(qty_list))
highlight_min_price = group_by_field == "item_code" or filters.get("item_code")
# final data format for report view
for supplier in suppliers:
supplier_wise_map[supplier][0].update({"supplier_name": supplier})
for entry in supplier_wise_map[supplier]:
for group in groups:
group_entries = group_wise_map[group] # all entries pertaining to item/supplier
group_entries[0].update({group_by_field : group}) # Add item/supplier name in first group row
if highlight_min_price:
prices = [group_entry["price_per_unit"] for group_entry in group_entries]
min_price = min(prices)
for entry in group_entries:
if highlight_min_price and entry["price_per_unit"] == min_price:
entry["min"] = 1
out.append(entry)
if filters.get("item_code"):
# render chart only for one item comparison
chart_data = prepare_chart_data(suppliers, qty_list, supplier_qty_price_map)
return out, chart_data
@ -145,8 +168,9 @@ def prepare_chart_data(suppliers, qty_list, supplier_qty_price_map):
return chart_data
def get_columns():
columns = [{
def get_columns(filters):
group_by_columns = [
{
"fieldname": "supplier_name",
"label": _("Supplier"),
"fieldtype": "Link",
@ -158,8 +182,10 @@ def get_columns():
"label": _("Item"),
"fieldtype": "Link",
"options": "Item",
"width": 200
},
"width": 150
}]
columns = [
{
"fieldname": "uom",
"label": _("UOM"),
@ -180,6 +206,20 @@ def get_columns():
"options": "Company:company:default_currency",
"width": 110
},
{
"fieldname": "stock_uom",
"label": _("Stock UOM"),
"fieldtype": "Link",
"options": "UOM",
"width": 90
},
{
"fieldname": "price_per_unit",
"label": _("Price per Unit (Stock UOM)"),
"fieldtype": "Currency",
"options": "Company:company:default_currency",
"width": 120
},
{
"fieldname": "quotation",
"label": _("Supplier Quotation"),
@ -205,9 +245,12 @@ def get_columns():
"fieldtype": "Link",
"options": "Request for Quotation",
"width": 150
}
]
}]
if filters.get("group_by") == "Group by Item":
group_by_columns.reverse()
columns[0:0] = group_by_columns # add positioned group by columns to the report
return columns
def get_message():

View File

@ -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
},
{

View File

@ -1242,7 +1242,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
try:
doc.check_permission(perm_type)
except frappe.PermissionError:
actions = { 'create': 'add', 'write': 'update', 'cancel': 'remove' }
actions = { 'create': 'add', 'write': 'update'}
frappe.throw(_("You do not have permissions to {} items in a {}.")
.format(actions[perm_type], parent_doctype), title=_("Insufficient Permissions"))
@ -1285,7 +1285,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
sales_doctypes = ['Sales Order', 'Sales Invoice', 'Delivery Note', 'Quotation']
parent = frappe.get_doc(parent_doctype, parent_doctype_name)
check_doc_permissions(parent, 'cancel')
check_doc_permissions(parent, 'write')
validate_and_delete_children(parent, data)
for d in data:
@ -1319,24 +1319,26 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
validate_quantity(child_item, d)
child_item.qty = flt(d.get("qty"))
precision = child_item.precision("rate") or 2
rate_precision = child_item.precision("rate") or 2
conv_fac_precision = child_item.precision("conversion_factor") or 2
qty_precision = child_item.precision("qty") or 2
if flt(child_item.billed_amt, precision) > flt(flt(d.get("rate")) * flt(d.get("qty")), precision):
if flt(child_item.billed_amt, rate_precision) > flt(flt(d.get("rate"), rate_precision) * flt(d.get("qty"), qty_precision), rate_precision):
frappe.throw(_("Row #{0}: Cannot set Rate if amount is greater than billed amount for Item {1}.")
.format(child_item.idx, child_item.item_code))
else:
child_item.rate = flt(d.get("rate"))
child_item.rate = flt(d.get("rate"), rate_precision)
if d.get("conversion_factor"):
if child_item.stock_uom == child_item.uom:
child_item.conversion_factor = 1
else:
child_item.conversion_factor = flt(d.get('conversion_factor'))
child_item.conversion_factor = flt(d.get('conversion_factor'), conv_fac_precision)
if d.get("uom"):
child_item.uom = d.get("uom")
conversion_factor = flt(get_conversion_factor(child_item.item_code, child_item.uom).get("conversion_factor"))
child_item.conversion_factor = flt(d.get('conversion_factor')) or conversion_factor
child_item.conversion_factor = flt(d.get('conversion_factor'), conv_fac_precision) or conversion_factor
if d.get("delivery_date") and parent_doctype == 'Sales Order':
child_item.delivery_date = d.get('delivery_date')

View File

@ -847,6 +847,7 @@ def get_items_from_bom(item_code, bom, exploded_item=1):
where
t2.parent = t1.name and t1.item = %s
and t1.docstatus = 1 and t1.is_active = 1 and t1.name = %s
and t2.sourced_by_supplier = 0
and t2.item_code = t3.name""".format(doctype),
(item_code, bom), as_dict=1)

View File

@ -368,13 +368,17 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters):
searchfields = meta.get_search_fields()
search_columns = ''
search_cond = ''
if searchfields:
search_columns = ", " + ", ".join(searchfields)
search_cond = " or " + " or ".join([field + " like %(txt)s" for field in searchfields])
if args.get('warehouse'):
searchfields = ['batch.' + field for field in searchfields]
if searchfields:
search_columns = ", " + ", ".join(searchfields)
search_cond = " or " + " or ".join([field + " like %(txt)s" for field in searchfields])
batch_nos = frappe.db.sql("""select sle.batch_no, round(sum(sle.actual_qty),2), sle.stock_uom,
concat('MFG-',batch.manufacturing_date), concat('EXP-',batch.expiry_date)
@ -387,7 +391,8 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters):
and sle.warehouse = %(warehouse)s
and (sle.batch_no like %(txt)s
or batch.expiry_date like %(txt)s
or batch.manufacturing_date like %(txt)s)
or batch.manufacturing_date like %(txt)s
{search_cond})
and batch.docstatus < 2
{cond}
{match_conditions}
@ -397,7 +402,8 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters):
search_columns = search_columns,
cond=cond,
match_conditions=get_match_cond(doctype),
having_clause = having_clause
having_clause = having_clause,
search_cond = search_cond
), args)
return batch_nos
@ -409,12 +415,15 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters):
and item = %(item_code)s
and (name like %(txt)s
or expiry_date like %(txt)s
or manufacturing_date like %(txt)s)
or manufacturing_date like %(txt)s
{search_cond})
and docstatus < 2
{0}
{match_conditions}
order by expiry_date, name desc
limit %(start)s, %(page_len)s""".format(cond, search_columns = search_columns, match_conditions=get_match_cond(doctype)), args)
limit %(start)s, %(page_len)s""".format(cond, search_columns = search_columns,
search_cond = search_cond, match_conditions=get_match_cond(doctype)), args)
@frappe.whitelist()

View File

@ -3,13 +3,14 @@
from __future__ import unicode_literals
import frappe
from frappe.utils import cint, flt, cstr, comma_or
from frappe.utils import cint, flt, cstr, comma_or, get_link_to_form
from frappe import _, throw
from erpnext.stock.get_item_details import get_bin_details
from erpnext.stock.utils import get_incoming_rate
from erpnext.stock.get_item_details import get_conversion_factor
from erpnext.stock.doctype.item.item import set_item_default
from frappe.contacts.doctype.address.address import get_address_display
from erpnext.controllers.accounts_controller import get_taxes_and_charges
from erpnext.controllers.stock_controller import StockController
@ -53,10 +54,10 @@ class SellingController(StockController):
super(SellingController, self).set_missing_values(for_validate)
# set contact and address details for customer, if they are not mentioned
self.set_missing_lead_customer_details()
self.set_missing_lead_customer_details(for_validate=for_validate)
self.set_price_list_and_item_details(for_validate=for_validate)
def set_missing_lead_customer_details(self):
def set_missing_lead_customer_details(self, for_validate=False):
customer, lead = None, None
if getattr(self, "customer", None):
customer = self.customer
@ -94,6 +95,11 @@ class SellingController(StockController):
posting_date=self.get('transaction_date') or self.get('posting_date'),
company=self.company))
if self.get('taxes_and_charges') and not self.get('taxes') and not for_validate:
taxes = get_taxes_and_charges('Sales Taxes and Charges Template', self.taxes_and_charges)
for tax in taxes:
self.append('taxes', tax)
def set_price_list_and_item_details(self, for_validate=False):
self.set_price_list_currency("Selling")
self.set_missing_item_details(for_validate=for_validate)
@ -167,12 +173,16 @@ class SellingController(StockController):
def validate_selling_price(self):
def throw_message(idx, item_name, rate, ref_rate_field):
frappe.throw(_("""Row #{}: Selling rate for item {} is lower than its {}. Selling rate should be atleast {}""")
.format(idx, item_name, ref_rate_field, rate))
bold_net_rate = frappe.bold("net rate")
msg = (_("""Row #{}: Selling rate for item {} is lower than its {}. Selling {} should be atleast {}""")
.format(idx, frappe.bold(item_name), frappe.bold(ref_rate_field), bold_net_rate, frappe.bold(rate)))
msg += "<br><br>"
msg += (_("""You can alternatively disable selling price validation in {} to bypass this validation.""")
.format(get_link_to_form("Selling Settings", "Selling Settings")))
frappe.throw(msg, title=_("Invalid Selling Price"))
if not frappe.db.get_single_value("Selling Settings", "validate_selling_price"):
return
if hasattr(self, "is_return") and self.is_return:
return
@ -181,8 +191,8 @@ class SellingController(StockController):
continue
last_purchase_rate, is_stock_item = frappe.get_cached_value("Item", it.item_code, ["last_purchase_rate", "is_stock_item"])
last_purchase_rate_in_sales_uom = last_purchase_rate / (it.conversion_factor or 1)
if flt(it.base_rate) < flt(last_purchase_rate_in_sales_uom):
last_purchase_rate_in_sales_uom = last_purchase_rate * (it.conversion_factor or 1)
if flt(it.base_net_rate) < flt(last_purchase_rate_in_sales_uom):
throw_message(it.idx, frappe.bold(it.item_name), last_purchase_rate_in_sales_uom, "last purchase rate")
last_valuation_rate = frappe.db.sql("""
@ -191,8 +201,8 @@ class SellingController(StockController):
ORDER BY posting_date DESC, posting_time DESC, creation DESC LIMIT 1
""", (it.item_code, it.warehouse))
if last_valuation_rate:
last_valuation_rate_in_sales_uom = last_valuation_rate[0][0] / (it.conversion_factor or 1)
if is_stock_item and flt(it.base_rate) < flt(last_valuation_rate_in_sales_uom) \
last_valuation_rate_in_sales_uom = last_valuation_rate[0][0] * (it.conversion_factor or 1)
if is_stock_item and flt(it.base_net_rate) < flt(last_valuation_rate_in_sales_uom) \
and not self.get('is_internal_customer'):
throw_message(it.idx, frappe.bold(it.item_name), last_valuation_rate_in_sales_uom, "valuation rate")

View File

@ -25,7 +25,7 @@ def get_transaction_list(doctype, txt=None, filters=None, limit_start=0, limit_p
if not filters: filters = []
if doctype in ['Supplier Quotation', 'Purchase Invoice']:
if doctype in ['Supplier Quotation', 'Purchase Invoice', 'Quotation']:
filters.append((doctype, 'docstatus', '<', 2))
else:
filters.append((doctype, 'docstatus', '=', 1))

View File

@ -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",

View File

@ -22,14 +22,12 @@ class Lead(SellingController):
load_address_and_contact(self)
def before_insert(self):
if self.address_title and self.address_type:
self.address_doc = self.create_address()
self.contact_doc = self.create_contact()
def after_insert(self):
self.update_links()
# after the address and contact are created, flush the field values
# to avoid inconsistent reporting in case the documents are changed
self.flush_address_and_contact_fields()
def validate(self):
self.set_lead_name()
@ -136,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()
@ -193,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,
@ -210,14 +199,6 @@ class Lead(SellingController):
})
self.contact_doc.save()
def flush_address_and_contact_fields(self):
fields = ['address_type', 'address_line1', 'address_line2', 'address_title',
'city', 'county', 'country', 'fax', 'pincode', 'state']
for field in fields:
self.set(field, None)
@frappe.whitelist()
def make_customer(source_name, target_doc=None):
return _make_customer(source_name, target_doc)
@ -376,3 +357,8 @@ def get_lead_with_phone_number(number):
lead = leads[0].name if leads else None
return lead
def daily_open_lead():
leads = frappe.get_all("Lead", filters = [["contact_date", "Between", [nowdate(), nowdate()]]])
for lead in leads:
frappe.db.set_value("Lead", lead.name, "status", "Open")

View File

@ -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()

View File

@ -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()}

View File

@ -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()

View File

@ -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

View File

@ -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",

View File

@ -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]] };

View File

@ -1,725 +1,185 @@
{
"allow_copy": 0,
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"actions": [],
"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",
"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
"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
"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
"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
"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
"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
"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
"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
"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
"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
"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
"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
"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
"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
"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
"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
"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
"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
"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": ""
"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
"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
"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",
"links": [],
"modified": "2020-09-15 18:12:11.988565",
"modified_by": "Administrator",
"module": "Education",
"name": "Program Enrollment",
"name_case": "",
"owner": "Administrator",
"permissions": [
{
@ -729,47 +189,30 @@
"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,
"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,
"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
"title_field": "student_name"
}

View File

@ -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})

View File

@ -24,3 +24,12 @@ class TestProgramEnrollment(unittest.TestCase):
self.assertTrue("_Test Course 1" in course_enrollments.keys())
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()

View File

@ -13,7 +13,7 @@
"fields": [
{
"fieldname": "question",
"fieldtype": "Small Text",
"fieldtype": "Text Editor",
"in_list_view": 1,
"label": "Question",
"reqd": 1
@ -34,7 +34,7 @@
"read_only": 1
}
],
"modified": "2019-05-30 18:39:21.880974",
"modified": "2020-09-24 18:39:21.880974",
"modified_by": "Administrator",
"module": "Education",
"name": "Question",

View File

@ -20,14 +20,14 @@
{
"fetch_from": "question_link.question",
"fieldname": "question",
"fieldtype": "Data",
"fieldtype": "Text Editor",
"in_list_view": 1,
"label": "Question",
"read_only": 1
}
],
"istable": 1,
"modified": "2019-06-12 12:24:02.312577",
"modified": "2020-09-24 12:24:02.312577",
"modified_by": "Administrator",
"module": "Education",
"name": "Quiz Question",

View File

@ -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

View File

@ -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:

View File

@ -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",

View File

@ -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

View File

@ -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 %}

Some files were not shown because too many files have changed in this diff Show More