Merge branch 'develop' of https://github.com/frappe/erpnext into stock_entry_conversion_factor

This commit is contained in:
Deepesh Garg 2020-10-15 17:13:29 +05:30
commit 01a7297f69
181 changed files with 3103 additions and 3417 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

@ -53,7 +53,7 @@
{ {
"hidden": 0, "hidden": 0,
"label": "Goods and Services Tax (GST India)", "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, "hidden": 0,
@ -98,7 +98,7 @@
"idx": 0, "idx": 0,
"is_standard": 1, "is_standard": 1,
"label": "Accounting", "label": "Accounting",
"modified": "2020-09-09 11:45:33.766400", "modified": "2020-10-08 20:31:46.022470",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Accounting", "name": "Accounting",
@ -108,7 +108,7 @@
"pin_to_top": 0, "pin_to_top": 0,
"shortcuts": [ "shortcuts": [
{ {
"label": "Chart Of Accounts", "label": "Chart of Accounts",
"link_to": "Account", "link_to": "Account",
"type": "DocType" "type": "DocType"
}, },

View File

@ -2,7 +2,7 @@ frappe.provide("frappe.treeview_settings")
frappe.treeview_settings["Account"] = { frappe.treeview_settings["Account"] = {
breadcrumb: "Accounts", breadcrumb: "Accounts",
title: __("Chart Of Accounts"), title: __("Chart of Accounts"),
get_tree_root: false, get_tree_root: false,
filters: [ filters: [
{ {
@ -97,7 +97,7 @@ frappe.treeview_settings["Account"] = {
treeview.page.add_inner_button(__("Journal Entry"), function() { treeview.page.add_inner_button(__("Journal Entry"), function() {
frappe.new_doc('Journal Entry', {company: get_company()}); frappe.new_doc('Journal Entry', {company: get_company()});
}, __('Create')); }, __('Create'));
treeview.page.add_inner_button(__("New Company"), function() { treeview.page.add_inner_button(__("Company"), function() {
frappe.new_doc('Company'); frappe.new_doc('Company');
}, __('Create')); }, __('Create'));

File diff suppressed because it is too large Load Diff

View File

@ -195,7 +195,7 @@ def build_response_as_excel(writer):
reader = csv.reader(f) reader = csv.reader(f)
from frappe.utils.xlsxutils import make_xlsx from frappe.utils.xlsxutils import make_xlsx
xlsx_file = make_xlsx(reader, "Chart Of Accounts Importer Template") xlsx_file = make_xlsx(reader, "Chart of Accounts Importer Template")
f.close() f.close()
os.remove(filename) os.remove(filename)

View File

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

View File

@ -195,88 +195,91 @@ def create_sales_invoice_record(qty=1):
def create_records(): def create_records():
# create a new loyalty Account # create a new loyalty Account
if frappe.db.exists("Account", "Loyalty - _TC"): if not frappe.db.exists("Account", "Loyalty - _TC"):
return frappe.get_doc({
"doctype": "Account",
frappe.get_doc({ "account_name": "Loyalty",
"doctype": "Account", "parent_account": "Direct Expenses - _TC",
"account_name": "Loyalty", "company": "_Test Company",
"parent_account": "Direct Expenses - _TC", "is_group": 0,
"company": "_Test Company", "account_type": "Expense Account",
"is_group": 0, }).insert()
"account_type": "Expense Account",
}).insert()
# create a new loyalty program Single tier # create a new loyalty program Single tier
frappe.get_doc({ if not frappe.db.exists("Loyalty Program","Test Single Loyalty"):
"doctype": "Loyalty Program", frappe.get_doc({
"loyalty_program_name": "Test Single Loyalty", "doctype": "Loyalty Program",
"auto_opt_in": 1, "loyalty_program_name": "Test Single Loyalty",
"from_date": today(), "auto_opt_in": 1,
"loyalty_program_type": "Single Tier Program", "from_date": today(),
"conversion_factor": 1, "loyalty_program_type": "Single Tier Program",
"expiry_duration": 10, "conversion_factor": 1,
"company": "_Test Company", "expiry_duration": 10,
"cost_center": "Main - _TC", "company": "_Test Company",
"expense_account": "Loyalty - _TC", "cost_center": "Main - _TC",
"collection_rules": [{ "expense_account": "Loyalty - _TC",
'tier_name': 'Silver', "collection_rules": [{
'collection_factor': 1000,
'min_spent': 1000
}]
}).insert()
# create a new customer
frappe.get_doc({
"customer_group": "_Test Customer Group",
"customer_name": "Test Loyalty Customer",
"customer_type": "Individual",
"doctype": "Customer",
"territory": "_Test Territory"
}).insert()
# create a new loyalty program Multiple tier
frappe.get_doc({
"doctype": "Loyalty Program",
"loyalty_program_name": "Test Multiple Loyalty",
"auto_opt_in": 1,
"from_date": today(),
"loyalty_program_type": "Multiple Tier Program",
"conversion_factor": 1,
"expiry_duration": 10,
"company": "_Test Company",
"cost_center": "Main - _TC",
"expense_account": "Loyalty - _TC",
"collection_rules": [
{
'tier_name': 'Silver', 'tier_name': 'Silver',
'collection_factor': 1000, 'collection_factor': 1000,
'min_spent': 10000 'min_spent': 1000
}, }]
{ }).insert()
'tier_name': 'Gold',
'collection_factor': 1000, # create a new customer
'min_spent': 19000 if not frappe.db.exists("Customer","Test Loyalty Customer"):
} frappe.get_doc({
] "customer_group": "_Test Customer Group",
}).insert() "customer_name": "Test Loyalty Customer",
"customer_type": "Individual",
"doctype": "Customer",
"territory": "_Test Territory"
}).insert()
# create a new loyalty program Multiple tier
if not frappe.db.exists("Loyalty Program","Test Multiple Loyalty"):
frappe.get_doc({
"doctype": "Loyalty Program",
"loyalty_program_name": "Test Multiple Loyalty",
"auto_opt_in": 1,
"from_date": today(),
"loyalty_program_type": "Multiple Tier Program",
"conversion_factor": 1,
"expiry_duration": 10,
"company": "_Test Company",
"cost_center": "Main - _TC",
"expense_account": "Loyalty - _TC",
"collection_rules": [
{
'tier_name': 'Silver',
'collection_factor': 1000,
'min_spent': 10000
},
{
'tier_name': 'Gold',
'collection_factor': 1000,
'min_spent': 19000
}
]
}).insert()
# create an item # create an item
item = frappe.get_doc({ if not frappe.db.exists("Item", "Loyal Item"):
"doctype": "Item", frappe.get_doc({
"item_code": "Loyal Item", "doctype": "Item",
"item_name": "Loyal Item", "item_code": "Loyal Item",
"item_group": "All Item Groups", "item_name": "Loyal Item",
"company": "_Test Company", "item_group": "All Item Groups",
"is_stock_item": 1, "company": "_Test Company",
"opening_stock": 100, "is_stock_item": 1,
"valuation_rate": 10000, "opening_stock": 100,
}).insert() "valuation_rate": 10000,
}).insert()
# create item price # create item price
frappe.get_doc({ if not frappe.db.exists("Item Price", {"price_list": "Standard Selling", "item_code": "Loyal Item"}):
"doctype": "Item Price", frappe.get_doc({
"price_list": "Standard Selling", "doctype": "Item Price",
"item_code": item.item_code, "price_list": "Standard Selling",
"price_list_rate": 10000 "item_code": "Loyal Item",
}).insert() "price_list_rate": 10000
}).insert()

View File

@ -1,12 +1,14 @@
frappe.listview_settings['Payment Entry'] = { frappe.listview_settings['Payment Entry'] = {
onload: function(listview) { onload: function(listview) {
listview.page.fields_dict.party_type.get_query = function() { if (listview.page.fields_dict.party_type) {
return { listview.page.fields_dict.party_type.get_query = function() {
"filters": { return {
"name": ["in", Object.keys(frappe.boot.party_account_types)], "filters": {
} "name": ["in", Object.keys(frappe.boot.party_account_types)],
}
};
}; };
}; }
} }
}; };

View File

@ -99,6 +99,7 @@ class PaymentReconciliation(Document):
and `tabGL Entry`.against_voucher_type = %(voucher_type)s and `tabGL Entry`.against_voucher_type = %(voucher_type)s
and `tab{doc}`.docstatus = 1 and `tabGL Entry`.party = %(party)s and `tab{doc}`.docstatus = 1 and `tabGL Entry`.party = %(party)s
and `tabGL Entry`.party_type = %(party_type)s and `tabGL Entry`.account = %(account)s and `tabGL Entry`.party_type = %(party_type)s and `tabGL Entry`.account = %(account)s
and `tabGL Entry`.is_cancelled = 0
GROUP BY `tab{doc}`.name GROUP BY `tab{doc}`.name
Having Having
amount > 0 amount > 0

View File

@ -45,6 +45,7 @@
"unique": 0 "unique": 0
}, },
{ {
"description": "Provide the invoice portion in percent",
"allow_bulk_edit": 0, "allow_bulk_edit": 0,
"allow_on_submit": 0, "allow_on_submit": 0,
"bold": 1, "bold": 1,
@ -170,6 +171,7 @@
"unique": 0 "unique": 0
}, },
{ {
"description": "Give number of days according to prior selection",
"allow_bulk_edit": 0, "allow_bulk_edit": 0,
"allow_on_submit": 0, "allow_on_submit": 0,
"bold": 1, "bold": 1,
@ -305,7 +307,7 @@
"issingle": 0, "issingle": 0,
"istable": 0, "istable": 0,
"max_attachments": 0, "max_attachments": 0,
"modified": "2018-03-08 10:47:32.830478", "modified": "2020-10-14 10:47:32.830478",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Payment Term", "name": "Payment Term",
@ -381,4 +383,4 @@
"sort_order": "DESC", "sort_order": "DESC",
"track_changes": 1, "track_changes": 1,
"track_seen": 0 "track_seen": 0
} }

View File

@ -45,7 +45,7 @@ class TestPOSClosingEntry(unittest.TestCase):
frappe.set_user("Administrator") frappe.set_user("Administrator")
frappe.db.sql("delete from `tabPOS Profile`") frappe.db.sql("delete from `tabPOS Profile`")
def init_user_and_profile(): def init_user_and_profile(**args):
user = 'test@example.com' user = 'test@example.com'
test_user = frappe.get_doc('User', user) test_user = frappe.get_doc('User', user)
@ -53,7 +53,7 @@ def init_user_and_profile():
test_user.add_roles(*roles) test_user.add_roles(*roles)
frappe.set_user(user) frappe.set_user(user)
pos_profile = make_pos_profile() pos_profile = make_pos_profile(**args)
pos_profile.append('applicable_for_users', { pos_profile.append('applicable_for_users', {
'default': 1, 'default': 1,
'user': user 'user': user

View File

@ -139,7 +139,8 @@ class POSInvoice(SalesInvoice):
frappe.throw(_("At least one mode of payment is required for POS invoice.")) frappe.throw(_("At least one mode of payment is required for POS invoice."))
def validate_change_account(self): 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)) frappe.throw(_("The selected change account {} doesn't belongs to Company {}.").format(self.account_for_change_amount, self.company))
def validate_change_amount(self): def validate_change_amount(self):

View File

@ -7,6 +7,8 @@ import frappe
import unittest, copy, time import unittest, copy, time
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile 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.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): class TestPOSInvoice(unittest.TestCase):
def test_timestamp_change(self): 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.stock_entry.test_stock_entry import make_serialized_item
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
se = make_serialized_item(company='_Test Company with perpetual inventory', se = make_serialized_item(company='_Test Company',
target_warehouse="Stores - TCP1", cost_center='Main - TCP1', expense_account='Cost of Goods Sold - TCP1') 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) 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', pos = create_pos_invoice(company='_Test Company', debit_to='Debtors - _TC',
account_for_change_amount='Cash - TCP1', warehouse='Stores - TCP1', income_account='Sales - TCP1', account_for_change_amount='Cash - _TC', warehouse='Stores - _TC', income_account='Sales - _TC',
expense_account='Cost of Goods Sold - TCP1', cost_center='Main - TCP1', expense_account='Cost of Goods Sold - _TC', cost_center='Main - _TC',
item=se.get("items")[0].item_code, rate=1000, do_not_save=1) item=se.get("items")[0].item_code, rate=1000, do_not_save=1)
pos.get("items")[0].serial_no = serial_nos[0] 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.insert()
pos.submit() pos.submit()
pos2 = create_pos_invoice(company='_Test Company with perpetual inventory', debit_to='Debtors - TCP1', pos2 = create_pos_invoice(company='_Test Company', debit_to='Debtors - _TC',
account_for_change_amount='Cash - TCP1', warehouse='Stores - TCP1', income_account='Sales - TCP1', account_for_change_amount='Cash - _TC', warehouse='Stores - _TC', income_account='Sales - _TC',
expense_account='Cost of Goods Sold - TCP1', cost_center='Main - TCP1', expense_account='Cost of Goods Sold - _TC', cost_center='Main - _TC',
item=se.get("items")[0].item_code, rate=1000, do_not_save=1) item=se.get("items")[0].item_code, rate=1000, do_not_save=1)
pos2.get("items")[0].serial_no = serial_nos[0] 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) self.assertRaises(frappe.ValidationError, pos2.insert)
@ -285,7 +287,7 @@ class TestPOSInvoice(unittest.TestCase):
after_redeem_lp_details = get_loyalty_program_details_with_points(inv.customer, company=inv.company, loyalty_program=inv.loyalty_program) 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) self.assertEqual(after_redeem_lp_details.loyalty_points, 9)
def test_merging_into_sales_invoice_with_discount(self): 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_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 from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import merge_pos_invoices
@ -294,7 +296,7 @@ class TestPOSInvoice(unittest.TestCase):
test_user, pos_profile = init_user_and_profile() 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 = create_pos_invoice(rate=300, additional_discount_percentage=10, do_not_submit=1)
pos_inv.append('payments', { pos_inv.append('payments', {
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 300 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 270
}) })
pos_inv.submit() pos_inv.submit()
@ -307,9 +309,10 @@ class TestPOSInvoice(unittest.TestCase):
merge_pos_invoices() merge_pos_invoices()
pos_inv.load_from_db() pos_inv.load_from_db()
sales_invoice = frappe.get_doc("Sales Invoice", pos_inv.consolidated_invoice) rounded_total = frappe.db.get_value("Sales Invoice", pos_inv.consolidated_invoice, "rounded_total")
self.assertEqual(sales_invoice.grand_total, 3500) self.assertEqual(rounded_total, 3470)
frappe.set_user("Administrator")
def test_merging_into_sales_invoice_with_discount_and_inclusive_tax(self): 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_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 from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import merge_pos_invoices
@ -348,8 +351,55 @@ class TestPOSInvoice(unittest.TestCase):
merge_pos_invoices() merge_pos_invoices()
pos_inv.load_from_db() pos_inv.load_from_db()
sales_invoice = frappe.get_doc("Sales Invoice", pos_inv.consolidated_invoice) rounded_total = frappe.db.get_value("Sales Invoice", pos_inv.consolidated_invoice, "rounded_total")
self.assertEqual(sales_invoice.rounded_total, 840) 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): def create_pos_invoice(**args):
args = frappe._dict(args) args = frappe._dict(args)
@ -364,8 +414,6 @@ def create_pos_invoice(**args):
pos_inv.is_pos = 1 pos_inv.is_pos = 1
pos_inv.pos_profile = args.pos_profile or pos_profile.name pos_inv.pos_profile = args.pos_profile or pos_profile.name
pos_inv.set_missing_values()
if args.posting_date: if args.posting_date:
pos_inv.set_posting_time = 1 pos_inv.set_posting_time = 1
pos_inv.posting_date = args.posting_date or frappe.utils.nowdate() pos_inv.posting_date = args.posting_date or frappe.utils.nowdate()
@ -379,6 +427,8 @@ def create_pos_invoice(**args):
pos_inv.conversion_rate = args.conversion_rate or 1 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.account_for_change_amount = args.account_for_change_amount or "Cash - _TC"
pos_inv.set_missing_values()
pos_inv.append("items", { pos_inv.append("items", {
"item_code": args.item or args.item_code or "_Test Item", "item_code": args.item or args.item_code or "_Test Item",
"warehouse": args.warehouse or "_Test Warehouse - _TC", "warehouse": args.warehouse or "_Test Warehouse - _TC",

View File

@ -15,15 +15,6 @@ frappe.ui.form.on("POS Profile", "onload", function(frm) {
erpnext.queries.setup_queries(frm, "Warehouse", function() { erpnext.queries.setup_queries(frm, "Warehouse", function() {
return erpnext.queries.warehouse(frm.doc); 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', { frappe.ui.form.on('POS Profile', {

View File

@ -8,7 +8,6 @@
"field_order": [ "field_order": [
"disabled", "disabled",
"section_break_2", "section_break_2",
"naming_series",
"customer", "customer",
"company", "company",
"country", "country",
@ -59,17 +58,6 @@
"fieldname": "section_break_2", "fieldname": "section_break_2",
"fieldtype": "Section Break" "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", "fieldname": "customer",
"fieldtype": "Link", "fieldtype": "Link",
@ -323,7 +311,7 @@
"icon": "icon-cog", "icon": "icon-cog",
"idx": 1, "idx": 1,
"links": [], "links": [],
"modified": "2020-06-29 12:20:30.977272", "modified": "2020-10-01 17:29:27.759088",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "POS Profile", "name": "POS Profile",
@ -350,4 +338,4 @@
], ],
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC" "sort_order": "DESC"
} }

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 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) 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.whitelist()
@frappe.validate_and_sanitize_search_inputs @frappe.validate_and_sanitize_search_inputs
def pos_profile_query(doctype, txt, searchfield, start, page_len, filters): def pos_profile_query(doctype, txt, searchfield, start, page_len, filters):

View File

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

View File

@ -6,9 +6,10 @@ from __future__ import unicode_literals
import frappe import frappe
import json import json
import copy import copy
import re
from frappe import throw, _ from frappe import throw, _
from frappe.utils import flt, cint, getdate from frappe.utils import flt, cint, getdate
from frappe.model.document import Document from frappe.model.document import Document
from six import string_types from six import string_types
@ -30,6 +31,7 @@ class PricingRule(Document):
self.validate_max_discount() self.validate_max_discount()
self.validate_price_list_with_currency() self.validate_price_list_with_currency()
self.validate_dates() self.validate_dates()
self.validate_condition()
if not self.margin_type: self.margin_rate_or_amount = 0.0 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): 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")) 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() @frappe.whitelist()

View File

@ -429,7 +429,34 @@ class TestPricingRule(unittest.TestCase):
details = get_item_details(args) details = get_item_details(args)
self.assertTrue(details) 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): def make_pricing_rule(**args):
args = frappe._dict(args) args = frappe._dict(args)
@ -448,7 +475,8 @@ def make_pricing_rule(**args):
"discount_percentage": args.discount_percentage or 0.0, "discount_percentage": args.discount_percentage or 0.0,
"rate": args.rate or 0.0, "rate": args.rate or 0.0,
"margin_type": args.margin_type, "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() apply_on = doc.apply_on.replace(' ', '_').lower()

View File

@ -37,6 +37,8 @@ def get_pricing_rules(args, doc=None):
rules = [] rules = []
pricing_rules = filter_pricing_rule_based_on_condition(pricing_rules, doc)
if not pricing_rules: return [] if not pricing_rules: return []
if apply_multiple_pricing_rules(pricing_rules): if apply_multiple_pricing_rules(pricing_rules):
@ -51,6 +53,23 @@ def get_pricing_rules(args, doc=None):
return rules 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): def _get_pricing_rules(apply_on, args, values):
apply_on_field = frappe.scrub(apply_on) apply_on_field = frappe.scrub(apply_on)

View File

@ -711,7 +711,8 @@ class PurchaseInvoice(BuyingController):
item.item_tax_amount / self.conversion_rate) item.item_tax_amount / self.conversion_rate)
}, item=item)) }, item=item))
else: 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) cwip_account_currency = get_account_currency(cwip_account)
gl_entries.append(self.get_gl_dict({ 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", "cost_center": args.cost_center or "_Test Cost Center - _TC",
"project": args.project, "project": args.project,
"rejected_warehouse": args.rejected_warehouse or "", "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: if args.get_taxes_and_charges:

View File

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

View File

@ -428,7 +428,7 @@ class SalesInvoice(SellingController):
if pos.get('account_for_change_amount'): if pos.get('account_for_change_amount'):
self.account_for_change_amount = 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', 'company', 'select_print_heading', 'write_off_account', 'taxes_and_charges',
'write_off_cost_center', 'apply_discount_on', 'cost_center'): 'write_off_cost_center', 'apply_discount_on', 'cost_center'):
if (not for_validate) or (for_validate and not self.get(fieldname)): if (not for_validate) or (for_validate and not self.get(fieldname)):
@ -572,7 +572,8 @@ class SalesInvoice(SellingController):
def validate_pos(self): def validate_pos(self):
if self.is_return: if self.is_return:
if flt(self.paid_amount) + flt(self.write_off_amount) - flt(self.grand_total) > \ invoice_total = self.rounded_total or self.grand_total
if flt(self.paid_amount) + flt(self.write_off_amount) - flt(invoice_total) > \
1.0/(10.0**(self.precision("grand_total") + 1.0)): 1.0/(10.0**(self.precision("grand_total") + 1.0)):
frappe.throw(_("Paid amount + Write Off Amount can not be greater than Grand Total")) frappe.throw(_("Paid amount + Write Off Amount can not be greater than Grand Total"))

View File

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

View File

@ -345,13 +345,14 @@ class Subscription(Document):
invoice.set_taxes() invoice.set_taxes()
# Due date # Due date
invoice.append( if self.days_until_due:
'payment_schedule', invoice.append(
{ 'payment_schedule',
'due_date': add_days(invoice.posting_date, cint(self.days_until_due)), {
'invoice_portion': 100 'due_date': add_days(invoice.posting_date, cint(self.days_until_due)),
} 'invoice_portion': 100
) }
)
# Discounts # Discounts
if self.additional_discount_percentage: if self.additional_discount_percentage:

View File

@ -106,6 +106,7 @@ def get_tds_amount(suppliers, net_total, company, tax_details, fiscal_year_detai
from `tabGL Entry` from `tabGL Entry`
where company = %s and where company = %s and
party in %s and fiscal_year=%s and credit > 0 party in %s and fiscal_year=%s and credit > 0
and is_opening = 'No'
""", (company, tuple(suppliers), fiscal_year), as_dict=1) """, (company, tuple(suppliers), fiscal_year), as_dict=1)
vouchers = [d.voucher_no for d in entries] 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 select distinct voucher_no
from `tabGL Entry` from `tabGL Entry`
where party in %s and %s and debit > 0 where party in %s and %s and debit > 0
and is_opening = 'No'
""", (tuple(suppliers), condition)) or [] """, (tuple(suppliers), condition)) or []
def get_debit_note_amount(suppliers, year_start_date, year_end_date, company=None): 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") frappe.throw(_("Account: {0} can only be updated via Stock Transactions")
.format(account), StockAccountInvalidTransaction) .format(account), StockAccountInvalidTransaction)
elif account_bal != stock_bal: elif abs(account_bal - stock_bal) > 0.1:
precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"), precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"),
currency=frappe.get_cached_value('Company', gl_map[0].company, "default_currency")) currency=frappe.get_cached_value('Company', gl_map[0].company, "default_currency"))

View File

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

View File

@ -10,11 +10,11 @@
"is_skipped": 0, "is_skipped": 0,
"modified": "2020-05-14 17:40:28.410447", "modified": "2020-05-14 17:40:28.410447",
"modified_by": "Administrator", "modified_by": "Administrator",
"name": "Chart Of Accounts", "name": "Chart of Accounts",
"owner": "Administrator", "owner": "Administrator",
"path": "Tree/Account", "path": "Tree/Account",
"reference_document": "Account", "reference_document": "Account",
"show_full_form": 0, "show_full_form": 0,
"title": "Review Chart Of Accounts", "title": "Review Chart of Accounts",
"validate_action": 0 "validate_action": 0
} }

View File

@ -3,6 +3,14 @@
frappe.query_reports["Bank Reconciliation Statement"] = { frappe.query_reports["Bank Reconciliation Statement"] = {
"filters": [ "filters": [
{
"fieldname":"company",
"label": __("Company"),
"fieldtype": "Link",
"options": "Company",
"reqd": 1,
"default": frappe.defaults.get_user_default("Company")
},
{ {
"fieldname":"account", "fieldname":"account",
"label": __("Bank Account"), "label": __("Bank Account"),
@ -12,11 +20,14 @@ frappe.query_reports["Bank Reconciliation Statement"] = {
locals[":Company"][frappe.defaults.get_user_default("Company")]["default_bank_account"]: "", locals[":Company"][frappe.defaults.get_user_default("Company")]["default_bank_account"]: "",
"reqd": 1, "reqd": 1,
"get_query": function() { "get_query": function() {
var company = frappe.query_report.get_filter_value('company')
return { return {
"query": "erpnext.controllers.queries.get_account_list", "query": "erpnext.controllers.queries.get_account_list",
"filters": [ "filters": [
['Account', 'account_type', 'in', 'Bank, Cash'], ['Account', 'account_type', 'in', 'Bank, Cash'],
['Account', 'is_group', '=', 0], ['Account', 'is_group', '=', 0],
['Account', 'disabled', '=', 0],
['Account', 'company', '=', company],
] ]
} }
} }
@ -34,4 +45,4 @@ frappe.query_reports["Bank Reconciliation Statement"] = {
"fieldtype": "Check" "fieldtype": "Check"
}, },
] ]
} }

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

@ -131,7 +131,7 @@ class Asset(AccountsController):
def validate_gross_and_purchase_amount(self): def validate_gross_and_purchase_amount(self):
if self.is_existing_asset: return if self.is_existing_asset: return
if self.gross_purchase_amount and self.gross_purchase_amount != self.purchase_receipt_amount: if self.gross_purchase_amount and self.gross_purchase_amount != self.purchase_receipt_amount:
frappe.throw(_("Gross Purchase Amount should be {} to purchase amount of one single Asset. {}\ frappe.throw(_("Gross Purchase Amount should be {} to purchase amount of one single Asset. {}\
Please do not book expense of multiple assets against one single Asset.") Please do not book expense of multiple assets against one single Asset.")
@ -466,50 +466,63 @@ class Asset(AccountsController):
def validate_make_gl_entry(self): def validate_make_gl_entry(self):
purchase_document = self.get_purchase_document() purchase_document = self.get_purchase_document()
asset_bought_with_invoice = purchase_document == self.purchase_invoice if not purchase_document:
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
return False return False
elif not cwip_enabled and (not expense_booked or cwip_booked):
# if cwip is disabled but expense hasn't been booked yet asset_bought_with_invoice = (purchase_document == self.purchase_invoice)
return True fixed_asset_account = self.get_fixed_asset_account()
elif cwip_enabled:
# default condition cwip_enabled = is_cwip_accounting_enabled(self.asset_category)
return True 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): def get_purchase_document(self):
asset_bought_with_invoice = self.purchase_invoice and frappe.db.get_value('Purchase Invoice', self.purchase_invoice, 'update_stock') asset_bought_with_invoice = self.purchase_invoice and frappe.db.get_value('Purchase Invoice', self.purchase_invoice, 'update_stock')
purchase_document = self.purchase_invoice if asset_bought_with_invoice else self.purchase_receipt purchase_document = self.purchase_invoice if asset_bought_with_invoice else self.purchase_receipt
return purchase_document return purchase_document
def get_fixed_asset_account(self):
return get_asset_category_account('fixed_asset_account', None, self.name, None, 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
def get_asset_accounts(self): return cwip_account
fixed_asset_account = get_asset_category_account('fixed_asset_account', asset=self.name,
asset_category = self.asset_category, company = self.company)
cwip_account = get_asset_account("capital_work_in_progress_account",
self.name, self.asset_category, self.company)
return fixed_asset_account, cwip_account
def make_gl_entries(self): def make_gl_entries(self):
gl_entries = [] gl_entries = []
purchase_document = self.get_purchase_document() 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()): 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)) return 100 * (1 - flt(depreciation_rate, float_precision))
def update_maintenance_status(): 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: for asset in assets:
asset = frappe.get_doc("Asset", asset.name) asset = frappe.get_doc("Asset", asset.name)
if frappe.db.exists('Asset Maintenance Task', {'parent': asset.name, 'next_due_date': today()}): if frappe.db.exists("Asset Repair", {"asset_name": asset.name, "repair_status": "Pending"}):
asset.set_status('In Maintenance') asset.set_status("Out of Order")
if frappe.db.exists('Asset Repair', {'asset_name': asset.name, 'repair_status': 'Pending'}): elif frappe.db.exists("Asset Maintenance Task", {"parent": asset.name, "next_due_date": today()}):
asset.set_status('Out of Order') asset.set_status("In Maintenance")
else:
asset.set_status()
def make_post_gl_entry(): 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.depreciation import post_depreciation_entries, scrap_asset, restore_asset
from erpnext.assets.doctype.asset.asset import make_sales_invoice 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.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 from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_invoice as make_invoice
class TestAsset(unittest.TestCase): class TestAsset(unittest.TestCase):
@ -558,81 +559,6 @@ class TestAsset(unittest.TestCase):
self.assertEqual(gle, expected_gle) 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): def test_expense_head(self):
pr = make_purchase_receipt(item_code="Macbook Pro", pr = make_purchase_receipt(item_code="Macbook Pro",
qty=2, rate=200000.0, location="Test Location") qty=2, rate=200000.0, location="Test Location")
@ -640,6 +566,74 @@ class TestAsset(unittest.TestCase):
doc = make_invoice(pr.name) doc = make_invoice(pr.name)
self.assertEquals('Asset Received But Not Billed - _TC', doc.items[0].expense_account) 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(): def create_asset_data():
if not frappe.db.exists("Asset Category", "Computers"): if not frappe.db.exists("Asset Category", "Computers"):

View File

@ -5,7 +5,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
from frappe import _ from frappe import _
from frappe.utils import cint from frappe.utils import cint, get_link_to_form
from frappe.model.document import Document from frappe.model.document import Document
class AssetCategory(Document): class AssetCategory(Document):
@ -13,6 +13,7 @@ class AssetCategory(Document):
self.validate_finance_books() self.validate_finance_books()
self.validate_account_types() self.validate_account_types()
self.validate_account_currency() self.validate_account_currency()
self.valide_cwip_account()
def validate_finance_books(self): def validate_finance_books(self):
for d in self.finance_books: for d in self.finance_books:
@ -58,6 +59,21 @@ class AssetCategory(Document):
frappe.throw(_("Row #{}: {} of {} should be {}. Please modify the account or select a different account.") frappe.throw(_("Row #{}: {} of {} should be {}. Please modify the account or select a different account.")
.format(d.idx, frappe.unscrub(key_to_match), frappe.bold(selected_account), frappe.bold(expected_key_type)), .format(d.idx, frappe.unscrub(key_to_match), frappe.bold(selected_account), frappe.bold(expected_key_type)),
title=_("Invalid Account")) 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() @frappe.whitelist()

View File

@ -26,4 +26,22 @@ class TestAssetCategory(unittest.TestCase):
asset_category.insert() asset_category.insert()
except frappe.DuplicateEntryError: except frappe.DuplicateEntryError:
pass 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, "hidden": 0,
"label": "Other Reports", "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, "hidden": 0,
@ -60,7 +60,7 @@
"idx": 0, "idx": 0,
"is_standard": 1, "is_standard": 1,
"label": "Buying", "label": "Buying",
"modified": "2020-06-29 19:30:24.983050", "modified": "2020-09-30 14:40:55.638458",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Buying", "name": "Buying",

View File

@ -651,12 +651,12 @@ class TestPurchaseOrder(unittest.TestCase):
make_subcontracted_item(item_code) make_subcontracted_item(item_code)
po = create_purchase_order(item_code=item_code, qty=1, 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') name = frappe.db.get_value('BOM', {'item': item_code}, 'name')
bom = frappe.get_doc('BOM', 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]) supplied_items = sorted([d.rm_item_code for d in po.supplied_items])
self.assertEquals(exploded_items, 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) 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]) 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) self.assertEquals(supplied_items1, bom_items)

View File

@ -22,8 +22,6 @@ frappe.ui.form.on("Request for Quotation",{
}, },
onload: function(frm) { onload: function(frm) {
frm.add_fetch('email_template', 'response', 'message_for_supplier');
if(!frm.doc.message_for_supplier) { if(!frm.doc.message_for_supplier) {
frm.set_value("message_for_supplier", __("Please supply the specified items at the best possible rates")) frm.set_value("message_for_supplier", __("Please supply the specified items at the best possible rates"))
} }
@ -179,7 +177,7 @@ frappe.ui.form.on("Request for Quotation",{
dialog.hide(); dialog.hide();
return frappe.call({ return frappe.call({
type: "GET", 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: { args: {
"source_name": doc.name, "source_name": doc.name,
"for_supplier": args.supplier "for_supplier": args.supplier
@ -194,6 +192,66 @@ frappe.ui.form.on("Request for Quotation",{
}); });
}); });
dialog.show() 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();
} }
}) })
@ -276,7 +334,7 @@ erpnext.buying.RequestforQuotationController = erpnext.buying.BuyingController.e
}) })
}, __("Get items from")); }, __("Get items from"));
// Get items from Opportunity // Get items from Opportunity
this.frm.add_custom_button(__('Opportunity'), this.frm.add_custom_button(__('Opportunity'),
function() { function() {
erpnext.utils.map_current_doc({ erpnext.utils.map_current_doc({
method: "erpnext.crm.doctype.opportunity.opportunity.make_request_for_quotation", method: "erpnext.crm.doctype.opportunity.opportunity.make_request_for_quotation",

View File

@ -1,5 +1,5 @@
{ {
"actions": "", "actions": [],
"allow_import": 1, "allow_import": 1,
"autoname": "naming_series:", "autoname": "naming_series:",
"creation": "2016-02-25 01:24:07.224790", "creation": "2016-02-25 01:24:07.224790",
@ -19,7 +19,12 @@
"items", "items",
"link_to_mrs", "link_to_mrs",
"supplier_response_section", "supplier_response_section",
"salutation",
"email_template", "email_template",
"col_break_email_1",
"subject",
"preview",
"sec_break_email_2",
"message_for_supplier", "message_for_supplier",
"terms_section_break", "terms_section_break",
"tc_name", "tc_name",
@ -126,8 +131,10 @@
"label": "Link to Material Requests" "label": "Link to Material Requests"
}, },
{ {
"depends_on": "eval:!doc.__islocal",
"fieldname": "supplier_response_section", "fieldname": "supplier_response_section",
"fieldtype": "Section Break" "fieldtype": "Section Break",
"label": "Email Details"
}, },
{ {
"fieldname": "email_template", "fieldname": "email_template",
@ -137,6 +144,8 @@
"print_hide": 1 "print_hide": 1
}, },
{ {
"fetch_from": "email_template.response",
"fetch_if_empty": 1,
"fieldname": "message_for_supplier", "fieldname": "message_for_supplier",
"fieldtype": "Text Editor", "fieldtype": "Text Editor",
"in_list_view": 1, "in_list_view": 1,
@ -230,12 +239,45 @@
"options": "Request for Quotation", "options": "Request for Quotation",
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
},
{
"fetch_from": "email_template.subject",
"fetch_if_empty": 1,
"fieldname": "subject",
"fieldtype": "Data",
"label": "Subject",
"print_hide": 1
},
{
"description": "Select a greeting for the receiver. E.g. Mr., Ms., etc.",
"fieldname": "salutation",
"fieldtype": "Link",
"label": "Salutation",
"no_copy": 1,
"options": "Salutation",
"print_hide": 1
},
{
"fieldname": "col_break_email_1",
"fieldtype": "Column Break"
},
{
"depends_on": "eval:!doc.docstatus==1",
"fieldname": "preview",
"fieldtype": "Button",
"label": "Preview Email"
},
{
"depends_on": "eval:!doc.__islocal",
"fieldname": "sec_break_email_2",
"fieldtype": "Section Break",
"hide_border": 1
} }
], ],
"icon": "fa fa-shopping-cart", "icon": "fa fa-shopping-cart",
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-06-25 14:37:21.140194", "modified": "2020-10-01 14:54:50.888729",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Request for Quotation", "name": "Request for Quotation",

View File

@ -51,7 +51,7 @@ class RequestforQuotation(BuyingController):
def validate_email_id(self, args): def validate_email_id(self, args):
if not args.email_id: if not args.email_id:
frappe.throw(_("Row {0}: For Supplier {0}, Email Address is Required to Send Email").format(args.idx, args.supplier)) frappe.throw(_("Row {0}: For Supplier {1}, Email Address is Required to send an email").format(args.idx, frappe.bold(args.supplier)))
def on_submit(self): def on_submit(self):
frappe.db.set(self, 'status', 'Submitted') frappe.db.set(self, 'status', 'Submitted')
@ -62,17 +62,31 @@ class RequestforQuotation(BuyingController):
def on_cancel(self): def on_cancel(self):
frappe.db.set(self, 'status', 'Cancelled') frappe.db.set(self, 'status', 'Cancelled')
def get_supplier_email_preview(self, supplier):
"""Returns formatted email preview as string."""
rfq_suppliers = list(filter(lambda row: row.supplier == supplier, self.suppliers))
rfq_supplier = rfq_suppliers[0]
self.validate_email_id(rfq_supplier)
message = self.supplier_rfq_mail(rfq_supplier, '', self.get_link(), True)
return message
def send_to_supplier(self): def send_to_supplier(self):
"""Sends RFQ mail to involved suppliers."""
for rfq_supplier in self.suppliers: for rfq_supplier in self.suppliers:
if rfq_supplier.send_email: if rfq_supplier.send_email:
self.validate_email_id(rfq_supplier) self.validate_email_id(rfq_supplier)
# make new user if required # make new user if required
update_password_link = self.update_supplier_contact(rfq_supplier, self.get_link()) update_password_link, contact = self.update_supplier_contact(rfq_supplier, self.get_link())
self.update_supplier_part_no(rfq_supplier) self.update_supplier_part_no(rfq_supplier)
self.supplier_rfq_mail(rfq_supplier, update_password_link, self.get_link()) self.supplier_rfq_mail(rfq_supplier, update_password_link, self.get_link())
rfq_supplier.email_sent = 1 rfq_supplier.email_sent = 1
if not rfq_supplier.contact:
rfq_supplier.contact = contact
rfq_supplier.save() rfq_supplier.save()
def get_link(self): def get_link(self):
@ -87,18 +101,19 @@ class RequestforQuotation(BuyingController):
def update_supplier_contact(self, rfq_supplier, link): def update_supplier_contact(self, rfq_supplier, link):
'''Create a new user for the supplier if not set in contact''' '''Create a new user for the supplier if not set in contact'''
update_password_link = '' update_password_link, contact = '', ''
if frappe.db.exists("User", rfq_supplier.email_id): if frappe.db.exists("User", rfq_supplier.email_id):
user = frappe.get_doc("User", rfq_supplier.email_id) user = frappe.get_doc("User", rfq_supplier.email_id)
else: else:
user, update_password_link = self.create_user(rfq_supplier, link) user, update_password_link = self.create_user(rfq_supplier, link)
self.update_contact_of_supplier(rfq_supplier, user) contact = self.link_supplier_contact(rfq_supplier, user)
return update_password_link return update_password_link, contact
def update_contact_of_supplier(self, rfq_supplier, user): def link_supplier_contact(self, rfq_supplier, user):
"""If no Contact, create a new contact against Supplier. If Contact exists, check if email and user id set."""
if rfq_supplier.contact: if rfq_supplier.contact:
contact = frappe.get_doc("Contact", rfq_supplier.contact) contact = frappe.get_doc("Contact", rfq_supplier.contact)
else: else:
@ -115,6 +130,10 @@ class RequestforQuotation(BuyingController):
contact.save(ignore_permissions=True) contact.save(ignore_permissions=True)
if not rfq_supplier.contact:
# return contact to later update, RFQ supplier row's contact
return contact.name
def create_user(self, rfq_supplier, link): def create_user(self, rfq_supplier, link):
user = frappe.get_doc({ user = frappe.get_doc({
'doctype': 'User', 'doctype': 'User',
@ -129,22 +148,36 @@ class RequestforQuotation(BuyingController):
return user, update_password_link return user, update_password_link
def supplier_rfq_mail(self, data, update_password_link, rfq_link): def supplier_rfq_mail(self, data, update_password_link, rfq_link, preview=False):
full_name = get_user_fullname(frappe.session['user']) full_name = get_user_fullname(frappe.session['user'])
if full_name == "Guest": if full_name == "Guest":
full_name = "Administrator" full_name = "Administrator"
# send document dict and some important data from suppliers row
# to render message_for_supplier from any template
doc_args = self.as_dict()
doc_args.update({
'supplier': data.get('supplier'),
'supplier_name': data.get('supplier_name')
})
args = { args = {
'update_password_link': update_password_link, 'update_password_link': update_password_link,
'message': frappe.render_template(self.message_for_supplier, data.as_dict()), 'message': frappe.render_template(self.message_for_supplier, doc_args),
'rfq_link': rfq_link, 'rfq_link': rfq_link,
'user_fullname': full_name 'user_fullname': full_name,
'supplier_name' : data.get('supplier_name'),
'supplier_salutation' : self.salutation or 'Dear Mx.',
} }
subject = _("Request for Quotation") subject = self.subject or _("Request for Quotation")
template = "templates/emails/request_for_quotation.html" template = "templates/emails/request_for_quotation.html"
sender = frappe.session.user not in STANDARD_USERS and frappe.session.user or None sender = frappe.session.user not in STANDARD_USERS and frappe.session.user or None
message = frappe.get_template(template).render(args) message = frappe.get_template(template).render(args)
if preview:
return message
attachments = self.get_attachments() attachments = self.get_attachments()
self.send_email(data, sender, subject, message, attachments) self.send_email(data, sender, subject, message, attachments)
@ -214,14 +247,14 @@ 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 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')}) 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() @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): def postprocess(source, target_doc):
target_doc.supplier = for_supplier if for_supplier:
args = get_party_details(for_supplier, party_type="Supplier", ignore_permissions=True) target_doc.supplier = for_supplier
target_doc.currency = args.currency or get_party_account_currency('Supplier', for_supplier, source.company) args = get_party_details(for_supplier, party_type="Supplier", ignore_permissions=True)
target_doc.buying_price_list = args.buying_price_list or frappe.db.get_value('Buying Settings', None, 'buying_price_list') target_doc.currency = args.currency or get_party_account_currency('Supplier', for_supplier, source.company)
target_doc.buying_price_list = args.buying_price_list or frappe.db.get_value('Buying Settings', None, 'buying_price_list')
set_missing_values(source, target_doc) set_missing_values(source, target_doc)
doclist = get_mapped_doc("Request for Quotation", source_name, { doclist = get_mapped_doc("Request for Quotation", source_name, {
@ -354,3 +387,32 @@ def get_supplier_tag():
frappe.cache().hset("Supplier", "Tags", tags) frappe.cache().hset("Supplier", "Tags", tags)
return frappe.cache().hget("Supplier", "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 frappe.utils import nowdate
from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.item.test_item import make_item
from erpnext.templates.pages.rfq import check_supplier_has_docname_access 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.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.test_opportunity import make_opportunity
from erpnext.crm.doctype.opportunity.opportunity import make_request_for_quotation as make_rfq 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') self.assertEqual(rfq.get('suppliers')[1].quote_status, 'Pending')
# Submit the first supplier quotation # 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() sq.submit()
# No Quote first supplier quotation # No Quote first supplier quotation
@ -37,10 +37,10 @@ class TestRequestforQuotation(unittest.TestCase):
def test_make_supplier_quotation(self): def test_make_supplier_quotation(self):
rfq = make_request_for_quotation() 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() 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() sq1.submit()
self.assertEqual(sq.supplier, rfq.get('suppliers')[0].supplier) 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) 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() sq.submit()
frappe.form_dict = frappe.local("form_dict") frappe.form_dict = frappe.local("form_dict")

View File

@ -1,362 +1,112 @@
{ {
"allow_copy": 0, "actions": [],
"allow_guest_to_view": 0, "creation": "2016-03-29 05:59:11.896885",
"allow_import": 0, "doctype": "DocType",
"allow_rename": 0, "editable_grid": 1,
"beta": 0, "engine": "InnoDB",
"creation": "2016-03-29 05:59:11.896885", "field_order": [
"custom": 0, "send_email",
"docstatus": 0, "email_sent",
"doctype": "DocType", "supplier",
"document_type": "", "contact",
"editable_grid": 1, "no_quote",
"engine": "InnoDB", "quote_status",
"column_break_3",
"supplier_name",
"email_id",
"download_pdf"
],
"fields": [ "fields": [
{ {
"allow_bulk_edit": 0, "allow_on_submit": 1,
"allow_on_submit": 1, "default": "1",
"bold": 0, "fieldname": "send_email",
"collapsible": 0, "fieldtype": "Check",
"columns": 0, "label": "Send Email"
"default": "1", },
"fieldname": "send_email",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Send Email",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "allow_on_submit": 1,
"allow_on_submit": 1, "default": "0",
"bold": 0, "depends_on": "eval:doc.docstatus >= 1",
"collapsible": 0, "fieldname": "email_sent",
"columns": 0, "fieldtype": "Check",
"default": "0", "label": "Email Sent",
"depends_on": "eval:doc.docstatus >= 1", "no_copy": 1,
"fieldname": "email_sent", "read_only": 1
"fieldtype": "Check", },
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Email Sent",
"length": 0,
"no_copy": 1,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "columns": 4,
"allow_on_submit": 0, "fieldname": "supplier",
"bold": 0, "fieldtype": "Link",
"collapsible": 0, "in_list_view": 1,
"columns": 4, "label": "Supplier",
"fieldname": "supplier", "options": "Supplier",
"fieldtype": "Link", "reqd": 1
"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
},
{ {
"allow_bulk_edit": 0, "allow_on_submit": 1,
"allow_on_submit": 0, "columns": 3,
"bold": 0, "fieldname": "contact",
"collapsible": 0, "fieldtype": "Link",
"columns": 3, "in_list_view": 1,
"fieldname": "contact", "label": "Contact",
"fieldtype": "Link", "no_copy": 1,
"hidden": 0, "options": "Contact"
"ignore_user_permissions": 0, },
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Contact",
"length": 0,
"no_copy": 1,
"options": "Contact",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "allow_on_submit": 1,
"allow_on_submit": 1, "default": "0",
"bold": 0, "depends_on": "eval:doc.docstatus >= 1 && doc.quote_status != 'Received'",
"collapsible": 0, "fieldname": "no_quote",
"columns": 0, "fieldtype": "Check",
"depends_on": "eval:doc.docstatus >= 1 && doc.quote_status != 'Received'", "label": "No Quote"
"fieldname": "no_quote", },
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "No Quote",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "allow_on_submit": 1,
"allow_on_submit": 1, "depends_on": "eval:doc.docstatus >= 1 && !doc.no_quote",
"bold": 0, "fieldname": "quote_status",
"collapsible": 0, "fieldtype": "Select",
"columns": 0, "label": "Quote Status",
"depends_on": "eval:doc.docstatus >= 1 && !doc.no_quote", "options": "Pending\nReceived\nNo Quote",
"fieldname": "quote_status", "read_only": 1
"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
},
{ {
"allow_bulk_edit": 0, "fieldname": "column_break_3",
"allow_on_submit": 0, "fieldtype": "Column Break"
"bold": 0, },
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_3",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "bold": 1,
"allow_on_submit": 0,
"bold": 1,
"collapsible": 0,
"columns": 0,
"fetch_from": "supplier.supplier_name", "fetch_from": "supplier.supplier_name",
"fieldname": "supplier_name", "fieldname": "supplier_name",
"fieldtype": "Read Only", "fieldtype": "Read Only",
"hidden": 0, "in_global_search": 1,
"ignore_user_permissions": 0, "label": "Supplier Name"
"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
},
{ {
"allow_bulk_edit": 0, "columns": 3,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 3,
"fetch_from": "contact.email_id", "fetch_from": "contact.email_id",
"fieldname": "email_id", "fieldname": "email_id",
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 0, "in_list_view": 1,
"ignore_user_permissions": 0, "label": "Email Id",
"ignore_xss_filter": 0, "no_copy": 1
"in_filter": 0, },
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Email Id",
"length": 0,
"no_copy": 1,
"options": "",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "allow_on_submit": 1,
"allow_on_submit": 1, "fieldname": "download_pdf",
"bold": 0, "fieldtype": "Button",
"collapsible": 0, "label": "Download PDF"
"columns": 0,
"fieldname": "download_pdf",
"fieldtype": "Button",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Download PDF",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
} }
], ],
"has_web_view": 0, "istable": 1,
"hide_heading": 0, "links": [],
"hide_toolbar": 0, "modified": "2020-09-28 19:31:11.855588",
"idx": 0, "modified_by": "Administrator",
"image_view": 0, "module": "Buying",
"in_create": 0, "name": "Request for Quotation Supplier",
"is_submittable": 0, "owner": "Administrator",
"issingle": 0, "permissions": [],
"istable": 1, "sort_field": "modified",
"max_attachments": 0, "sort_order": "DESC",
"modified": "2018-05-16 22:43:30.212408", "track_changes": 1
"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
} }

View File

@ -8,8 +8,7 @@ erpnext.buying.SupplierQuotationController = erpnext.buying.BuyingController.ext
setup: function() { setup: function() {
this.frm.custom_make_buttons = { this.frm.custom_make_buttons = {
'Purchase Order': 'Purchase Order', 'Purchase Order': 'Purchase Order',
'Quotation': 'Quotation', 'Quotation': 'Quotation'
'Subscription': 'Subscription'
} }
this._super(); 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.page.set_inner_btn_group_as_primary(__('Create'));
cur_frm.add_custom_button(__("Quotation"), this.make_quotation, cur_frm.add_custom_button(__("Quotation"), this.make_quotation,
__('Create')); __('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) { else if (this.frm.doc.docstatus===0) {
@ -54,6 +47,27 @@ erpnext.buying.SupplierQuotationController = erpnext.buying.BuyingController.ext
} }
}) })
}, __("Get items from")); }, __("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", "default": "Today",
"fieldname": "transaction_date", "fieldname": "transaction_date",
"fieldtype": "Date", "fieldtype": "Date",
"in_list_view": 1,
"label": "Date", "label": "Date",
"oldfieldname": "transaction_date", "oldfieldname": "transaction_date",
"oldfieldtype": "Date", "oldfieldtype": "Date",
@ -798,6 +799,7 @@
{ {
"fieldname": "valid_till", "fieldname": "valid_till",
"fieldtype": "Date", "fieldtype": "Date",
"in_list_view": 1,
"label": "Valid Till" "label": "Valid Till"
} }
], ],
@ -805,7 +807,7 @@
"idx": 29, "idx": 29,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-07-18 05:10:45.556792", "modified": "2020-10-01 20:56:17.932007",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Supplier Quotation", "name": "Supplier Quotation",

View File

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

View File

@ -143,7 +143,7 @@ def get_conditions(filters):
conditions = "" conditions = ""
if filters.get("company"): if filters.get("company"):
conditions += " AND par.company=%s" % frappe.db.escape(filters.get('company')) conditions += " AND parent.company=%s" % frappe.db.escape(filters.get('company'))
if filters.get("cost_center") or filters.get("project"): if filters.get("cost_center") or filters.get("project"):
conditions += """ conditions += """
@ -151,10 +151,10 @@ def get_conditions(filters):
""" % (frappe.db.escape(filters.get('cost_center')), frappe.db.escape(filters.get('project'))) """ % (frappe.db.escape(filters.get('cost_center')), frappe.db.escape(filters.get('project')))
if filters.get("from_date"): if filters.get("from_date"):
conditions += " AND par.transaction_date>='%s'" % filters.get('from_date') conditions += " AND parent.transaction_date>='%s'" % filters.get('from_date')
if filters.get("to_date"): if filters.get("to_date"):
conditions += " AND par.transaction_date<='%s'" % filters.get('to_date') conditions += " AND parent.transaction_date<='%s'" % filters.get('to_date')
return conditions return conditions
def get_data(filters): def get_data(filters):
@ -198,21 +198,23 @@ def get_mapped_mr_details(conditions):
mr_records = {} mr_records = {}
mr_details = frappe.db.sql(""" mr_details = frappe.db.sql("""
SELECT SELECT
par.transaction_date, parent.transaction_date,
par.per_ordered, parent.per_ordered,
par.owner, parent.owner,
child.name, child.name,
child.parent, child.parent,
child.amount, child.amount,
child.qty, child.qty,
child.item_code, child.item_code,
child.uom, child.uom,
par.status parent.status,
FROM `tabMaterial Request` par, `tabMaterial Request Item` child child.project,
child.cost_center
FROM `tabMaterial Request` parent, `tabMaterial Request Item` child
WHERE WHERE
par.per_ordered>=0 parent.per_ordered>=0
AND par.name=child.parent AND parent.name=child.parent
AND par.docstatus=1 AND parent.docstatus=1
{conditions} {conditions}
""".format(conditions=conditions), as_dict=1) #nosec """.format(conditions=conditions), as_dict=1) #nosec
@ -232,7 +234,9 @@ def get_mapped_mr_details(conditions):
status=record.status, status=record.status,
actual_cost=0, actual_cost=0,
purchase_order_amt=0, purchase_order_amt=0,
purchase_order_amt_in_company_currency=0 purchase_order_amt_in_company_currency=0,
project = record.project,
cost_center = record.cost_center
) )
procurement_record_against_mr.append(procurement_record_details) procurement_record_against_mr.append(procurement_record_details)
return mr_records, procurement_record_against_mr return mr_records, procurement_record_against_mr
@ -280,16 +284,16 @@ def get_po_entries(conditions):
child.amount, child.amount,
child.base_amount, child.base_amount,
child.schedule_date, child.schedule_date,
par.transaction_date, parent.transaction_date,
par.supplier, parent.supplier,
par.status, parent.status,
par.owner parent.owner
FROM `tabPurchase Order` par, `tabPurchase Order Item` child FROM `tabPurchase Order` parent, `tabPurchase Order Item` child
WHERE WHERE
par.docstatus = 1 parent.docstatus = 1
AND par.name = child.parent AND parent.name = child.parent
AND par.status not in ("Closed","Completed","Cancelled") AND parent.status not in ("Closed","Completed","Cancelled")
{conditions} {conditions}
GROUP BY GROUP BY
par.name, child.item_code parent.name, child.item_code
""".format(conditions=conditions), as_dict=1) #nosec """.format(conditions=conditions), as_dict=1) #nosec

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 // Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // For license information, please see license.txt
frappe.query_reports["Quoted Item Comparison"] = { frappe.query_reports["Supplier Quotation Comparison"] = {
filters: [ filters: [
{ {
fieldtype: "Link", fieldtype: "Link",
@ -78,6 +78,13 @@ frappe.query_reports["Quoted Item Comparison"] = {
return { filters: { "docstatus": ["<", 2] } } 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", fieldtype: "Check",
label: __("Include Expired"), 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; 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: if not filters:
return [], [] return [], []
columns = get_columns(filters)
conditions = get_conditions(filters) conditions = get_conditions(filters)
supplier_quotation_data = get_data(filters, conditions) supplier_quotation_data = get_data(filters, conditions)
columns = get_columns()
data, chart_data = prepare_data(supplier_quotation_data, filters) data, chart_data = prepare_data(supplier_quotation_data, filters)
message = get_message() message = get_message()
@ -41,9 +41,13 @@ def get_conditions(filters):
return conditions return conditions
def get_data(filters, conditions): def get_data(filters, conditions):
supplier_quotation_data = frappe.db.sql("""SELECT supplier_quotation_data = frappe.db.sql("""
sqi.parent, sqi.item_code, sqi.qty, sqi.rate, sqi.uom, sqi.request_for_quotation, SELECT
sqi.lead_time_days, sq.supplier, sq.valid_till 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 FROM
`tabSupplier Quotation Item` sqi, `tabSupplier Quotation Item` sqi,
`tabSupplier Quotation` sq `tabSupplier Quotation` sq
@ -58,16 +62,18 @@ def get_data(filters, conditions):
return supplier_quotation_data return supplier_quotation_data
def prepare_data(supplier_quotation_data, filters): def prepare_data(supplier_quotation_data, filters):
out, suppliers, qty_list, chart_data = [], [], [], [] out, groups, qty_list, suppliers, chart_data = [], [], [], [], []
supplier_wise_map = defaultdict(list) group_wise_map = defaultdict(list)
supplier_qty_price_map = {} 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") company_currency = frappe.db.get_default("currency")
float_precision = cint(frappe.db.get_default("float_precision")) or 2 float_precision = cint(frappe.db.get_default("float_precision")) or 2
for data in supplier_quotation_data: for data in supplier_quotation_data:
supplier = data.get("supplier") group = data.get(group_by_field) # get item or supplier value for this row
supplier_currency = frappe.db.get_value("Supplier", data.get("supplier"), "default_currency")
supplier_currency = frappe.db.get_value("Supplier", data.get("supplier_name"), "default_currency")
if supplier_currency: if supplier_currency:
exchange_rate = get_exchange_rate(supplier_currency, company_currency) exchange_rate = get_exchange_rate(supplier_currency, company_currency)
@ -75,38 +81,55 @@ def prepare_data(supplier_quotation_data, filters):
exchange_rate = 1 exchange_rate = 1
row = { 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"), "quotation": data.get("parent"),
"qty": data.get("qty"), "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"), "uom": data.get("uom"),
"stock_uom": data.get('stock_uom'),
"request_for_quotation": data.get("request_for_quotation"), "request_for_quotation": data.get("request_for_quotation"),
"valid_till": data.get('valid_till'), "valid_till": data.get('valid_till'),
"lead_time_days": data.get('lead_time_days') "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':[{},{},...]} # map for report view of form {'supplier1'/'item1':[{},{},...]}
supplier_wise_map[supplier].append(row) group_wise_map[group].append(row)
# map for chart preparation of the form {'supplier1': {'qty': 'price'}} # map for chart preparation of the form {'supplier1': {'qty': 'price'}}
supplier = data.get("supplier_name")
if filters.get("item_code"): if filters.get("item_code"):
if not supplier in supplier_qty_price_map: if not supplier in supplier_qty_price_map:
supplier_qty_price_map[supplier] = {} supplier_qty_price_map[supplier] = {}
supplier_qty_price_map[supplier][row["qty"]] = row["price"] supplier_qty_price_map[supplier][row["qty"]] = row["price"]
groups.append(group)
suppliers.append(supplier) suppliers.append(supplier)
qty_list.append(data.get("qty")) qty_list.append(data.get("qty"))
groups = list(set(groups))
suppliers = list(set(suppliers)) suppliers = list(set(suppliers))
qty_list = list(set(qty_list)) 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 # final data format for report view
for supplier in suppliers: for group in groups:
supplier_wise_map[supplier][0].update({"supplier_name": supplier}) group_entries = group_wise_map[group] # all entries pertaining to item/supplier
for entry in supplier_wise_map[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) out.append(entry)
if filters.get("item_code"): if filters.get("item_code"):
# render chart only for one item comparison
chart_data = prepare_chart_data(suppliers, qty_list, supplier_qty_price_map) chart_data = prepare_chart_data(suppliers, qty_list, supplier_qty_price_map)
return out, chart_data return out, chart_data
@ -145,8 +168,9 @@ def prepare_chart_data(suppliers, qty_list, supplier_qty_price_map):
return chart_data return chart_data
def get_columns(): def get_columns(filters):
columns = [{ group_by_columns = [
{
"fieldname": "supplier_name", "fieldname": "supplier_name",
"label": _("Supplier"), "label": _("Supplier"),
"fieldtype": "Link", "fieldtype": "Link",
@ -158,8 +182,10 @@ def get_columns():
"label": _("Item"), "label": _("Item"),
"fieldtype": "Link", "fieldtype": "Link",
"options": "Item", "options": "Item",
"width": 200 "width": 150
}, }]
columns = [
{ {
"fieldname": "uom", "fieldname": "uom",
"label": _("UOM"), "label": _("UOM"),
@ -180,6 +206,20 @@ def get_columns():
"options": "Company:company:default_currency", "options": "Company:company:default_currency",
"width": 110 "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", "fieldname": "quotation",
"label": _("Supplier Quotation"), "label": _("Supplier Quotation"),
@ -205,9 +245,12 @@ def get_columns():
"fieldtype": "Link", "fieldtype": "Link",
"options": "Request for Quotation", "options": "Request for Quotation",
"width": 150 "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 return columns
def get_message(): def get_message():

View File

@ -63,8 +63,8 @@ def get_data():
{ {
"type": "doctype", "type": "doctype",
"name": "Chart of Accounts Importer", "name": "Chart of Accounts Importer",
"labe": _("Chart Of Accounts Importer"), "label": _("Chart of Accounts Importer"),
"description": _("Import Chart Of Accounts from CSV / Excel files"), "description": _("Import Chart of Accounts from CSV / Excel files"),
"onboard": 1 "onboard": 1
}, },
{ {

View File

@ -1319,24 +1319,26 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
validate_quantity(child_item, d) validate_quantity(child_item, d)
child_item.qty = flt(d.get("qty")) 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}.") 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)) .format(child_item.idx, child_item.item_code))
else: else:
child_item.rate = flt(d.get("rate")) child_item.rate = flt(d.get("rate"), rate_precision)
if d.get("conversion_factor"): if d.get("conversion_factor"):
if child_item.stock_uom == child_item.uom: if child_item.stock_uom == child_item.uom:
child_item.conversion_factor = 1 child_item.conversion_factor = 1
else: 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"): if d.get("uom"):
child_item.uom = 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")) 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': if d.get("delivery_date") and parent_doctype == 'Sales Order':
child_item.delivery_date = d.get('delivery_date') 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 where
t2.parent = t1.name and t1.item = %s t2.parent = t1.name and t1.item = %s
and t1.docstatus = 1 and t1.is_active = 1 and t1.name = %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), and t2.item_code = t3.name""".format(doctype),
(item_code, bom), as_dict=1) (item_code, bom), as_dict=1)

View File

@ -3,7 +3,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe 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 frappe import _, throw
from erpnext.stock.get_item_details import get_bin_details from erpnext.stock.get_item_details import get_bin_details
from erpnext.stock.utils import get_incoming_rate from erpnext.stock.utils import get_incoming_rate
@ -173,22 +173,26 @@ class SellingController(StockController):
def validate_selling_price(self): def validate_selling_price(self):
def throw_message(idx, item_name, rate, ref_rate_field): 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 {}""") bold_net_rate = frappe.bold("net rate")
.format(idx, item_name, ref_rate_field, 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"): if not frappe.db.get_single_value("Selling Settings", "validate_selling_price"):
return return
if hasattr(self, "is_return") and self.is_return: if hasattr(self, "is_return") and self.is_return:
return return
for it in self.get("items"): for it in self.get("items"):
if not it.item_code: if not it.item_code:
continue continue
last_purchase_rate, is_stock_item = frappe.get_cached_value("Item", it.item_code, ["last_purchase_rate", "is_stock_item"]) 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) 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): 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") throw_message(it.idx, frappe.bold(it.item_name), last_purchase_rate_in_sales_uom, "last purchase rate")
last_valuation_rate = frappe.db.sql(""" last_valuation_rate = frappe.db.sql("""
@ -197,8 +201,8 @@ class SellingController(StockController):
ORDER BY posting_date DESC, posting_time DESC, creation DESC LIMIT 1 ORDER BY posting_date DESC, posting_time DESC, creation DESC LIMIT 1
""", (it.item_code, it.warehouse)) """, (it.item_code, it.warehouse))
if last_valuation_rate: if last_valuation_rate:
last_valuation_rate_in_sales_uom = last_valuation_rate[0][0] / (it.conversion_factor or 1) 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) \ if is_stock_item and flt(it.base_net_rate) < flt(last_valuation_rate_in_sales_uom) \
and not self.get('is_internal_customer'): and not self.get('is_internal_customer'):
throw_message(it.idx, frappe.bold(it.item_name), last_valuation_rate_in_sales_uom, "valuation rate") throw_message(it.idx, frappe.bold(it.item_name), last_valuation_rate_in_sales_uom, "valuation rate")

View File

@ -241,6 +241,7 @@
}, },
{ {
"depends_on": "eval: doc.__islocal", "depends_on": "eval: doc.__islocal",
"description": "Home, Work, etc.",
"fieldname": "address_title", "fieldname": "address_title",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Address Title" "label": "Address Title"
@ -249,7 +250,8 @@
"depends_on": "eval: doc.__islocal", "depends_on": "eval: doc.__islocal",
"fieldname": "address_line1", "fieldname": "address_line1",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Address Line 1" "label": "Address Line 1",
"mandatory_depends_on": "eval: doc.address_title && doc.address_type"
}, },
{ {
"depends_on": "eval: doc.__islocal", "depends_on": "eval: doc.__islocal",
@ -261,7 +263,8 @@
"depends_on": "eval: doc.__islocal", "depends_on": "eval: doc.__islocal",
"fieldname": "city", "fieldname": "city",
"fieldtype": "Data", "fieldtype": "Data",
"label": "City/Town" "label": "City/Town",
"mandatory_depends_on": "eval: doc.address_title && doc.address_type"
}, },
{ {
"depends_on": "eval: doc.__islocal", "depends_on": "eval: doc.__islocal",
@ -280,6 +283,7 @@
"fieldname": "country", "fieldname": "country",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Country", "label": "Country",
"mandatory_depends_on": "eval: doc.address_title && doc.address_type",
"options": "Country" "options": "Country"
}, },
{ {
@ -449,7 +453,7 @@
"idx": 5, "idx": 5,
"image_field": "image", "image_field": "image",
"links": [], "links": [],
"modified": "2020-06-18 14:39:41.835416", "modified": "2020-10-13 15:24:00.094811",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "CRM", "module": "CRM",
"name": "Lead", "name": "Lead",

View File

@ -22,7 +22,8 @@ class Lead(SellingController):
load_address_and_contact(self) load_address_and_contact(self)
def before_insert(self): def before_insert(self):
self.address_doc = self.create_address() if self.address_title and self.address_type:
self.address_doc = self.create_address()
self.contact_doc = self.create_contact() self.contact_doc = self.create_contact()
def after_insert(self): def after_insert(self):
@ -133,15 +134,6 @@ class Lead(SellingController):
# skipping country since the system auto-sets it from system defaults # skipping country since the system auto-sets it from system defaults
address = frappe.new_doc("Address") address = frappe.new_doc("Address")
mandatory_fields = [ df.fieldname for df in address.meta.fields if df.reqd ]
if not all([self.get(field) for field in mandatory_fields]):
frappe.msgprint(_('Missing mandatory fields in address. \
{0} to create address' ).format("<a href='desk#Form/Address/New Address 1' \
> Click here </a>"),
alert=True, indicator='yellow')
return
address.update({addr_field: self.get(addr_field) for addr_field in address_fields}) address.update({addr_field: self.get(addr_field) for addr_field in address_fields})
address.update({info_field: self.get(info_field) for info_field in info_fields}) address.update({info_field: self.get(info_field) for info_field in info_fields})
address.insert() address.insert()
@ -190,7 +182,7 @@ class Lead(SellingController):
def update_links(self): def update_links(self):
# update address links # update address links
if self.address_doc: if hasattr(self, 'address_doc'):
self.address_doc.append("links", { self.address_doc.append("links", {
"link_doctype": "Lead", "link_doctype": "Lead",
"link_name": self.name, "link_name": self.name,

View File

@ -11,7 +11,7 @@ from erpnext.accounts.party import get_party_account_currency
from erpnext.exceptions import InvalidCurrency from erpnext.exceptions import InvalidCurrency
from erpnext.stock.doctype.material_request.material_request import make_request_for_quotation from erpnext.stock.doctype.material_request.material_request import make_request_for_quotation
from erpnext.buying.doctype.request_for_quotation.request_for_quotation import \ 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(): def work():
frappe.set_user(frappe.db.get_global('demo_purchase_user')) 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) rfq = frappe.get_doc('Request for Quotation', rfq.name)
for supplier in rfq.suppliers: 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.save()
supplier_quotation.submit() supplier_quotation.submit()

View File

@ -68,7 +68,7 @@ def make_program_and_linked_courses(program_name, course_name_list):
program = frappe.get_doc("Program", program_name) program = frappe.get_doc("Program", program_name)
course_list = [make_course(course_name) for course_name in course_name_list] course_list = [make_course(course_name) for course_name in course_name_list]
for course in course_list: for course in course_list:
program.append("courses", {"course": course}) program.append("courses", {"course": course, "required": 1})
program.save() program.save()
return program return program

View File

@ -17,9 +17,7 @@
"in_list_view": 1, "in_list_view": 1,
"label": "Course", "label": "Course",
"options": "Course", "options": "Course",
"reqd": 1, "reqd": 1
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fetch_from": "course.course_name", "fetch_from": "course.course_name",
@ -27,23 +25,19 @@
"fieldtype": "Data", "fieldtype": "Data",
"in_list_view": 1, "in_list_view": 1,
"label": "Course Name", "label": "Course Name",
"read_only": 1, "read_only": 1
"show_days": 1,
"show_seconds": 1
}, },
{ {
"default": "0", "default": "1",
"fieldname": "required", "fieldname": "required",
"fieldtype": "Check", "fieldtype": "Check",
"in_list_view": 1, "in_list_view": 1,
"label": "Mandatory", "label": "Mandatory"
"show_days": 1,
"show_seconds": 1
} }
], ],
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-06-09 18:56:10.213241", "modified": "2020-09-15 18:14:22.816795",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Education", "module": "Education",
"name": "Program Course", "name": "Program Course",

View File

@ -2,16 +2,24 @@
// For license information, please see license.txt // For license information, please see license.txt
frappe.ui.form.on("Program Enrollment", { frappe.ui.form.on('Program Enrollment', {
setup: function(frm) { setup: function(frm) {
frm.add_fetch('fee_structure', 'total_amount', 'amount'); frm.add_fetch('fee_structure', 'total_amount', 'amount');
}, },
onload: function(frm, cdt, cdn){ onload: function(frm) {
frm.set_query("academic_term", "fees", function(){ frm.set_query('academic_term', function() {
return{ return {
"filters":{ 'filters':{
"academic_year": (frm.doc.academic_year) 'academic_year': frm.doc.academic_year
}
};
});
frm.set_query('academic_term', 'fees', function() {
return {
'filters':{
'academic_year': frm.doc.academic_year
} }
}; };
}); });
@ -24,9 +32,9 @@ frappe.ui.form.on("Program Enrollment", {
}; };
if (frm.doc.program) { if (frm.doc.program) {
frm.set_query("course", "courses", function(doc, cdt, cdn) { frm.set_query('course', 'courses', function() {
return{ return {
query: "erpnext.education.doctype.program_enrollment.program_enrollment.get_program_courses", query: 'erpnext.education.doctype.program_enrollment.program_enrollment.get_program_courses',
filters: { filters: {
'program': frm.doc.program 'program': frm.doc.program
} }
@ -34,9 +42,9 @@ frappe.ui.form.on("Program Enrollment", {
}); });
} }
frm.set_query("student", function() { frm.set_query('student', function() {
return{ return{
query: "erpnext.education.doctype.program_enrollment.program_enrollment.get_students", query: 'erpnext.education.doctype.program_enrollment.program_enrollment.get_students',
filters: { filters: {
'academic_year': frm.doc.academic_year, 'academic_year': frm.doc.academic_year,
'academic_term': frm.doc.academic_term 'academic_term': frm.doc.academic_term
@ -49,14 +57,14 @@ frappe.ui.form.on("Program Enrollment", {
frm.events.get_courses(frm); frm.events.get_courses(frm);
if (frm.doc.program) { if (frm.doc.program) {
frappe.call({ frappe.call({
method: "erpnext.education.api.get_fee_schedule", method: 'erpnext.education.api.get_fee_schedule',
args: { args: {
"program": frm.doc.program, 'program': frm.doc.program,
"student_category": frm.doc.student_category 'student_category': frm.doc.student_category
}, },
callback: function(r) { callback: function(r) {
if(r.message) { if (r.message) {
frm.set_value("fees" ,r.message); frm.set_value('fees' ,r.message);
frm.events.get_courses(frm); frm.events.get_courses(frm);
} }
} }
@ -65,17 +73,17 @@ frappe.ui.form.on("Program Enrollment", {
}, },
student_category: function() { student_category: function() {
frappe.ui.form.trigger("Program Enrollment", "program"); frappe.ui.form.trigger('Program Enrollment', 'program');
}, },
get_courses: function(frm) { get_courses: function(frm) {
frm.set_value("courses",[]); frm.set_value('courses',[]);
frappe.call({ frappe.call({
method: "get_courses", method: 'get_courses',
doc:frm.doc, doc:frm.doc,
callback: function(r) { callback: function(r) {
if(r.message) { if (r.message) {
frm.set_value("courses", r.message); frm.set_value('courses', r.message);
} }
} }
}) })
@ -84,10 +92,10 @@ frappe.ui.form.on("Program Enrollment", {
frappe.ui.form.on('Program Enrollment Course', { frappe.ui.form.on('Program Enrollment Course', {
courses_add: function(frm){ courses_add: function(frm){
frm.fields_dict['courses'].grid.get_field('course').get_query = function(doc){ frm.fields_dict['courses'].grid.get_field('course').get_query = function(doc) {
var course_list = []; var course_list = [];
if(!doc.__islocal) course_list.push(doc.name); if(!doc.__islocal) course_list.push(doc.name);
$.each(doc.courses, function(idx, val){ $.each(doc.courses, function(_idx, val) {
if (val.course) course_list.push(val.course); if (val.course) course_list.push(val.course);
}); });
return { filters: [['Course', 'name', 'not in', course_list]] }; return { filters: [['Course', 'name', 'not in', course_list]] };

View File

@ -1,775 +1,218 @@
{ {
"allow_copy": 0, "actions": [],
"allow_events_in_timeline": 0, "allow_import": 1,
"allow_guest_to_view": 0, "autoname": "EDU-ENR-.YYYY.-.#####",
"allow_import": 1, "creation": "2015-12-02 12:58:32.916080",
"allow_rename": 0, "doctype": "DocType",
"autoname": "EDU-ENR-.YYYY.-.#####", "document_type": "Document",
"beta": 0, "engine": "InnoDB",
"creation": "2015-12-02 12:58:32.916080", "field_order": [
"custom": 0, "student",
"docstatus": 0, "student_name",
"doctype": "DocType", "student_category",
"document_type": "Document", "student_batch_name",
"editable_grid": 0, "school_house",
"engine": "InnoDB", "column_break_4",
"program",
"academic_year",
"academic_term",
"enrollment_date",
"boarding_student",
"enrolled_courses",
"courses",
"transportation",
"mode_of_transportation",
"column_break_13",
"vehicle_no",
"section_break_7",
"fees",
"amended_from",
"image"
],
"fields": [ "fields": [
{ {
"allow_bulk_edit": 0, "fieldname": "student",
"allow_in_quick_entry": 0, "fieldtype": "Link",
"allow_on_submit": 0, "in_global_search": 1,
"bold": 0, "label": "Student",
"collapsible": 0, "options": "Student",
"columns": 0, "reqd": 1
"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
},
{ {
"allow_bulk_edit": 0, "fetch_from": "student.title",
"allow_in_quick_entry": 0, "fieldname": "student_name",
"allow_on_submit": 0, "fieldtype": "Read Only",
"bold": 0, "in_global_search": 1,
"collapsible": 0, "label": "Student Name",
"columns": 0, "read_only": 1
"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
},
{ {
"allow_bulk_edit": 0, "fieldname": "student_category",
"allow_in_quick_entry": 0, "fieldtype": "Link",
"allow_on_submit": 0, "label": "Student Category",
"bold": 0, "options": "Student Category"
"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
},
{ {
"allow_bulk_edit": 0, "allow_on_submit": 1,
"allow_in_quick_entry": 0, "fieldname": "student_batch_name",
"allow_on_submit": 1, "fieldtype": "Link",
"bold": 0, "in_global_search": 1,
"collapsible": 0, "label": "Student Batch",
"columns": 0, "options": "Student Batch Name"
"fieldname": "student_batch_name", },
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 1,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Student Batch",
"length": 0,
"no_copy": 0,
"options": "Student Batch Name",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "allow_on_submit": 1,
"allow_in_quick_entry": 0, "fieldname": "school_house",
"allow_on_submit": 1, "fieldtype": "Link",
"bold": 0, "label": "School House",
"collapsible": 0, "options": "School House"
"columns": 0, },
"fieldname": "school_house",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "School House",
"length": 0,
"no_copy": 0,
"options": "School House",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "column_break_4",
"allow_in_quick_entry": 0, "fieldtype": "Column Break"
"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
},
{ {
"allow_bulk_edit": 0, "fieldname": "program",
"allow_in_quick_entry": 0, "fieldtype": "Link",
"allow_on_submit": 0, "in_list_view": 1,
"bold": 0, "in_standard_filter": 1,
"collapsible": 0, "label": "Program",
"columns": 0, "options": "Program",
"fieldname": "program", "reqd": 1
"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
},
{ {
"allow_bulk_edit": 0, "fieldname": "academic_year",
"allow_in_quick_entry": 0, "fieldtype": "Link",
"allow_on_submit": 0, "in_list_view": 1,
"bold": 0, "in_standard_filter": 1,
"collapsible": 0, "label": "Academic Year",
"columns": 0, "options": "Academic Year",
"fieldname": "academic_year", "reqd": 1
"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
},
{ {
"allow_bulk_edit": 0, "fieldname": "academic_term",
"allow_in_quick_entry": 0, "fieldtype": "Link",
"allow_on_submit": 0, "label": "Academic Term",
"bold": 0, "options": "Academic Term"
"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
},
{ {
"allow_bulk_edit": 0, "default": "Today",
"allow_in_quick_entry": 0, "fieldname": "enrollment_date",
"allow_on_submit": 0, "fieldtype": "Date",
"bold": 0, "label": "Enrollment Date",
"collapsible": 0, "reqd": 1
"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
},
{ {
"allow_bulk_edit": 0, "default": "0",
"allow_in_quick_entry": 0, "description": "Check this if the Student is residing at the Institute's Hostel.",
"allow_on_submit": 0, "fieldname": "boarding_student",
"bold": 0, "fieldtype": "Check",
"collapsible": 0, "label": "Boarding Student"
"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
},
{ {
"allow_bulk_edit": 0, "collapsible": 1,
"allow_in_quick_entry": 0, "collapsible_depends_on": "vehicle_no",
"allow_on_submit": 0, "fieldname": "transportation",
"bold": 0, "fieldtype": "Section Break",
"collapsible": 1, "label": "Transportation"
"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
},
{ {
"allow_bulk_edit": 0, "allow_on_submit": 1,
"allow_in_quick_entry": 0, "fieldname": "mode_of_transportation",
"allow_on_submit": 1, "fieldtype": "Select",
"bold": 0, "label": "Mode of Transportation",
"collapsible": 0, "options": "\nWalking\nInstitute's Bus\nPublic Transport\nSelf-Driving Vehicle\nPick/Drop by Guardian"
"columns": 0, },
"fieldname": "mode_of_transportation",
"fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Mode of Transportation",
"length": 0,
"no_copy": 0,
"options": "\nWalking\nInstitute's Bus\nPublic Transport\nSelf-Driving Vehicle\nPick/Drop by Guardian",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "column_break_13",
"allow_in_quick_entry": 0, "fieldtype": "Column Break"
"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
},
{ {
"allow_bulk_edit": 0, "allow_on_submit": 1,
"allow_in_quick_entry": 0, "fieldname": "vehicle_no",
"allow_on_submit": 1, "fieldtype": "Data",
"bold": 0, "label": "Vehicle/Bus Number"
"collapsible": 0, },
"columns": 0,
"fieldname": "vehicle_no",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Vehicle/Bus Number",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "enrolled_courses",
"allow_in_quick_entry": 0, "fieldtype": "Section Break",
"allow_on_submit": 0, "label": "Enrolled courses"
"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
},
{ {
"allow_bulk_edit": 0, "allow_on_submit": 1,
"allow_in_quick_entry": 0, "fieldname": "courses",
"allow_on_submit": 1, "fieldtype": "Table",
"bold": 0, "label": "Courses",
"collapsible": 0, "options": "Program Enrollment Course"
"columns": 0, },
"fieldname": "courses",
"fieldtype": "Table",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Courses",
"length": 0,
"no_copy": 0,
"options": "Program Enrollment Course",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "collapsible": 1,
"allow_in_quick_entry": 0, "fieldname": "section_break_7",
"allow_on_submit": 0, "fieldtype": "Section Break",
"bold": 0, "label": "Fees"
"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
},
{ {
"allow_bulk_edit": 0, "fieldname": "fees",
"allow_in_quick_entry": 0, "fieldtype": "Table",
"allow_on_submit": 0, "label": "Fees",
"bold": 0, "options": "Program Fee"
"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": ""
},
{ {
"allow_bulk_edit": 0, "fieldname": "amended_from",
"allow_in_quick_entry": 0, "fieldtype": "Link",
"allow_on_submit": 0, "label": "Amended From",
"bold": 0, "no_copy": 1,
"collapsible": 0, "options": "Program Enrollment",
"columns": 0, "print_hide": 1,
"fieldname": "amended_from", "read_only": 1
"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
},
{ {
"allow_bulk_edit": 0, "fieldname": "image",
"allow_in_quick_entry": 0, "fieldtype": "Attach Image",
"allow_on_submit": 0, "hidden": 1,
"bold": 0, "label": "Image"
"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
} }
], ],
"has_web_view": 0, "image_field": "image",
"hide_heading": 0, "is_submittable": 1,
"hide_toolbar": 0, "links": [],
"idx": 0, "modified": "2020-09-15 18:12:11.988565",
"image_field": "image", "modified_by": "Administrator",
"image_view": 0, "module": "Education",
"in_create": 0, "name": "Program Enrollment",
"is_submittable": 1, "owner": "Administrator",
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"menu_index": 0,
"modified": "2018-11-07 21:13:06.502279",
"modified_by": "Administrator",
"module": "Education",
"name": "Program Enrollment",
"name_case": "",
"owner": "Administrator",
"permissions": [ "permissions": [
{ {
"amend": 1, "amend": 1,
"cancel": 1, "cancel": 1,
"create": 1, "create": 1,
"delete": 1, "delete": 1,
"email": 1, "email": 1,
"export": 1, "export": 1,
"if_owner": 0, "print": 1,
"import": 0, "read": 1,
"permlevel": 0, "report": 1,
"print": 1, "role": "Academics User",
"read": 1, "share": 1,
"report": 1, "submit": 1,
"role": "Academics User",
"set_user_permissions": 0,
"share": 1,
"submit": 1,
"write": 1 "write": 1
}, },
{ {
"amend": 0, "create": 1,
"cancel": 0, "email": 1,
"create": 1, "export": 1,
"delete": 0, "print": 1,
"email": 1, "read": 1,
"export": 1, "report": 1,
"if_owner": 0, "role": "LMS User",
"import": 0, "share": 1,
"permlevel": 0, "submit": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "LMS User",
"set_user_permissions": 0,
"share": 1,
"submit": 1,
"write": 1 "write": 1
} }
], ],
"quick_entry": 0, "restrict_to_domain": "Education",
"read_only": 0, "show_name_in_global_search": 1,
"read_only_onload": 0, "sort_field": "modified",
"restrict_to_domain": "Education", "sort_order": "DESC",
"show_name_in_global_search": 1, "title_field": "student_name"
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "student_name",
"track_changes": 0,
"track_seen": 0,
"track_views": 0
} }

View File

@ -7,12 +7,15 @@ import frappe
from frappe import msgprint, _ from frappe import msgprint, _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.desk.reportview import get_match_cond, get_filters_cond from frappe.desk.reportview import get_match_cond, get_filters_cond
from frappe.utils import comma_and from frappe.utils import comma_and, get_link_to_form, getdate
import erpnext.www.lms as lms import erpnext.www.lms as lms
class ProgramEnrollment(Document): class ProgramEnrollment(Document):
def validate(self): def validate(self):
self.validate_duplication() self.validate_duplication()
self.validate_academic_year()
if self.academic_term:
self.validate_academic_term()
if not self.student_name: if not self.student_name:
self.student_name = frappe.db.get_value("Student", self.student, "title") self.student_name = frappe.db.get_value("Student", self.student, "title")
if not self.courses: if not self.courses:
@ -23,11 +26,34 @@ class ProgramEnrollment(Document):
self.make_fee_records() self.make_fee_records()
self.create_course_enrollments() self.create_course_enrollments()
def validate_academic_year(self):
start_date, end_date = frappe.db.get_value("Academic Year", self.academic_year, ["year_start_date", "year_end_date"])
if self.enrollment_date:
if start_date and getdate(self.enrollment_date) < getdate(start_date):
frappe.throw(_("Enrollment Date cannot be before the Start Date of the Academic Year {0}").format(
get_link_to_form("Academic Year", self.academic_year)))
if end_date and getdate(self.enrollment_date) > getdate(end_date):
frappe.throw(_("Enrollment Date cannot be after the End Date of the Academic Term {0}").format(
get_link_to_form("Academic Year", self.academic_year)))
def validate_academic_term(self):
start_date, end_date = frappe.db.get_value("Academic Term", self.academic_term, ["term_start_date", "term_end_date"])
if self.enrollment_date:
if start_date and getdate(self.enrollment_date) < getdate(start_date):
frappe.throw(_("Enrollment Date cannot be before the Start Date of the Academic Term {0}").format(
get_link_to_form("Academic Term", self.academic_term)))
if end_date and getdate(self.enrollment_date) > getdate(end_date):
frappe.throw(_("Enrollment Date cannot be after the End Date of the Academic Term {0}").format(
get_link_to_form("Academic Term", self.academic_term)))
def validate_duplication(self): def validate_duplication(self):
enrollment = frappe.get_all("Program Enrollment", filters={ enrollment = frappe.get_all("Program Enrollment", filters={
"student": self.student, "student": self.student,
"program": self.program, "program": self.program,
"academic_year": self.academic_year, "academic_year": self.academic_year,
"academic_term": self.academic_term,
"docstatus": ("<", 2), "docstatus": ("<", 2),
"name": ("!=", self.name) "name": ("!=", self.name)
}) })
@ -70,10 +96,9 @@ class ProgramEnrollment(Document):
def create_course_enrollments(self): def create_course_enrollments(self):
student = frappe.get_doc("Student", self.student) student = frappe.get_doc("Student", self.student)
program = frappe.get_doc("Program", self.program) course_list = [course.course for course in self.courses]
course_list = [course.course for course in program.courses]
for course_name in course_list: for course_name in course_list:
student.enroll_in_course(course_name=course_name, program_enrollment=self.name) student.enroll_in_course(course_name=course_name, program_enrollment=self.name, enrollment_date=self.enrollment_date)
def get_all_course_enrollments(self): def get_all_course_enrollments(self):
course_enrollment_names = frappe.get_list("Course Enrollment", filters={'program_enrollment': self.name}) course_enrollment_names = frappe.get_list("Course Enrollment", filters={'program_enrollment': self.name})

View File

@ -392,6 +392,9 @@ regional_overrides = {
'Italy': { 'Italy': {
'erpnext.controllers.taxes_and_totals.update_itemised_tax_data': 'erpnext.regional.italy.utils.update_itemised_tax_data', 'erpnext.controllers.taxes_and_totals.update_itemised_tax_data': 'erpnext.regional.italy.utils.update_itemised_tax_data',
'erpnext.controllers.accounts_controller.validate_regional': 'erpnext.regional.italy.utils.sales_invoice_validate', 'erpnext.controllers.accounts_controller.validate_regional': 'erpnext.regional.italy.utils.sales_invoice_validate',
},
'Germany': {
'erpnext.controllers.accounts_controller.validate_regional': 'erpnext.regional.germany.accounts_controller.validate_regional',
} }
} }
user_privacy_documents = [ user_privacy_documents = [

View File

@ -109,7 +109,6 @@
"encashment_date", "encashment_date",
"exit_interview_details", "exit_interview_details",
"held_on", "held_on",
"reason_for_resignation",
"new_workplace", "new_workplace",
"feedback", "feedback",
"lft", "lft",
@ -682,7 +681,7 @@
}, },
{ {
"fieldname": "reason_for_leaving", "fieldname": "reason_for_leaving",
"fieldtype": "Data", "fieldtype": "Small Text",
"label": "Reason for Leaving", "label": "Reason for Leaving",
"oldfieldname": "reason_for_leaving", "oldfieldname": "reason_for_leaving",
"oldfieldtype": "Data" "oldfieldtype": "Data"
@ -696,6 +695,7 @@
"options": "\nYes\nNo" "options": "\nYes\nNo"
}, },
{ {
"depends_on": "eval:doc.leave_encashed ==\"Yes\"",
"fieldname": "encashment_date", "fieldname": "encashment_date",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Encashment Date", "label": "Encashment Date",
@ -705,7 +705,6 @@
{ {
"fieldname": "exit_interview_details", "fieldname": "exit_interview_details",
"fieldtype": "Column Break", "fieldtype": "Column Break",
"label": "Exit Interview Details",
"oldfieldname": "col_brk6", "oldfieldname": "col_brk6",
"oldfieldtype": "Column Break", "oldfieldtype": "Column Break",
"width": "50%" "width": "50%"
@ -713,18 +712,10 @@
{ {
"fieldname": "held_on", "fieldname": "held_on",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Held On", "label": "Exit Interview Held On",
"oldfieldname": "held_on", "oldfieldname": "held_on",
"oldfieldtype": "Date" "oldfieldtype": "Date"
}, },
{
"fieldname": "reason_for_resignation",
"fieldtype": "Select",
"label": "Reason for Resignation",
"oldfieldname": "reason_for_resignation",
"oldfieldtype": "Select",
"options": "\nBetter Prospects\nHealth Concerns"
},
{ {
"fieldname": "new_workplace", "fieldname": "new_workplace",
"fieldtype": "Data", "fieldtype": "Data",
@ -809,37 +800,29 @@
"fieldname": "expense_approver", "fieldname": "expense_approver",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Expense Approver", "label": "Expense Approver",
"options": "User", "options": "User"
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "approvers_section", "fieldname": "approvers_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Approvers", "label": "Approvers"
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "column_break_45", "fieldname": "column_break_45",
"fieldtype": "Column Break", "fieldtype": "Column Break"
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "shift_request_approver", "fieldname": "shift_request_approver",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Shift Request Approver", "label": "Shift Request Approver",
"options": "User", "options": "User"
"show_days": 1,
"show_seconds": 1
} }
], ],
"icon": "fa fa-user", "icon": "fa fa-user",
"idx": 24, "idx": 24,
"image_field": "image", "image_field": "image",
"links": [], "links": [],
"modified": "2020-07-28 01:36:04.109189", "modified": "2020-10-06 15:58:23.805489",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "Employee", "name": "Employee",

View File

@ -1,3 +1,4 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt # License: GNU General Public License v3. See license.txt
@ -32,7 +33,7 @@ class HolidayList(Document):
def validate_days(self): def validate_days(self):
if self.from_date > self.to_date: if getdate(self.from_date) > getdate(self.to_date):
throw(_("To Date cannot be before From Date")) throw(_("To Date cannot be before From Date"))
for day in self.get("holidays"): for day in self.get("holidays"):

View File

@ -434,7 +434,8 @@ var get_bom_material_detail= function(doc, cdt, cdn, scrap_items) {
"include_item_in_manufacturing": d.include_item_in_manufacturing, "include_item_in_manufacturing": d.include_item_in_manufacturing,
"uom": d.uom, "uom": d.uom,
"stock_uom": d.stock_uom, "stock_uom": d.stock_uom,
"conversion_factor": d.conversion_factor "conversion_factor": d.conversion_factor,
"sourced_by_supplier": d.sourced_by_supplier
}, },
callback: function(r) { callback: function(r) {
d = locals[cdt][cdn]; d = locals[cdt][cdn];
@ -616,6 +617,22 @@ frappe.ui.form.on("BOM Item", "item_code", function(frm, cdt, cdn) {
refresh_field("allow_alternative_item", d.name, d.parentfield); refresh_field("allow_alternative_item", d.name, d.parentfield);
}); });
frappe.ui.form.on("BOM Item", "sourced_by_supplier", function(frm, cdt, cdn) {
var d = locals[cdt][cdn];
if (d.sourced_by_supplier) {
d.rate = 0;
refresh_field("rate", d.name, d.parentfield);
}
});
frappe.ui.form.on("BOM Item", "rate", function(frm, cdt, cdn) {
var d = locals[cdt][cdn];
if (d.sourced_by_supplier) {
d.rate = 0;
refresh_field("rate", d.name, d.parentfield);
}
});
frappe.ui.form.on("BOM Operation", "operations_remove", function(frm) { frappe.ui.form.on("BOM Operation", "operations_remove", function(frm) {
erpnext.bom.calculate_op_cost(frm.doc); erpnext.bom.calculate_op_cost(frm.doc);
erpnext.bom.calculate_total(frm.doc); erpnext.bom.calculate_total(frm.doc);

View File

@ -137,7 +137,8 @@ class BOM(WebsiteGenerator):
"qty": item.qty, "qty": item.qty,
"uom": item.uom, "uom": item.uom,
"stock_uom": item.stock_uom, "stock_uom": item.stock_uom,
"conversion_factor": item.conversion_factor "conversion_factor": item.conversion_factor,
"sourced_by_supplier": item.sourced_by_supplier
}) })
for r in ret: for r in ret:
if not item.get(r): if not item.get(r):
@ -172,7 +173,8 @@ class BOM(WebsiteGenerator):
'qty' : args.get("qty") or args.get("stock_qty") or 1, 'qty' : args.get("qty") or args.get("stock_qty") or 1,
'stock_qty' : args.get("qty") or args.get("stock_qty") or 1, 'stock_qty' : args.get("qty") or args.get("stock_qty") or 1,
'base_rate' : flt(rate) * (flt(self.conversion_rate) or 1), 'base_rate' : flt(rate) * (flt(self.conversion_rate) or 1),
'include_item_in_manufacturing': cint(args['transfer_for_manufacture']) or 0 'include_item_in_manufacturing': cint(args['transfer_for_manufacture']) or 0,
'sourced_by_supplier' : args['sourced_by_supplier'] or 0
} }
return ret_item return ret_item
@ -191,8 +193,8 @@ class BOM(WebsiteGenerator):
if arg.get('scrap_items'): if arg.get('scrap_items'):
rate = get_valuation_rate(arg) rate = get_valuation_rate(arg)
elif arg: elif arg:
#Customer Provided parts will have zero rate #Customer Provided parts and Supplier sourced parts will have zero rate
if not frappe.db.get_value('Item', arg["item_code"], 'is_customer_provided_item'): if not frappe.db.get_value('Item', arg["item_code"], 'is_customer_provided_item') and not arg.get('sourced_by_supplier'):
if arg.get('bom_no') and self.set_rate_of_sub_assembly_item_based_on_bom: if arg.get('bom_no') and self.set_rate_of_sub_assembly_item_based_on_bom:
rate = flt(self.get_bom_unitcost(arg['bom_no'])) * (arg.get("conversion_factor") or 1) rate = flt(self.get_bom_unitcost(arg['bom_no'])) * (arg.get("conversion_factor") or 1)
else: else:
@ -205,7 +207,6 @@ class BOM(WebsiteGenerator):
else: else:
frappe.msgprint(_("{0} not found for item {1}") frappe.msgprint(_("{0} not found for item {1}")
.format(self.rm_cost_as_per, arg["item_code"]), alert=True) .format(self.rm_cost_as_per, arg["item_code"]), alert=True)
return flt(rate) * flt(self.plc_conversion_rate or 1) / (self.conversion_rate or 1) return flt(rate) * flt(self.plc_conversion_rate or 1) / (self.conversion_rate or 1)
def update_cost(self, update_parent=True, from_child_bom=False, save=True): def update_cost(self, update_parent=True, from_child_bom=False, save=True):
@ -221,7 +222,8 @@ class BOM(WebsiteGenerator):
"qty": d.qty, "qty": d.qty,
"uom": d.uom, "uom": d.uom,
"stock_uom": d.stock_uom, "stock_uom": d.stock_uom,
"conversion_factor": d.conversion_factor "conversion_factor": d.conversion_factor,
"sourced_by_supplier": d.sourced_by_supplier
}) })
if rate: if rate:
@ -495,7 +497,8 @@ class BOM(WebsiteGenerator):
'stock_uom' : d.stock_uom, 'stock_uom' : d.stock_uom,
'stock_qty' : flt(d.stock_qty), 'stock_qty' : flt(d.stock_qty),
'rate' : flt(d.base_rate) / (flt(d.conversion_factor) or 1.0), 'rate' : flt(d.base_rate) / (flt(d.conversion_factor) or 1.0),
'include_item_in_manufacturing': d.include_item_in_manufacturing 'include_item_in_manufacturing': d.include_item_in_manufacturing,
'sourced_by_supplier': d.sourced_by_supplier
})) }))
def company_currency(self): def company_currency(self):
@ -521,6 +524,7 @@ class BOM(WebsiteGenerator):
bom_item.stock_qty, bom_item.stock_qty,
bom_item.rate, bom_item.rate,
bom_item.include_item_in_manufacturing, bom_item.include_item_in_manufacturing,
bom_item.sourced_by_supplier,
bom_item.stock_qty / ifnull(bom.quantity, 1) AS qty_consumed_per_unit bom_item.stock_qty / ifnull(bom.quantity, 1) AS qty_consumed_per_unit
FROM `tabBOM Explosion Item` bom_item, tabBOM bom FROM `tabBOM Explosion Item` bom_item, tabBOM bom
WHERE WHERE
@ -539,7 +543,8 @@ class BOM(WebsiteGenerator):
'stock_uom' : d['stock_uom'], 'stock_uom' : d['stock_uom'],
'stock_qty' : d['qty_consumed_per_unit'] * stock_qty, 'stock_qty' : d['qty_consumed_per_unit'] * stock_qty,
'rate' : flt(d['rate']), 'rate' : flt(d['rate']),
'include_item_in_manufacturing': d.get('include_item_in_manufacturing', 0) 'include_item_in_manufacturing': d.get('include_item_in_manufacturing', 0),
'sourced_by_supplier': d.get('sourced_by_supplier', 0)
})) }))
def add_exploded_items(self): def add_exploded_items(self):
@ -679,7 +684,7 @@ def get_bom_items_as_dict(bom, company, qty=1, fetch_exploded=1, fetch_scrap_ite
is_stock_item=is_stock_item, is_stock_item=is_stock_item,
qty_field="stock_qty", qty_field="stock_qty",
select_columns = """, bom_item.source_warehouse, bom_item.operation, select_columns = """, bom_item.source_warehouse, bom_item.operation,
bom_item.include_item_in_manufacturing, bom_item.description, bom_item.rate, bom_item.include_item_in_manufacturing, bom_item.description, bom_item.rate, bom_item.sourced_by_supplier,
(Select idx from `tabBOM Item` where item_code = bom_item.item_code and parent = %(parent)s limit 1) as idx""") (Select idx from `tabBOM Item` where item_code = bom_item.item_code and parent = %(parent)s limit 1) as idx""")
items = frappe.db.sql(query, { "parent": bom, "qty": qty, "bom": bom, "company": company }, as_dict=True) items = frappe.db.sql(query, { "parent": bom, "qty": qty, "bom": bom, "company": company }, as_dict=True)
@ -692,7 +697,7 @@ def get_bom_items_as_dict(bom, company, qty=1, fetch_exploded=1, fetch_scrap_ite
query = query.format(table="BOM Item", where_conditions="", is_stock_item=is_stock_item, query = query.format(table="BOM Item", where_conditions="", is_stock_item=is_stock_item,
qty_field="stock_qty" if fetch_qty_in_stock_uom else "qty", qty_field="stock_qty" if fetch_qty_in_stock_uom else "qty",
select_columns = """, bom_item.uom, bom_item.conversion_factor, bom_item.source_warehouse, select_columns = """, bom_item.uom, bom_item.conversion_factor, bom_item.source_warehouse,
bom_item.idx, bom_item.operation, bom_item.include_item_in_manufacturing, bom_item.idx, bom_item.operation, bom_item.include_item_in_manufacturing, bom_item.sourced_by_supplier,
bom_item.description, bom_item.base_rate as rate """) bom_item.description, bom_item.base_rate as rate """)
items = frappe.db.sql(query, { "qty": qty, "bom": bom, "company": company }, as_dict=True) items = frappe.db.sql(query, { "qty": qty, "bom": bom, "company": company }, as_dict=True)

View File

@ -10,6 +10,8 @@ from frappe.test_runner import make_test_records
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import create_stock_reconciliation from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import create_stock_reconciliation
from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost
from six import string_types from six import string_types
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
test_records = frappe.get_test_records('BOM') test_records = frappe.get_test_records('BOM')
@ -138,6 +140,73 @@ class TestBOM(unittest.TestCase):
self.assertEqual(bom.items[0].rate, 20) self.assertEqual(bom.items[0].rate, 20)
def test_subcontractor_sourced_item(self):
item_code = "_Test Subcontracted FG Item 1"
if not frappe.db.exists('Item', item_code):
make_item(item_code, {
'is_stock_item': 1,
'is_sub_contracted_item': 1,
'stock_uom': 'Nos'
})
if not frappe.db.exists('Item', "Test Extra Item 1"):
make_item("Test Extra Item 1", {
'is_stock_item': 1,
'stock_uom': 'Nos'
})
if not frappe.db.exists('Item', "Test Extra Item 2"):
make_item("Test Extra Item 2", {
'is_stock_item': 1,
'stock_uom': 'Nos'
})
if not frappe.db.exists('Item', "Test Extra Item 3"):
make_item("Test Extra Item 3", {
'is_stock_item': 1,
'stock_uom': 'Nos'
})
bom = frappe.get_doc({
'doctype': 'BOM',
'is_default': 1,
'item': item_code,
'currency': 'USD',
'quantity': 1,
'company': '_Test Company'
})
for item in ["Test Extra Item 1", "Test Extra Item 2"]:
item_doc = frappe.get_doc('Item', item)
bom.append('items', {
'item_code': item,
'qty': 1,
'uom': item_doc.stock_uom,
'stock_uom': item_doc.stock_uom,
'rate': item_doc.valuation_rate
})
bom.append('items', {
'item_code': "Test Extra Item 3",
'qty': 1,
'uom': item_doc.stock_uom,
'stock_uom': item_doc.stock_uom,
'rate': 0,
'sourced_by_supplier': 1
})
bom.insert(ignore_permissions=True)
bom.update_cost()
bom.submit()
# test that sourced_by_supplier rate is zero even after updating cost
self.assertEqual(bom.items[2].rate, 0)
# test in Purchase Order sourced_by_supplier is not added to Supplied Item
po = create_purchase_order(item_code=item_code, qty=1,
is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC")
bom_items = sorted([d.item_code for d in bom.items if d.sourced_by_supplier != 1])
supplied_items = sorted([d.rm_item_code for d in po.supplied_items])
self.assertEquals(bom_items, supplied_items)
def get_default_bom(item_code="_Test FG Item 2"): def get_default_bom(item_code="_Test FG Item 2"):
return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1}) return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1})

View File

@ -1,626 +1,181 @@
{ {
"allow_copy": 0, "actions": [],
"allow_events_in_timeline": 0, "autoname": "hash",
"allow_guest_to_view": 0, "creation": "2013-03-07 11:42:57",
"allow_import": 0, "doctype": "DocType",
"allow_rename": 0, "document_type": "Setup",
"autoname": "hash", "editable_grid": 1,
"beta": 0, "engine": "InnoDB",
"creation": "2013-03-07 11:42:57", "field_order": [
"custom": 0, "item_code",
"docstatus": 0, "item_name",
"doctype": "DocType", "cb",
"document_type": "Setup", "source_warehouse",
"editable_grid": 1, "operation",
"engine": "InnoDB", "section_break_3",
"description",
"column_break_2",
"image",
"image_view",
"section_break_4",
"stock_qty",
"rate",
"qty_consumed_per_unit",
"column_break_8",
"stock_uom",
"amount",
"include_item_in_manufacturing",
"sourced_by_supplier"
],
"fields": [ "fields": [
{ {
"allow_bulk_edit": 0, "fieldname": "item_code",
"allow_in_quick_entry": 0, "fieldtype": "Link",
"allow_on_submit": 0, "in_global_search": 1,
"bold": 0, "in_list_view": 1,
"collapsible": 0, "label": "Item Code",
"columns": 0, "oldfieldname": "item_code",
"fieldname": "item_code", "oldfieldtype": "Link",
"fieldtype": "Link", "options": "Item",
"hidden": 0, "read_only": 1
"ignore_user_permissions": 0, },
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 1,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Item Code",
"length": 0,
"no_copy": 0,
"oldfieldname": "item_code",
"oldfieldtype": "Link",
"options": "Item",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "item_name",
"allow_in_quick_entry": 0, "fieldtype": "Data",
"allow_on_submit": 0, "in_global_search": 1,
"bold": 0, "in_list_view": 1,
"collapsible": 0, "label": "Item Name",
"columns": 0, "read_only": 1
"fieldname": "item_name", },
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 1,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Item Name",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "cb",
"allow_in_quick_entry": 0, "fieldtype": "Column Break"
"allow_on_submit": 0, },
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "cb",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "source_warehouse",
"allow_in_quick_entry": 0, "fieldtype": "Link",
"allow_on_submit": 0, "label": "Source Warehouse",
"bold": 0, "options": "Warehouse",
"collapsible": 0, "read_only": 1
"columns": 0, },
"fieldname": "source_warehouse",
"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": "Source Warehouse",
"length": 0,
"no_copy": 0,
"options": "Warehouse",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "operation",
"allow_in_quick_entry": 0, "fieldtype": "Link",
"allow_on_submit": 0, "label": "Operation",
"bold": 0, "options": "Operation",
"collapsible": 0, "read_only": 1
"columns": 0, },
"fieldname": "operation",
"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": "Operation",
"length": 0,
"no_copy": 0,
"options": "Operation",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "section_break_3",
"allow_in_quick_entry": 0, "fieldtype": "Section Break"
"allow_on_submit": 0, },
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "section_break_3",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "description",
"allow_in_quick_entry": 0, "fieldtype": "Text Editor",
"allow_on_submit": 0, "in_list_view": 1,
"bold": 0, "label": "Description",
"collapsible": 0, "oldfieldname": "description",
"columns": 0, "oldfieldtype": "Text",
"fieldname": "description", "print_width": "300px",
"fieldtype": "Text Editor", "read_only": 1,
"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": "Description",
"length": 0,
"no_copy": 0,
"oldfieldname": "description",
"oldfieldtype": "Text",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": "300px",
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0,
"width": "300px" "width": "300px"
}, },
{ {
"allow_bulk_edit": 0, "fieldname": "column_break_2",
"allow_in_quick_entry": 0, "fieldtype": "Column Break"
"allow_on_submit": 0, },
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_2",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "image",
"allow_in_quick_entry": 0, "fieldtype": "Attach",
"allow_on_submit": 0, "hidden": 1,
"bold": 0, "label": "Image",
"collapsible": 0, "print_hide": 1
"columns": 0, },
"fieldname": "image",
"fieldtype": "Attach",
"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": 1,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "image_view",
"allow_in_quick_entry": 0, "fieldtype": "Image",
"allow_on_submit": 0, "label": "Image View",
"bold": 0, "options": "image"
"collapsible": 0, },
"columns": 0,
"fieldname": "image_view",
"fieldtype": "Image",
"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": "Image View",
"length": 0,
"no_copy": 0,
"options": "image",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "section_break_4",
"allow_in_quick_entry": 0, "fieldtype": "Section Break"
"allow_on_submit": 0, },
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "section_break_4",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "stock_qty",
"allow_in_quick_entry": 0, "fieldtype": "Float",
"allow_on_submit": 0, "in_list_view": 1,
"bold": 0, "label": "Stock Qty",
"collapsible": 0, "oldfieldname": "qty",
"columns": 0, "oldfieldtype": "Currency",
"fieldname": "stock_qty", "read_only": 1
"fieldtype": "Float", },
"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": "Stock Qty",
"length": 0,
"no_copy": 0,
"oldfieldname": "qty",
"oldfieldtype": "Currency",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "rate",
"allow_in_quick_entry": 0, "fieldtype": "Currency",
"allow_on_submit": 0, "in_list_view": 1,
"bold": 0, "label": "Rate",
"collapsible": 0, "oldfieldname": "standard_rate",
"columns": 0, "oldfieldtype": "Currency",
"fieldname": "rate", "options": "Company:company:default_currency",
"fieldtype": "Currency", "read_only": 1
"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": "Rate",
"length": 0,
"no_copy": 0,
"oldfieldname": "standard_rate",
"oldfieldtype": "Currency",
"options": "Company:company:default_currency",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "qty_consumed_per_unit",
"allow_in_quick_entry": 0, "fieldtype": "Float",
"allow_on_submit": 0, "in_list_view": 1,
"bold": 0, "label": "Qty Consumed Per Unit",
"collapsible": 0, "read_only": 1
"columns": 0, },
"fieldname": "qty_consumed_per_unit",
"fieldtype": "Float",
"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": "Qty Consumed Per Unit",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "column_break_8",
"allow_in_quick_entry": 0, "fieldtype": "Column Break"
"allow_on_submit": 0, },
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_8",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "stock_uom",
"allow_in_quick_entry": 0, "fieldtype": "Link",
"allow_on_submit": 0, "label": "Stock UOM",
"bold": 0, "oldfieldname": "stock_uom",
"collapsible": 0, "oldfieldtype": "Link",
"columns": 0, "options": "UOM",
"fieldname": "stock_uom", "read_only": 1
"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": "Stock UOM",
"length": 0,
"no_copy": 0,
"oldfieldname": "stock_uom",
"oldfieldtype": "Link",
"options": "UOM",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "amount",
"allow_in_quick_entry": 0, "fieldtype": "Currency",
"allow_on_submit": 0, "in_list_view": 1,
"bold": 0, "label": "Amount",
"collapsible": 0, "oldfieldname": "amount_as_per_sr",
"columns": 0, "oldfieldtype": "Currency",
"fieldname": "amount", "options": "Company:company:default_currency",
"fieldtype": "Currency", "read_only": 1
"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": "Amount",
"length": 0,
"no_copy": 0,
"oldfieldname": "amount_as_per_sr",
"oldfieldtype": "Currency",
"options": "Company:company:default_currency",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "default": "0",
"allow_in_quick_entry": 0, "fieldname": "include_item_in_manufacturing",
"allow_on_submit": 0, "fieldtype": "Check",
"bold": 0, "label": "Include Item In Manufacturing",
"collapsible": 0, "read_only": 1
"columns": 0, },
"fieldname": "include_item_in_manufacturing", {
"fieldtype": "Check", "default": "0",
"hidden": 0, "fieldname": "sourced_by_supplier",
"ignore_user_permissions": 0, "fieldtype": "Check",
"ignore_xss_filter": 0, "label": "Sourced by Supplier",
"in_filter": 0, "read_only": 1
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Include Item In Manufacturing",
"length": 0,
"no_copy": 0,
"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
} }
], ],
"has_web_view": 0, "idx": 1,
"hide_heading": 0, "index_web_pages_for_search": 1,
"hide_toolbar": 0, "istable": 1,
"idx": 1, "links": [],
"image_view": 0, "modified": "2020-10-08 16:21:29.386212",
"in_create": 0, "modified_by": "Administrator",
"is_submittable": 0, "module": "Manufacturing",
"issingle": 0, "name": "BOM Explosion Item",
"istable": 1, "owner": "Administrator",
"max_attachments": 0, "permissions": [],
"modified": "2018-11-20 19:04:59.813773", "sort_field": "modified",
"modified_by": "Administrator", "sort_order": "DESC",
"module": "Manufacturing", "track_changes": 1
"name": "BOM Explosion Item",
"owner": "Administrator",
"permissions": [],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"track_changes": 1,
"track_seen": 0,
"track_views": 0
} }

View File

@ -37,7 +37,9 @@
"section_break_27", "section_break_27",
"has_variants", "has_variants",
"include_item_in_manufacturing", "include_item_in_manufacturing",
"original_item" "original_item",
"column_break_33",
"sourced_by_supplier"
], ],
"fields": [ "fields": [
{ {
@ -272,12 +274,23 @@
"no_copy": 1, "no_copy": 1,
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
},
{
"fieldname": "column_break_33",
"fieldtype": "Column Break"
},
{
"default": "0",
"fieldname": "sourced_by_supplier",
"fieldtype": "Check",
"label": "Sourced by Supplier"
} }
], ],
"idx": 1, "idx": 1,
"index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-04-09 14:30:26.535546", "modified": "2020-10-08 14:19:37.563300",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "BOM Item", "name": "BOM Item",

View File

@ -381,7 +381,6 @@ class ProductionPlan(Document):
"transaction_date": nowdate(), "transaction_date": nowdate(),
"status": "Draft", "status": "Draft",
"company": self.company, "company": self.company,
"requested_by": frappe.session.user,
'material_request_type': material_request_type, 'material_request_type': material_request_type,
'customer': item_doc.customer or '' 'customer': item_doc.customer or ''
}) })

View File

@ -434,7 +434,7 @@ class WorkOrder(Document):
elif flt(d.completed_qty) <= max_allowed_qty_for_wo: elif flt(d.completed_qty) <= max_allowed_qty_for_wo:
d.status = "Completed" d.status = "Completed"
else: else:
frappe.throw(_("Completed Qty can not be greater than 'Qty to Manufacture'")) frappe.throw(_("Completed Qty cannot be greater than 'Qty to Manufacture'"))
def set_actual_dates(self): def set_actual_dates(self):
if self.get("operations"): if self.get("operations"):

View File

@ -729,3 +729,4 @@ erpnext.patches.v13_0.setting_custom_roles_for_some_regional_reports
erpnext.patches.v13_0.rename_issue_doctype_fields erpnext.patches.v13_0.rename_issue_doctype_fields
erpnext.patches.v13_0.change_default_pos_print_format erpnext.patches.v13_0.change_default_pos_print_format
erpnext.patches.v13_0.set_youtube_video_id erpnext.patches.v13_0.set_youtube_video_id
erpnext.patches.v13_0.print_uom_after_quantity_patch

View File

@ -0,0 +1,10 @@
# Copyright (c) 2019, Frappe and Contributors
# License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
import frappe
from erpnext.setup.install import create_print_uom_after_qty_custom_field
def execute():
create_print_uom_after_qty_custom_field()

View File

@ -217,7 +217,7 @@
"fieldname": "help", "fieldname": "help",
"fieldtype": "HTML", "fieldtype": "HTML",
"label": "Help", "label": "Help",
"options": "<h3>Help</h3>\n\n<p>Notes:</p>\n\n<ol>\n<li>Use field <code>base</code> for using base salary of the Employee</li>\n<li>Use Salary Component abbreviations in conditions and formulas. <code>BS = Basic Salary</code></li>\n<li>Use field name for employee details in conditions and formulas. <code>Employment Type = employment_type</code><code>Branch = branch</code></li>\n<li>Use field name from Salary Slip in conditions and formulas. <code>Payment Days = payment_days</code><code>Leave without pay = leave_without_pay</code></li>\n<li>Direct Amount can also be entered based on Condtion. See example 3</li></ol>\n\n<h4>Examples</h4>\n<ol>\n<li>Calculating Basic Salary based on <code>base</code>\n<pre><code>Condition: base &lt; 10000</code></pre>\n<pre><code>Formula: base * .2</code></pre></li>\n<li>Calculating HRA based on Basic Salary<code>BS</code> \n<pre><code>Condition: BS &gt; 2000</code></pre>\n<pre><code>Formula: BS * .1</code></pre></li>\n<li>Calculating TDS based on Employment Type<code>employment_type</code> \n<pre><code>Condition: employment_type==\"Intern\"</code></pre>\n<pre><code>Amount: 1000</code></pre></li>\n</ol>" "options": "<h3>Help</h3>\n\n<p>Notes:</p>\n\n<ol>\n<li>Use field <code>base</code> for using base salary of the Employee</li>\n<li>Use Salary Component abbreviations in conditions and formulas. <code>BS = Basic Salary</code></li>\n<li>Use field name for employee details in conditions and formulas. <code>Employment Type = employment_type</code><code>Branch = branch</code></li>\n<li>Use field name from Salary Slip in conditions and formulas. <code>Payment Days = payment_days</code><code>Leave without pay = leave_without_pay</code></li>\n<li>Direct Amount can also be entered based on Condition. See example 3</li></ol>\n\n<h4>Examples</h4>\n<ol>\n<li>Calculating Basic Salary based on <code>base</code>\n<pre><code>Condition: base &lt; 10000</code></pre>\n<pre><code>Formula: base * .2</code></pre></li>\n<li>Calculating HRA based on Basic Salary<code>BS</code> \n<pre><code>Condition: BS &gt; 2000</code></pre>\n<pre><code>Formula: BS * .1</code></pre></li>\n<li>Calculating TDS based on Employment Type<code>employment_type</code> \n<pre><code>Condition: employment_type==\"Intern\"</code></pre>\n<pre><code>Amount: 1000</code></pre></li>\n</ol>"
}, },
{ {
"default": "0", "default": "0",
@ -238,14 +238,13 @@
"depends_on": "eval:doc.type == \"Deduction\"", "depends_on": "eval:doc.type == \"Deduction\"",
"fieldname": "is_income_tax_component", "fieldname": "is_income_tax_component",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Is Income Tax Component", "label": "Is Income Tax Component"
"show_days": 1,
"show_seconds": 1
} }
], ],
"icon": "fa fa-flag", "icon": "fa fa-flag",
"index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2020-06-22 15:39:20.826565", "modified": "2020-10-07 20:38:33.795853",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Payroll", "module": "Payroll",
"name": "Salary Component", "name": "Salary Component",

View File

@ -117,7 +117,7 @@
"depends_on": "eval:doc.is_flexible_benefit != 1", "depends_on": "eval:doc.is_flexible_benefit != 1",
"fieldname": "section_break_2", "fieldname": "section_break_2",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Condtion and formula" "label": "Condition and formula"
}, },
{ {
"allow_on_submit": 1, "allow_on_submit": 1,
@ -206,38 +206,28 @@
"collapsible": 1, "collapsible": 1,
"fieldname": "section_break_5", "fieldname": "section_break_5",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Component properties and references ", "label": "Component properties and references "
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "column_break_11", "fieldname": "column_break_11",
"fieldtype": "Column Break", "fieldtype": "Column Break"
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "section_break_19", "fieldname": "section_break_19",
"fieldtype": "Section Break", "fieldtype": "Section Break"
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "column_break_18", "fieldname": "column_break_18",
"fieldtype": "Column Break", "fieldtype": "Column Break"
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "column_break_24", "fieldname": "column_break_24",
"fieldtype": "Column Break", "fieldtype": "Column Break"
"show_days": 1,
"show_seconds": 1
} }
], ],
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-07-01 12:13:41.956495", "modified": "2020-10-07 20:39:41.619283",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Payroll", "module": "Payroll",
"name": "Salary Detail", "name": "Salary Detail",

View File

@ -347,8 +347,7 @@ class TestSalarySlip(unittest.TestCase):
# create additional salary of 150000 # create additional salary of 150000
frappe.db.sql("""delete from `tabSalary Slip` where employee=%s""", (employee)) frappe.db.sql("""delete from `tabSalary Slip` where employee=%s""", (employee))
data["additional-1"] = create_additional_salary(employee, payroll_period, 50000) data["additional-1"] = create_additional_salary(employee, payroll_period, 150000)
data["additional-2"] = create_additional_salary(employee, payroll_period, 100000)
data["deducted_dates"] = create_salary_slips_for_payroll_period(employee, data["deducted_dates"] = create_salary_slips_for_payroll_period(employee,
salary_structure.name, payroll_period) salary_structure.name, payroll_period)

View File

@ -136,6 +136,7 @@ def get_timesheet_details(filters, timesheet_list):
return timesheet_details_map return timesheet_details_map
def get_billable_and_total_duration(activity, start_time, end_time): def get_billable_and_total_duration(activity, start_time, end_time):
precision = frappe.get_precision("Timesheet Detail", "hours")
activity_duration = time_diff_in_hours(end_time, start_time) activity_duration = time_diff_in_hours(end_time, start_time)
billing_duration = 0.0 billing_duration = 0.0
if activity.billable: if activity.billable:
@ -143,4 +144,4 @@ def get_billable_and_total_duration(activity, start_time, end_time):
if activity_duration != activity.billing_hours: if activity_duration != activity.billing_hours:
billing_duration = activity_duration * activity.billing_hours / activity.hours billing_duration = activity_duration * activity.billing_hours / activity.hours
return flt(activity_duration, 2), flt(billing_duration, 2) return flt(activity_duration, precision), flt(billing_duration, precision)

View File

@ -309,7 +309,6 @@ erpnext.setup.fiscal_years = {
"Hong Kong": ["04-01", "03-31"], "Hong Kong": ["04-01", "03-31"],
"India": ["04-01", "03-31"], "India": ["04-01", "03-31"],
"Iran": ["06-23", "06-22"], "Iran": ["06-23", "06-22"],
"Italy": ["07-01", "06-30"],
"Myanmar": ["04-01", "03-31"], "Myanmar": ["04-01", "03-31"],
"New Zealand": ["04-01", "03-31"], "New Zealand": ["04-01", "03-31"],
"Pakistan": ["07-01", "06-30"], "Pakistan": ["07-01", "06-30"],

View File

@ -452,6 +452,9 @@ erpnext.utils.update_child_items = function(opts) {
const frm = opts.frm; const frm = opts.frm;
const cannot_add_row = (typeof opts.cannot_add_row === 'undefined') ? true : opts.cannot_add_row; const cannot_add_row = (typeof opts.cannot_add_row === 'undefined') ? true : opts.cannot_add_row;
const child_docname = (typeof opts.cannot_add_row === 'undefined') ? "items" : opts.child_docname; const child_docname = (typeof opts.cannot_add_row === 'undefined') ? "items" : opts.child_docname;
const child_meta = frappe.get_meta(`${frm.doc.doctype} Item`);
const get_precision = (fieldname) => child_meta.fields.find(f => f.fieldname == fieldname).precision;
this.data = []; this.data = [];
const fields = [{ const fields = [{
fieldtype:'Data', fieldtype:'Data',
@ -499,14 +502,16 @@ erpnext.utils.update_child_items = function(opts) {
default: 0, default: 0,
read_only: 0, read_only: 0,
in_list_view: 1, in_list_view: 1,
label: __('Qty') label: __('Qty'),
precision: get_precision("qty")
}, { }, {
fieldtype:'Currency', fieldtype:'Currency',
fieldname:"rate", fieldname:"rate",
default: 0, default: 0,
read_only: 0, read_only: 0,
in_list_view: 1, in_list_view: 1,
label: __('Rate') label: __('Rate'),
precision: get_precision("rate")
}]; }];
if (frm.doc.doctype == 'Sales Order' || frm.doc.doctype == 'Purchase Order' ) { if (frm.doc.doctype == 'Sales Order' || frm.doc.doctype == 'Purchase Order' ) {
@ -521,7 +526,8 @@ erpnext.utils.update_child_items = function(opts) {
fieldtype: 'Float', fieldtype: 'Float',
fieldname: "conversion_factor", fieldname: "conversion_factor",
in_list_view: 1, in_list_view: 1,
label: __("Conversion Factor") label: __("Conversion Factor"),
precision: get_precision('conversion_factor')
}) })
} }
@ -697,9 +703,13 @@ erpnext.utils.map_current_doc = function(opts) {
} }
frappe.form.link_formatters['Item'] = function(value, doc) { frappe.form.link_formatters['Item'] = function(value, doc) {
if(doc && doc.item_name && doc.item_name !== value) { if (doc && value && doc.item_name && doc.item_name !== value) {
return value? value + ': ' + doc.item_name: doc.item_name; return value + ': ' + doc.item_name;
} else if (!value && doc.doctype && doc.item_name) {
// format blank value in child table
return doc.item_name;
} else { } else {
// if value is blank in report view or item code and name are the same, return as is
return value; return value;
} }
} }

View File

@ -10,5 +10,13 @@ frappe.ui.form.on('Quality Procedure', {
} }
}; };
}); });
frm.set_query('parent_quality_procedure', function(){
return {
filters: {
is_group: 1
}
};
});
} }
}); });

View File

@ -21,8 +21,7 @@
"fieldname": "parent_quality_procedure", "fieldname": "parent_quality_procedure",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Parent Procedure", "label": "Parent Procedure",
"options": "Quality Procedure", "options": "Quality Procedure"
"read_only": 1
}, },
{ {
"default": "0", "default": "0",
@ -73,7 +72,7 @@
], ],
"is_tree": 1, "is_tree": 1,
"links": [], "links": [],
"modified": "2020-06-17 17:25:03.434953", "modified": "2020-10-13 11:46:07.744194",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Quality Management", "module": "Quality Management",
"name": "Quality Procedure", "name": "Quality Procedure",

View File

@ -4,7 +4,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
from frappe.utils.nestedset import NestedSet from frappe.utils.nestedset import NestedSet, rebuild_tree
from frappe import _ from frappe import _
class QualityProcedure(NestedSet): class QualityProcedure(NestedSet):
@ -42,6 +42,8 @@ class QualityProcedure(NestedSet):
doc.save(ignore_permissions=True) doc.save(ignore_permissions=True)
def set_parent(self): def set_parent(self):
rebuild_tree('Quality Procedure', 'parent_quality_procedure')
for process in self.processes: for process in self.processes:
# Set parent for only those children who don't have a parent # Set parent for only those children who don't have a parent
parent_quality_procedure = frappe.db.get_value("Quality Procedure", process.procedure, "parent_quality_procedure") parent_quality_procedure = frappe.db.get_value("Quality Procedure", process.procedure, "parent_quality_procedure")

View File

@ -0,0 +1,4 @@
{% if address_line1 %}{{ address_line1 }}<br>{% endif -%}
{% if address_line2 %}{{ address_line2 }}<br>{% endif -%}
{% if pincode %}L-{{ pincode }}{% endif -%}{% if city %} {{ city }}{% endif %}<br>
{% if country %}{{ country | upper }}{% endif %}

View File

@ -0,0 +1,57 @@
import frappe
from frappe import _
from frappe import msgprint
REQUIRED_FIELDS = {
"Sales Invoice": [
{
"field_name": "company_address",
"regulation": "§ 14 Abs. 4 Nr. 1 UStG"
},
{
"field_name": "company_tax_id",
"regulation": "§ 14 Abs. 4 Nr. 2 UStG"
},
{
"field_name": "taxes",
"regulation": "§ 14 Abs. 4 Nr. 8 UStG",
"condition": "not exempt_from_sales_tax"
},
{
"field_name": "customer_address",
"regulation": "§ 14 Abs. 4 Nr. 1 UStG",
"condition": "base_grand_total > 250"
}
]
}
def validate_regional(doc):
"""Check if required fields for this document are present."""
required_fields = REQUIRED_FIELDS.get(doc.doctype)
if not required_fields:
return
meta = frappe.get_meta(doc.doctype)
field_map = {field.fieldname: field.label for field in meta.fields}
for field in required_fields:
condition = field.get("condition")
if condition and not frappe.safe_eval(condition, doc.as_dict()):
continue
field_name = field.get("field_name")
regulation = field.get("regulation")
if field_name and not doc.get(field_name):
missing(field_map.get(field_name), regulation)
def missing(field_label, regulation):
"""Notify the user that a required field is missing."""
context = 'Specific for Germany. Example: Remember to set Company Tax ID. It is required by § 14 Abs. 4 Nr. 2 UStG.'
msgprint(_('Remember to set {field_label}. It is required by {regulation}.', context=context).format(
field_label=frappe.bold(_(field_label)),
regulation=regulation
)
)

View File

@ -132,7 +132,7 @@ class Customer(TransactionBase):
address = frappe.get_doc('Address', address_name.get('name')) address = frappe.get_doc('Address', address_name.get('name'))
if not address.has_link('Customer', self.name): if not address.has_link('Customer', self.name):
address.append('links', dict(link_doctype='Customer', link_name=self.name)) address.append('links', dict(link_doctype='Customer', link_name=self.name))
address.save() address.save(ignore_permissions=self.flags.ignore_permissions)
lead = frappe.db.get_value("Lead", self.lead_name, ["organization_lead", "lead_name", "email_id", "phone", "mobile_no", "gender", "salutation"], as_dict=True) lead = frappe.db.get_value("Lead", self.lead_name, ["organization_lead", "lead_name", "email_id", "phone", "mobile_no", "gender", "salutation"], as_dict=True)
@ -150,7 +150,7 @@ class Customer(TransactionBase):
contact = frappe.get_doc('Contact', contact_name.get('name')) contact = frappe.get_doc('Contact', contact_name.get('name'))
if not contact.has_link('Customer', self.name): if not contact.has_link('Customer', self.name):
contact.append('links', dict(link_doctype='Customer', link_name=self.name)) contact.append('links', dict(link_doctype='Customer', link_name=self.name))
contact.save() contact.save(ignore_permissions=self.flags.ignore_permissions)
else: else:
lead.lead_name = lead.lead_name.lstrip().split(" ") lead.lead_name = lead.lead_name.lstrip().split(" ")

View File

@ -46,7 +46,7 @@ QUnit.test("test: quotation", function (assert) {
assert.ok(cur_frm.doc.items[0].rate == 200, "Price Changed Manually"); assert.ok(cur_frm.doc.items[0].rate == 200, "Price Changed Manually");
assert.equal(cur_frm.doc.total, 1000, "New Total Calculated"); assert.equal(cur_frm.doc.total, 1000, "New Total Calculated");
// Check Terms and Condtions // Check Terms and Conditions
assert.ok(cur_frm.doc.tc_name == "Test Term 1", "Terms and Conditions Checked"); assert.ok(cur_frm.doc.tc_name == "Test Term 1", "Terms and Conditions Checked");
assert.ok(cur_frm.doc.payment_terms_template, "Payment Terms Template is correct"); assert.ok(cur_frm.doc.payment_terms_template, "Payment Terms Template is correct");

View File

@ -989,7 +989,6 @@ def make_raw_material_request(items, company, sales_order, project=None):
doctype = 'Material Request', doctype = 'Material Request',
transaction_date = nowdate(), transaction_date = nowdate(),
company = company, company = company,
requested_by = frappe.session.user,
material_request_type = 'Purchase' material_request_type = 'Purchase'
)) ))
for item in raw_materials: for item in raw_materials:

View File

@ -403,6 +403,22 @@ class TestSalesOrder(unittest.TestCase):
trans_item = json.dumps([{'item_code' : '_Test Item', 'rate' : 200, 'qty' : 2, 'docname': so.items[0].name}]) trans_item = json.dumps([{'item_code' : '_Test Item', 'rate' : 200, 'qty' : 2, 'docname': so.items[0].name}])
self.assertRaises(frappe.ValidationError, update_child_qty_rate,'Sales Order', trans_item, so.name) self.assertRaises(frappe.ValidationError, update_child_qty_rate,'Sales Order', trans_item, so.name)
def test_update_child_with_precision(self):
from frappe.model.meta import get_field_precision
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
precision = get_field_precision(frappe.get_meta("Sales Order Item").get_field("rate"))
make_property_setter("Sales Order Item", "rate", "precision", 7, "Currency")
so = make_sales_order(item_code= "_Test Item", qty=4, rate=200.34664)
trans_item = json.dumps([{'item_code' : '_Test Item', 'rate' : 200.34669, 'qty' : 4, 'docname': so.items[0].name}])
update_child_qty_rate('Sales Order', trans_item, so.name)
so.reload()
self.assertEqual(so.items[0].rate, 200.34669)
make_property_setter("Sales Order Item", "rate", "precision", precision, "Currency")
def test_update_child_perm(self): def test_update_child_perm(self):
so = make_sales_order(item_code= "_Test Item", qty=4) so = make_sales_order(item_code= "_Test Item", qty=4)

View File

@ -43,7 +43,7 @@
{ {
"hidden": 0, "hidden": 0,
"label": "Data Import and Settings", "label": "Data Import and Settings",
"links": "[\n {\n \"description\": \"Import Data from CSV / Excel files.\",\n \"icon\": \"octicon octicon-cloud-upload\",\n \"label\": \"Import Data\",\n \"name\": \"Data Import\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Import Chart Of Accounts from CSV / Excel files\",\n \"labe\": \"Chart Of Accounts Importer\",\n \"label\": \"Chart of Accounts Importer\",\n \"name\": \"Chart of Accounts Importer\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Letter Heads for print templates.\",\n \"label\": \"Letter Head\",\n \"name\": \"Letter Head\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Add / Manage Email Accounts.\",\n \"label\": \"Email Account\",\n \"name\": \"Email Account\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n }\n]" "links": "[\n {\n \"description\": \"Import Data from CSV / Excel files.\",\n \"icon\": \"octicon octicon-cloud-upload\",\n \"label\": \"Import Data\",\n \"name\": \"Data Import\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Import Chart of Accounts from CSV / Excel files\",\n \"label\": \"Chart of Accounts Importer\",\n \"label\": \"Chart of Accounts Importer\",\n \"name\": \"Chart of Accounts Importer\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Letter Heads for print templates.\",\n \"label\": \"Letter Head\",\n \"name\": \"Letter Head\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Add / Manage Email Accounts.\",\n \"label\": \"Email Account\",\n \"name\": \"Email Account\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n }\n]"
} }
], ],
"category": "Modules", "category": "Modules",

View File

@ -261,14 +261,14 @@
{ {
"fieldname": "create_chart_of_accounts_based_on", "fieldname": "create_chart_of_accounts_based_on",
"fieldtype": "Select", "fieldtype": "Select",
"label": "Create Chart Of Accounts Based On", "label": "Create Chart of Accounts Based on",
"options": "\nStandard Template\nExisting Company" "options": "\nStandard Template\nExisting Company"
}, },
{ {
"depends_on": "eval:doc.create_chart_of_accounts_based_on===\"Standard Template\"", "depends_on": "eval:doc.create_chart_of_accounts_based_on===\"Standard Template\"",
"fieldname": "chart_of_accounts", "fieldname": "chart_of_accounts",
"fieldtype": "Select", "fieldtype": "Select",
"label": "Chart Of Accounts Template", "label": "Chart of Accounts Template",
"no_copy": 1 "no_copy": 1
}, },
{ {

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