diff --git a/erpnext/accounts/desk_page/accounting/accounting.json b/erpnext/accounts/desk_page/accounting/accounting.json index a2497838ee..3f23ba9019 100644 --- a/erpnext/accounts/desk_page/accounting/accounting.json +++ b/erpnext/accounts/desk_page/accounting/accounting.json @@ -98,7 +98,7 @@ "idx": 0, "is_standard": 1, "label": "Accounting", - "modified": "2020-06-19 12:42:44.054598", + "modified": "2020-09-09 11:45:33.766400", "modified_by": "Administrator", "module": "Accounts", "name": "Accounting", @@ -147,15 +147,10 @@ "link_to": "Trial Balance", "type": "Report" }, - { - "label": "Point of Sale", - "link_to": "point-of-sale", - "type": "Page" - }, { "label": "Dashboard", "link_to": "Accounts", "type": "Dashboard" } ] -} +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/bank_statement_transaction_entry/bank_statement_transaction_entry.py b/erpnext/accounts/doctype/bank_statement_transaction_entry/bank_statement_transaction_entry.py index 5b6eb9dc24..27dd8e463f 100644 --- a/erpnext/accounts/doctype/bank_statement_transaction_entry/bank_statement_transaction_entry.py +++ b/erpnext/accounts/doctype/bank_statement_transaction_entry/bank_statement_transaction_entry.py @@ -55,7 +55,7 @@ class BankStatementTransactionEntry(Document): def populate_payment_entries(self): if self.bank_statement is None: return - filename = self.bank_statement.split("/")[-1] + file_url = self.bank_statement if (len(self.new_transaction_items + self.reconciled_transaction_items) > 0): frappe.throw(_("Transactions already retreived from the statement")) @@ -65,7 +65,7 @@ class BankStatementTransactionEntry(Document): if self.bank_settings: mapped_items = frappe.get_doc("Bank Statement Settings", self.bank_settings).mapped_items statement_headers = self.get_statement_headers() - transactions = get_transaction_entries(filename, statement_headers) + transactions = get_transaction_entries(file_url, statement_headers) for entry in transactions: date = entry[statement_headers["Date"]].strip() #print("Processing entry DESC:{0}-W:{1}-D:{2}-DT:{3}".format(entry["Particulars"], entry["Withdrawals"], entry["Deposits"], entry["Date"])) @@ -398,20 +398,21 @@ def get_transaction_info(headers, header_index, row): transaction[header] = "" return transaction -def get_transaction_entries(filename, headers): +def get_transaction_entries(file_url, headers): header_index = {} rows, transactions = [], [] - if (filename.lower().endswith("xlsx")): + if (file_url.lower().endswith("xlsx")): from frappe.utils.xlsxutils import read_xlsx_file_from_attached_file - rows = read_xlsx_file_from_attached_file(file_id=filename) - elif (filename.lower().endswith("csv")): + rows = read_xlsx_file_from_attached_file(file_url=file_url) + elif (file_url.lower().endswith("csv")): from frappe.utils.csvutils import read_csv_content - _file = frappe.get_doc("File", {"file_name": filename}) + _file = frappe.get_doc("File", {"file_url": file_url}) filepath = _file.get_full_path() with open(filepath,'rb') as csvfile: rows = read_csv_content(csvfile.read()) - elif (filename.lower().endswith("xls")): + elif (file_url.lower().endswith("xls")): + filename = file_url.split("/")[-1] rows = get_rows_from_xls_file(filename) else: frappe.throw(_("Only .csv and .xlsx files are supported currently")) diff --git a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py index 0fe57c3239..27546335c9 100644 --- a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py +++ b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py @@ -91,15 +91,11 @@ class TestBankTransaction(unittest.TestCase): self.assertEqual(frappe.db.get_value("Bank Transaction", bank_transaction.name, "unallocated_amount"), 0) self.assertTrue(frappe.db.get_value("Sales Invoice Payment", dict(parent=payment.name), "clearance_date") is not None) -def add_transactions(): - if frappe.flags.test_bank_transactions_created: - return - - frappe.set_user("Administrator") +def create_bank_account(bank_name="Citi Bank", account_name="_Test Bank - _TC"): try: frappe.get_doc({ "doctype": "Bank", - "bank_name":"Citi Bank", + "bank_name":bank_name, }).insert() except frappe.DuplicateEntryError: pass @@ -108,12 +104,19 @@ def add_transactions(): frappe.get_doc({ "doctype": "Bank Account", "account_name":"Checking Account", - "bank": "Citi Bank", - "account": "_Test Bank - _TC" + "bank": bank_name, + "account": account_name }).insert() except frappe.DuplicateEntryError: pass +def add_transactions(): + if frappe.flags.test_bank_transactions_created: + return + + frappe.set_user("Administrator") + create_bank_account() + doc = frappe.get_doc({ "doctype": "Bank Transaction", "description":"1512567 BG/000002918 OPSKATTUZWXXX AT776000000098709837 Herr G", diff --git a/erpnext/accounts/doctype/coupon_code/test_coupon_code.py b/erpnext/accounts/doctype/coupon_code/test_coupon_code.py index 3a0d4162ae..340b9dd58a 100644 --- a/erpnext/accounts/doctype/coupon_code/test_coupon_code.py +++ b/erpnext/accounts/doctype/coupon_code/test_coupon_code.py @@ -9,6 +9,8 @@ from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_orde from erpnext.stock.get_item_details import get_item_details from frappe.test_runner import make_test_objects +test_dependencies = ['Item'] + def test_create_test_data(): frappe.set_user("Administrator") # create test item @@ -95,7 +97,6 @@ def test_create_test_data(): }) coupon_code.insert() - class TestCouponCode(unittest.TestCase): def setUp(self): test_create_test_data() diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js index a09face791..409c15f75c 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.js +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js @@ -638,20 +638,12 @@ $.extend(erpnext.journal_entry, { return { filters: filters }; }, - reverse_journal_entry: function(frm) { - var me = frm.doc; - for(var i=0; i { let pos_docs = r.message; - set_form_data(pos_docs, frm) - refresh_fields(frm) - set_html_data(frm) + set_form_data(pos_docs, frm); + refresh_fields(frm); + set_html_data(frm); } }) } }); +cur_frm.cscript.before_pos_transactions_remove = function(doc, cdt, cdn) { + const removed_row = locals[cdt][cdn]; + + if (!removed_row.pos_invoice) return; + + frappe.db.get_doc("POS Invoice", removed_row.pos_invoice).then(doc => { + cur_frm.doc.grand_total -= flt(doc.grand_total); + cur_frm.doc.net_total -= flt(doc.net_total); + cur_frm.doc.total_quantity -= flt(doc.total_qty); + refresh_payments(doc, cur_frm, 1); + refresh_taxes(doc, cur_frm, 1); + refresh_fields(cur_frm); + set_html_data(cur_frm); + }); +} + +frappe.ui.form.on('POS Invoice Reference', { + pos_invoice(frm, cdt, cdn) { + const added_row = locals[cdt][cdn]; + + if (!added_row.pos_invoice) return; + + frappe.db.get_doc("POS Invoice", added_row.pos_invoice).then(doc => { + frm.doc.grand_total += flt(doc.grand_total); + frm.doc.net_total += flt(doc.net_total); + frm.doc.total_quantity += flt(doc.total_qty); + refresh_payments(doc, frm); + refresh_taxes(doc, frm); + refresh_fields(frm); + set_html_data(frm); + }); + } +}) + frappe.ui.form.on('POS Closing Entry Detail', { closing_amount: (frm, cdt, cdn) => { const row = locals[cdt][cdn]; @@ -76,8 +110,8 @@ function set_form_data(data, frm) { frm.doc.grand_total += flt(d.grand_total); frm.doc.net_total += flt(d.net_total); frm.doc.total_quantity += flt(d.total_qty); - add_to_payments(d, frm); - add_to_taxes(d, frm); + refresh_payments(d, frm); + refresh_taxes(d, frm); }); } @@ -90,11 +124,12 @@ function add_to_pos_transaction(d, frm) { }) } -function add_to_payments(d, frm) { +function refresh_payments(d, frm, remove) { d.payments.forEach(p => { const payment = frm.doc.payment_reconciliation.find(pay => pay.mode_of_payment === p.mode_of_payment); if (payment) { - payment.expected_amount += flt(p.amount); + if (!remove) payment.expected_amount += flt(p.amount); + else payment.expected_amount -= flt(p.amount); } else { frm.add_child("payment_reconciliation", { mode_of_payment: p.mode_of_payment, @@ -105,11 +140,12 @@ function add_to_payments(d, frm) { }) } -function add_to_taxes(d, frm) { +function refresh_taxes(d, frm, remove) { d.taxes.forEach(t => { const tax = frm.doc.taxes.find(tx => tx.account_head === t.account_head && tx.rate === t.rate); if (tax) { - tax.amount += flt(t.tax_amount); + if (!remove) tax.amount += flt(t.tax_amount); + else tax.amount -= flt(t.tax_amount); } else { frm.add_child("taxes", { account_head: t.account_head, diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json index 2a2e3df8ae..4780688471 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json @@ -279,7 +279,8 @@ "fieldtype": "Check", "label": "Is Return (Credit Note)", "no_copy": 1, - "print_hide": 1 + "print_hide": 1, + "set_only_once": 1 }, { "fieldname": "column_break1", @@ -1578,9 +1579,10 @@ } ], "icon": "fa fa-file-text", + "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-05-29 15:08:39.337385", + "modified": "2020-09-07 12:43:09.138720", "modified_by": "Administrator", "module": "Accounts", "name": "POS Invoice", diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index 8680b710ac..ba68df7673 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -21,7 +21,7 @@ from six import iteritems class POSInvoice(SalesInvoice): def __init__(self, *args, **kwargs): super(POSInvoice, self).__init__(*args, **kwargs) - + def validate(self): if not cint(self.is_pos): frappe.throw(_("POS Invoice should have {} field checked.").format(frappe.bold("Include Payment"))) @@ -58,7 +58,7 @@ class POSInvoice(SalesInvoice): if self.redeem_loyalty_points and self.loyalty_points: self.apply_loyalty_points() self.set_status(update=True) - + def on_cancel(self): # run on cancel method of selling controller super(SalesInvoice, self).on_cancel() @@ -68,10 +68,10 @@ class POSInvoice(SalesInvoice): against_psi_doc = frappe.get_doc("POS Invoice", self.return_against) against_psi_doc.delete_loyalty_point_entry() against_psi_doc.make_loyalty_point_entry() - + def validate_stock_availablility(self): allow_negative_stock = frappe.db.get_value('Stock Settings', None, 'allow_negative_stock') - + for d in self.get('items'): if d.serial_no: filters = { @@ -89,11 +89,11 @@ class POSInvoice(SalesInvoice): for s in serial_nos: if s in reserved_serial_nos: invalid_serial_nos.append(s) - + if len(invalid_serial_nos): multiple_nos = 's' if len(invalid_serial_nos) > 1 else '' frappe.throw(_("Row #{}: Serial No{}. {} has already been transacted into another POS Invoice. \ - Please select valid serial no.".format(d.idx, multiple_nos, + Please select valid serial no.".format(d.idx, multiple_nos, frappe.bold(', '.join(invalid_serial_nos)))), title=_("Not Available")) else: if allow_negative_stock: @@ -105,9 +105,9 @@ class POSInvoice(SalesInvoice): .format(d.idx, frappe.bold(d.item_code), frappe.bold(d.warehouse))), title=_("Not Available")) elif flt(available_stock) < flt(d.qty): frappe.msgprint(_('Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}. \ - Available quantity {}.'.format(d.idx, frappe.bold(d.item_code), + Available quantity {}.'.format(d.idx, frappe.bold(d.item_code), frappe.bold(d.warehouse), frappe.bold(d.qty))), title=_("Not Available")) - + def validate_serialised_or_batched_item(self): for d in self.get("items"): serialized = d.get("has_serial_no") @@ -125,7 +125,7 @@ class POSInvoice(SalesInvoice): if batched and no_batch_selected: frappe.throw(_('Row #{}: No batch selected against item: {}. Please select a batch or remove it to complete transaction.' .format(d.idx, frappe.bold(d.item_code))), title=_("Invalid Item")) - + def validate_return_items(self): if not self.get("is_return"): return @@ -158,7 +158,7 @@ class POSInvoice(SalesInvoice): frappe.throw(_("Row #{0} (Payment Table): Amount must be positive").format(entry.idx)) if self.is_return and entry.amount > 0: frappe.throw(_("Row #{0} (Payment Table): Amount must be negative").format(entry.idx)) - + def validate_pos_return(self): if self.is_pos and self.is_return: total_amount_in_payments = 0 @@ -167,12 +167,12 @@ class POSInvoice(SalesInvoice): invoice_total = self.rounded_total or self.grand_total if total_amount_in_payments < invoice_total: frappe.throw(_("Total payments amount can't be greater than {}".format(-invoice_total))) - + def validate_loyalty_transaction(self): if self.redeem_loyalty_points and (not self.loyalty_redemption_account or not self.loyalty_redemption_cost_center): expense_account, cost_center = frappe.db.get_value('Loyalty Program', self.loyalty_program, ["expense_account", "cost_center"]) if not self.loyalty_redemption_account: - self.loyalty_redemption_account = expense_account + self.loyalty_redemption_account = expense_account if not self.loyalty_redemption_cost_center: self.loyalty_redemption_cost_center = cost_center @@ -212,7 +212,7 @@ class POSInvoice(SalesInvoice): if update: self.db_set('status', self.status, update_modified = update_modified) - + def set_pos_fields(self, for_validate=False): """Set retail related fields from POS Profiles""" from erpnext.stock.get_item_details import get_pos_profile_item_details, get_pos_profile @@ -315,25 +315,25 @@ class POSInvoice(SalesInvoice): @frappe.whitelist() def get_stock_availability(item_code, warehouse): - latest_sle = frappe.db.sql("""select qty_after_transaction - from `tabStock Ledger Entry` + latest_sle = frappe.db.sql("""select qty_after_transaction + from `tabStock Ledger Entry` where item_code = %s and warehouse = %s order by posting_date desc, posting_time desc limit 1""", (item_code, warehouse), as_dict=1) - + pos_sales_qty = frappe.db.sql("""select sum(p_item.qty) as qty from `tabPOS Invoice` p, `tabPOS Invoice Item` p_item - where p.name = p_item.parent - and p.consolidated_invoice is NULL + where p.name = p_item.parent + and p.consolidated_invoice is NULL and p.docstatus = 1 and p_item.docstatus = 1 and p_item.item_code = %s and p_item.warehouse = %s """, (item_code, warehouse), as_dict=1) - + sle_qty = latest_sle[0].qty_after_transaction or 0 if latest_sle else 0 pos_sales_qty = pos_sales_qty[0].qty or 0 if pos_sales_qty else 0 - + if sle_qty and pos_sales_qty and sle_qty > pos_sales_qty: return sle_qty - pos_sales_qty else: @@ -360,14 +360,14 @@ def make_merge_log(invoices): merge_log = frappe.new_doc("POS Invoice Merge Log") merge_log.posting_date = getdate(nowdate()) for inv in invoices: - inv_data = frappe.db.get_values("POS Invoice", inv.get('name'), + inv_data = frappe.db.get_values("POS Invoice", inv.get('name'), ["customer", "posting_date", "grand_total"], as_dict=1)[0] merge_log.customer = inv_data.customer merge_log.append("pos_invoices", { 'pos_invoice': inv.get('name'), 'customer': inv_data.customer, 'posting_date': inv_data.posting_date, - 'grand_total': inv_data.grand_total + 'grand_total': inv_data.grand_total }) if merge_log.get('pos_invoices'): diff --git a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py index 9c62a87677..514a2acd8c 100644 --- a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py @@ -182,8 +182,9 @@ class TestPOSInvoice(unittest.TestCase): def test_pos_returns_with_repayment(self): pos = create_pos_invoice(qty = 10, do_not_save=True) + pos.set('payments', []) pos.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 500}) - pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 500}) + pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 500, 'default': 1}) pos.insert() pos.submit() @@ -200,8 +201,9 @@ class TestPOSInvoice(unittest.TestCase): income_account = "Sales - _TC", expense_account = "Cost of Goods Sold - _TC", rate=105, cost_center = "Main - _TC", do_not_save=True) + pos.set('payments', []) pos.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 50}) - pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 60}) + pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 60, 'default': 1}) pos.insert() pos.submit() diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py index 00dbad5fa0..11b9d2509e 100644 --- a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py +++ b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py @@ -24,11 +24,20 @@ class POSInvoiceMergeLog(Document): def validate_pos_invoice_status(self): for d in self.pos_invoices: - status, docstatus = frappe.db.get_value('POS Invoice', d.pos_invoice, ['status', 'docstatus']) + status, docstatus, is_return, return_against = frappe.db.get_value( + 'POS Invoice', d.pos_invoice, ['status', 'docstatus', 'is_return', 'return_against']) + if docstatus != 1: frappe.throw(_("Row #{}: POS Invoice {} is not submitted yet").format(d.idx, d.pos_invoice)) - if status in ['Consolidated']: + if status == "Consolidated": frappe.throw(_("Row #{}: POS Invoice {} has been {}").format(d.idx, d.pos_invoice, status)) + if is_return and return_against not in [d.pos_invoice for d in self.pos_invoices] and status != "Consolidated": + # if return entry is not getting merged in the current pos closing and if it is not consolidated + frappe.throw( + _("Row #{}: Return Invoice {} cannot be made against unconsolidated invoice. \ + You can add original invoice {} manually to proceed.") + .format(d.idx, frappe.bold(d.pos_invoice), frappe.bold(return_against)) + ) def on_submit(self): pos_invoice_docs = [frappe.get_doc("POS Invoice", d.pos_invoice) for d in self.pos_invoices] @@ -36,12 +45,12 @@ class POSInvoiceMergeLog(Document): returns = [d for d in pos_invoice_docs if d.get('is_return') == 1] sales = [d for d in pos_invoice_docs if d.get('is_return') == 0] - sales_invoice = self.process_merging_into_sales_invoice(sales) + sales_invoice, credit_note = "", "" + if sales: + sales_invoice = self.process_merging_into_sales_invoice(sales) - if len(returns): + if returns: credit_note = self.process_merging_into_credit_note(returns) - else: - credit_note = "" self.save() # save consolidated_sales_invoice & consolidated_credit_note ref in merge log diff --git a/erpnext/accounts/doctype/pos_profile/test_pos_profile.py b/erpnext/accounts/doctype/pos_profile/test_pos_profile.py index 8a4050cf9e..edf86590c8 100644 --- a/erpnext/accounts/doctype/pos_profile/test_pos_profile.py +++ b/erpnext/accounts/doctype/pos_profile/test_pos_profile.py @@ -8,6 +8,8 @@ import unittest from erpnext.stock.get_item_details import get_pos_profile from erpnext.accounts.doctype.pos_profile.pos_profile import get_child_nodes +test_dependencies = ['Item'] + class TestPOSProfile(unittest.TestCase): def test_pos_profile(self): make_pos_profile() @@ -88,7 +90,7 @@ def make_pos_profile(**args): "write_off_account": args.write_off_account or "_Test Write Off - _TC", "write_off_cost_center": args.write_off_cost_center or "_Test Write Off Cost Center - _TC" }) - + payments = [{ 'mode_of_payment': 'Cash', 'default': 1 diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py index cff7d5ba22..aa6194cbc3 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py @@ -1,5 +1,4 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# MIT License. See license.txt # For license information, please see license.txt @@ -208,7 +207,7 @@ def get_serial_no_for_item(args): def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=False): from erpnext.accounts.doctype.pricing_rule.utils import (get_pricing_rules, - get_applied_pricing_rules, get_pricing_rule_items, get_product_discount_rule) + get_applied_pricing_rules, get_pricing_rule_items, get_product_discount_rule) if isinstance(doc, string_types): doc = json.loads(doc) @@ -237,7 +236,7 @@ def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=Fa update_args_for_pricing_rule(args) - pricing_rules = (get_applied_pricing_rules(args) + pricing_rules = (get_applied_pricing_rules(args.get('pricing_rules')) if for_validate and args.get("pricing_rules") else get_pricing_rules(args, doc)) if pricing_rules: @@ -365,8 +364,9 @@ def set_discount_amount(rate, item_details): item_details.rate = rate def remove_pricing_rule_for_item(pricing_rules, item_details, item_code=None): - from erpnext.accounts.doctype.pricing_rule.utils import get_pricing_rule_items - for d in json.loads(pricing_rules): + from erpnext.accounts.doctype.pricing_rule.utils import (get_applied_pricing_rules, + get_pricing_rule_items) + for d in get_applied_pricing_rules(pricing_rules): if not d or not frappe.db.exists("Pricing Rule", d): continue pricing_rule = frappe.get_cached_doc('Pricing Rule', d) diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py index 3fd316f75e..53b0cf7bba 100644 --- a/erpnext/accounts/doctype/pricing_rule/utils.py +++ b/erpnext/accounts/doctype/pricing_rule/utils.py @@ -447,9 +447,14 @@ def apply_pricing_rule_on_transaction(doc): apply_pricing_rule_for_free_items(doc, item_details.free_item_data) doc.set_missing_values() -def get_applied_pricing_rules(item_row): - return (json.loads(item_row.get("pricing_rules")) - if item_row.get("pricing_rules") else []) +def get_applied_pricing_rules(pricing_rules): + if pricing_rules: + if pricing_rules.startswith('['): + return json.loads(pricing_rules) + else: + return pricing_rules.split(',') + + return [] def get_product_discount_rule(pricing_rule, item_details, args=None, doc=None): free_item = pricing_rule.free_item diff --git a/erpnext/accounts/doctype/process_deferred_accounting/process_deferred_accounting.js b/erpnext/accounts/doctype/process_deferred_accounting/process_deferred_accounting.js index 2800c195ce..1ec6805ae0 100644 --- a/erpnext/accounts/doctype/process_deferred_accounting/process_deferred_accounting.js +++ b/erpnext/accounts/doctype/process_deferred_accounting/process_deferred_accounting.js @@ -10,13 +10,15 @@ frappe.ui.form.on('Process Deferred Accounting', { } }; }); + }, - if (frm.doc.company) { + type: function(frm) { + if (frm.doc.company && frm.doc.type) { frm.set_query("account", function() { return { filters: { 'company': frm.doc.company, - 'root_type': 'Liability', + 'root_type': frm.doc.type === 'Income' ? 'Liability' : 'Asset', 'is_group': 0 } }; diff --git a/erpnext/accounts/doctype/process_deferred_accounting/process_deferred_accounting.json b/erpnext/accounts/doctype/process_deferred_accounting/process_deferred_accounting.json index 4daafef3ec..457e98ca54 100644 --- a/erpnext/accounts/doctype/process_deferred_accounting/process_deferred_accounting.json +++ b/erpnext/accounts/doctype/process_deferred_accounting/process_deferred_accounting.json @@ -60,6 +60,7 @@ "reqd": 1 }, { + "depends_on": "eval: doc.type", "fieldname": "account", "fieldtype": "Link", "label": "Account", @@ -73,9 +74,10 @@ "reqd": 1 } ], + "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-02-06 18:18:09.852844", + "modified": "2020-09-03 18:07:02.463754", "modified_by": "Administrator", "module": "Accounts", "name": "Process Deferred Accounting", diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index 4f6be59c65..2bfa4a572e 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -25,6 +25,12 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({ this.frm.set_df_property("credit_to", "print_hide", 0); } } + + // Trigger supplier event on load if supplier is available + // The reason for this is PI can be created from PR or PO and supplier is pre populated + if (this.frm.doc.supplier) { + this.frm.trigger('supplier'); + } }, refresh: function(doc) { @@ -135,6 +141,8 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({ } }); } + + this.frm.set_df_property("tax_withholding_category", "hidden", doc.apply_tds ? 0 : 1); }, unblock_invoice: function() { diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 7b1062f654..079f599706 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -132,6 +132,11 @@ class PurchaseInvoice(BuyingController): if not self.due_date: self.due_date = get_due_date(self.posting_date, "Supplier", self.supplier, self.company, self.bill_date) + tds_category = frappe.db.get_value("Supplier", self.supplier, "tax_withholding_category") + if tds_category and not for_validate: + self.apply_tds = 1 + self.tax_withholding_category = tds_category + super(PurchaseInvoice, self).set_missing_values(for_validate) def check_conversion_rate(self): @@ -400,8 +405,6 @@ class PurchaseInvoice(BuyingController): update_linked_doc(self.doctype, self.name, self.inter_company_invoice_reference) def make_gl_entries(self, gl_entries=None): - if not self.grand_total: - return if not gl_entries: gl_entries = self.get_gl_entries() diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json index 52a5be0984..f6d76e5050 100644 --- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json +++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json @@ -1,5 +1,4 @@ { - "actions": [], "autoname": "hash", "creation": "2013-05-22 12:43:10", "doctype": "DocType", @@ -82,6 +81,7 @@ "item_tax_rate", "bom", "include_exploded_items", + "purchase_invoice_item", "col_break6", "purchase_order", "po_detail", @@ -769,12 +769,21 @@ "collapsible": 1, "fieldname": "col_break7", "fieldtype": "Column Break" + }, + { + "depends_on": "eval:parent.update_stock == 1", + "fieldname": "purchase_invoice_item", + "fieldtype": "Data", + "ignore_user_permissions": 1, + "label": "Purchase Invoice Item", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 } ], "idx": 1, "istable": 1, - "links": [], - "modified": "2020-04-22 10:37:35.103176", + "modified": "2020-08-20 11:48:01.398356", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice Item", diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index 31613e50b0..2397b7d0cb 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -447,7 +447,7 @@ { "allow_on_submit": 1, "fieldname": "po_no", - "fieldtype": "Small Text", + "fieldtype": "Data", "hide_days": 1, "hide_seconds": 1, "label": "Customer's Purchase Order", @@ -1946,7 +1946,7 @@ "idx": 181, "is_submittable": 1, "links": [], - "modified": "2020-08-03 23:31:12.675040", + "modified": "2020-08-27 01:56:28.532140", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice", diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py index f1069282ed..2980213f3b 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py @@ -13,7 +13,8 @@ def get_data(): 'Auto Repeat': 'reference_document', }, 'internal_links': { - 'Sales Order': ['items', 'sales_order'] + 'Sales Order': ['items', 'sales_order'], + 'Delivery Note': ['items', 'delivery_note'] }, 'transactions': [ { diff --git a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json index 004d358ef9..fb3dd6a92a 100644 --- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json +++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json @@ -1,5 +1,4 @@ { - "actions": [], "autoname": "hash", "creation": "2013-06-04 11:02:19", "doctype": "DocType", @@ -87,6 +86,7 @@ "edit_references", "sales_order", "so_detail", + "sales_invoice_item", "column_break_74", "delivery_note", "dn_detail", @@ -790,12 +790,22 @@ "fieldtype": "Link", "label": "Project", "options": "Project" - } + }, + { + "depends_on": "eval:parent.update_stock == 1", + "fieldname": "sales_invoice_item", + "fieldtype": "Data", + "ignore_user_permissions": 1, + "label": "Sales Invoice Item", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + } ], "idx": 1, "istable": 1, "links": [], - "modified": "2020-07-18 12:24:41.749986", + "modified": "2020-08-20 11:24:41.749986", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice Item", diff --git a/erpnext/accounts/doctype/shipping_rule/shipping_rule.js b/erpnext/accounts/doctype/shipping_rule/shipping_rule.js index 53ee08a773..d0904eec3e 100644 --- a/erpnext/accounts/doctype/shipping_rule/shipping_rule.js +++ b/erpnext/accounts/doctype/shipping_rule/shipping_rule.js @@ -3,6 +3,22 @@ frappe.ui.form.on('Shipping Rule', { refresh: function(frm) { + frm.set_query("cost_center", function() { + return { + filters: { + company: frm.doc.company + } + } + }) + + frm.set_query("account", function() { + return { + filters: { + company: frm.doc.company + } + } + }) + frm.trigger('toggle_reqd'); }, calculate_based_on: function(frm) { @@ -12,4 +28,4 @@ frappe.ui.form.on('Shipping Rule', { frm.toggle_reqd("shipping_amount", frm.doc.calculate_based_on === 'Fixed'); frm.toggle_reqd("conditions", frm.doc.calculate_based_on !== 'Fixed'); } -}); \ No newline at end of file +}); diff --git a/erpnext/accounts/doctype/tax_category/tax_category.json b/erpnext/accounts/doctype/tax_category/tax_category.json index 1e3ae455b3..6f682a0466 100644 --- a/erpnext/accounts/doctype/tax_category/tax_category.json +++ b/erpnext/accounts/doctype/tax_category/tax_category.json @@ -1,134 +1,66 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, + "actions": [], + "allow_rename": 1, "autoname": "field:title", - "beta": 0, "creation": "2018-11-22 23:38:39.668804", - "custom": 0, - "docstatus": 0, "doctype": "DocType", - "document_type": "", "editable_grid": 1, "engine": "InnoDB", + "field_order": [ + "title" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "title", "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": "Title", - "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": 1 } ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2020-01-15 17:14:28.951793", + "index_web_pages_for_search": 1, + "links": [], + "modified": "2020-08-30 19:41:25.783852", "modified_by": "Administrator", "module": "Accounts", "name": "Tax Category", - "name_case": "", "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, "create": 1, "delete": 1, "email": 1, "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "System Manager", - "set_user_permissions": 0, "share": 1, - "submit": 0, "write": 1 }, { - "amend": 0, - "cancel": 0, "create": 1, "delete": 1, "email": 1, "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "Accounts Manager", - "set_user_permissions": 0, "share": 1, - "submit": 0, "write": 1 }, { - "amend": 0, - "cancel": 0, - "create": 0, - "delete": 0, "email": 1, "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "Accounts User", - "set_user_permissions": 0, - "share": 1, - "submit": 0, - "write": 0 + "share": 1 } ], "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, "sort_field": "modified", "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 -} + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index cf3deb828f..01d3903d28 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -45,8 +45,8 @@ def validate_accounting_period(gl_map): }, as_dict=1) if accounting_periods: - frappe.throw(_("You can't create accounting entries in the closed accounting period {0}") - .format(accounting_periods[0].name), ClosedAccountingPeriod) + frappe.throw(_("You cannot create or cancel any accounting entries with in the closed Accounting Period {0}") + .format(frappe.bold(accounting_periods[0].name)), ClosedAccountingPeriod) def process_gl_map(gl_map, merge_entries=True): if merge_entries: @@ -301,8 +301,9 @@ def make_reverse_gl_entries(gl_entries=None, voucher_type=None, voucher_no=None, }) if gl_entries: - set_as_cancel(gl_entries[0]['voucher_type'], gl_entries[0]['voucher_no']) + validate_accounting_period(gl_entries) check_freezing_date(gl_entries[0]["posting_date"], adv_adj) + set_as_cancel(gl_entries[0]['voucher_type'], gl_entries[0]['voucher_no']) for entry in gl_entries: entry['name'] = None @@ -342,7 +343,7 @@ def set_as_cancel(voucher_type, voucher_no): """ Set is_cancelled=1 in all original gl entries for the voucher """ - frappe.db.sql("""update `tabGL Entry` set is_cancelled = 1, + frappe.db.sql("""UPDATE `tabGL Entry` SET is_cancelled = 1, modified=%s, modified_by=%s where voucher_type=%s and voucher_no=%s and is_cancelled = 0""", (now(), frappe.session.user, voucher_type, voucher_no)) diff --git a/erpnext/accounts/report/budget_variance_report/budget_variance_report.js b/erpnext/accounts/report/budget_variance_report/budget_variance_report.js index 3ec4d306c3..f547ca619b 100644 --- a/erpnext/accounts/report/budget_variance_report/budget_variance_report.js +++ b/erpnext/accounts/report/budget_variance_report/budget_variance_report.js @@ -71,7 +71,22 @@ frappe.query_reports["Budget Variance Report"] = { fieldtype: "Check", default: 0, }, - ] + ], + "formatter": function (value, row, column, data, default_formatter) { + value = default_formatter(value, row, column, data); + + if (column.fieldname.includes('variance')) { + + if (data[column.fieldname] < 0) { + value = "" + value + ""; + } + else if (data[column.fieldname] > 0) { + value = "" + value + ""; + } + } + + return value; + } } erpnext.dimension_filters.forEach((dimension) => { diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py index c2c7207e37..d0116890b6 100644 --- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py +++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py @@ -256,7 +256,7 @@ def accumulate_values_into_parents(accounts, accounts_by_name, companies): """accumulate children's values in parent accounts""" for d in reversed(accounts): if d.parent_account: - account = d.parent_account.split('-')[0].strip() + account = d.parent_account.split(' - ')[0].strip() if not accounts_by_name.get(account): continue @@ -378,7 +378,7 @@ def set_gl_entries_by_account(from_date, to_date, root_lft, root_rgt, filters, g if filters and filters.get('presentation_currency') != d.default_currency: currency_info['company'] = d.name currency_info['company_currency'] = d.default_currency - convert_to_presentation_currency(gl_entries, currency_info) + convert_to_presentation_currency(gl_entries, currency_info, filters.get('company')) for entry in gl_entries: key = entry.account_number or entry.account_name diff --git a/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py b/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py index 2cb10b11e1..10b32fea56 100644 --- a/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py +++ b/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py @@ -173,7 +173,7 @@ class PartyLedgerSummaryReport(object): from `tabGL Entry` gle {join} where - gle.docstatus < 2 and gle.party_type=%(party_type)s and ifnull(gle.party, '') != '' + gle.docstatus < 2 and gle.is_cancelled = 0 and gle.party_type=%(party_type)s and ifnull(gle.party, '') != '' and gle.posting_date <= %(to_date)s {conditions} order by gle.posting_date """.format(join=join, join_field=join_field, conditions=conditions), self.filters, as_dict=True) @@ -248,7 +248,7 @@ class PartyLedgerSummaryReport(object): from `tabGL Entry` where - docstatus < 2 + docstatus < 2 and is_cancelled = 0 and (voucher_type, voucher_no) in ( select voucher_type, voucher_no from `tabGL Entry` gle, `tabAccount` acc where acc.name = gle.account and acc.account_type = '{income_or_expense}' diff --git a/erpnext/accounts/report/financial_statements.py b/erpnext/accounts/report/financial_statements.py index d5b8cdb1d4..1b65a318b6 100644 --- a/erpnext/accounts/report/financial_statements.py +++ b/erpnext/accounts/report/financial_statements.py @@ -423,7 +423,7 @@ def set_gl_entries_by_account( distributed_cost_center_query=distributed_cost_center_query), gl_filters, as_dict=True) #nosec if filters and filters.get('presentation_currency'): - convert_to_presentation_currency(gl_entries, get_currency(filters)) + convert_to_presentation_currency(gl_entries, get_currency(filters), filters.get('company')) for entry in gl_entries: gl_entries_by_account.setdefault(entry.account, []).append(entry) diff --git a/erpnext/accounts/report/general_ledger/general_ledger.js b/erpnext/accounts/report/general_ledger/general_ledger.js index 1fc0f79478..fb0d359926 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.js +++ b/erpnext/accounts/report/general_ledger/general_ledger.js @@ -146,6 +146,12 @@ frappe.query_reports["General Ledger"] = { return frappe.db.get_link_options('Project', txt); } }, + { + "fieldname": "include_dimensions", + "label": __("Consider Accounting Dimensions"), + "fieldtype": "Check", + "default": 0 + }, { "fieldname": "show_opening_entries", "label": __("Show Opening Entries"), diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index fcd36e4e6e..f735d87a76 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -43,8 +43,11 @@ def execute(filters=None): def validate_filters(filters, account_details): - if not filters.get('company'): - frappe.throw(_('{0} is mandatory').format(_('Company'))) + 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.get("account") and not account_details.get(filters.account): frappe.throw(_("Account {0} does not exists").format(filters.account)) @@ -106,15 +109,20 @@ def set_account_currency(filters): return filters def get_result(filters, account_details): - gl_entries = get_gl_entries(filters) + accounting_dimensions = [] + if filters.get("include_dimensions"): + accounting_dimensions = get_accounting_dimensions() - data = get_data_with_opening_closing(filters, account_details, gl_entries) + gl_entries = get_gl_entries(filters, accounting_dimensions) + + data = get_data_with_opening_closing(filters, account_details, + accounting_dimensions, gl_entries) result = get_result_as_list(data, filters) return result -def get_gl_entries(filters): +def get_gl_entries(filters, accounting_dimensions): currency_map = get_currency(filters) select_fields = """, debit, credit, debit_in_account_currency, credit_in_account_currency """ @@ -128,11 +136,15 @@ def get_gl_entries(filters): filters['company_fb'] = frappe.db.get_value("Company", filters.get("company"), 'default_finance_book') + dimension_fields = "" + if accounting_dimensions: + dimension_fields = ', '.join(accounting_dimensions) + ',' + distributed_cost_center_query = "" if filters and filters.get('cost_center'): select_fields_with_percentage = """, debit*(DCC_allocation.percentage_allocation/100) as debit, credit*(DCC_allocation.percentage_allocation/100) as credit, debit_in_account_currency*(DCC_allocation.percentage_allocation/100) as debit_in_account_currency, credit_in_account_currency*(DCC_allocation.percentage_allocation/100) as credit_in_account_currency """ - + distributed_cost_center_query = """ UNION ALL SELECT name as gl_entry, @@ -141,12 +153,12 @@ def get_gl_entries(filters): party_type, party, voucher_type, - voucher_no, + voucher_no, {dimension_fields} cost_center, project, against_voucher_type, against_voucher, account_currency, - remarks, against, + remarks, against, is_opening, `tabGL Entry`.creation {select_fields_with_percentage} FROM `tabGL Entry`, ( @@ -160,13 +172,14 @@ def get_gl_entries(filters): {conditions} AND posting_date <= %(to_date)s AND cost_center = DCC_allocation.parent - """.format(select_fields_with_percentage=select_fields_with_percentage, conditions=get_conditions(filters).replace("and cost_center in %(cost_center)s ", '')) + """.format(dimension_fields=dimension_fields,select_fields_with_percentage=select_fields_with_percentage, conditions=get_conditions(filters).replace("and cost_center in %(cost_center)s ", '')) gl_entries = frappe.db.sql( """ select name as gl_entry, posting_date, account, party_type, party, - voucher_type, voucher_no, cost_center, project, + voucher_type, voucher_no, {dimension_fields} + cost_center, project, against_voucher_type, against_voucher, account_currency, remarks, against, is_opening, creation {select_fields} from `tabGL Entry` @@ -174,13 +187,13 @@ def get_gl_entries(filters): {distributed_cost_center_query} {order_by_statement} """.format( - select_fields=select_fields, conditions=get_conditions(filters), distributed_cost_center_query=distributed_cost_center_query, + dimension_fields=dimension_fields, select_fields=select_fields, conditions=get_conditions(filters), distributed_cost_center_query=distributed_cost_center_query, order_by_statement=order_by_statement ), filters, as_dict=1) if filters.get('presentation_currency'): - return convert_to_presentation_currency(gl_entries, currency_map) + return convert_to_presentation_currency(gl_entries, currency_map, filters.get('company')) else: return gl_entries @@ -247,12 +260,12 @@ def get_conditions(filters): return "and {}".format(" and ".join(conditions)) if conditions else "" -def get_data_with_opening_closing(filters, account_details, gl_entries): +def get_data_with_opening_closing(filters, account_details, accounting_dimensions, gl_entries): data = [] gle_map = initialize_gle_map(gl_entries, filters) - totals, entries = get_accountwise_gle(filters, gl_entries, gle_map) + totals, entries = get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map) # Opening for filtered account data.append(totals.opening) @@ -318,7 +331,7 @@ def initialize_gle_map(gl_entries, filters): return gle_map -def get_accountwise_gle(filters, gl_entries, gle_map): +def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map): totals = get_totals_dict() entries = [] consolidated_gle = OrderedDict() @@ -350,8 +363,11 @@ def get_accountwise_gle(filters, gl_entries, gle_map): if filters.get("group_by") != _('Group by Voucher (Consolidated)'): gle_map[gle.get(group_by)].entries.append(gle) elif filters.get("group_by") == _('Group by Voucher (Consolidated)'): - key = (gle.get("voucher_type"), gle.get("voucher_no"), - gle.get("account"), gle.get("cost_center")) + keylist = [gle.get("voucher_type"), gle.get("voucher_no"), gle.get("account")] + for dim in accounting_dimensions: + keylist.append(gle.get(dim)) + keylist.append(gle.get("cost_center")) + key = tuple(keylist) if key not in consolidated_gle: consolidated_gle.setdefault(key, gle) else: @@ -478,7 +494,19 @@ def get_columns(filters): "options": "Project", "fieldname": "project", "width": 100 - }, + } + ]) + + if filters.get("include_dimensions"): + for dim in get_accounting_dimensions(as_list = False): + columns.append({ + "label": _(dim.label), + "options": dim.label, + "fieldname": dim.fieldname, + "width": 100 + }) + + columns.extend([ { "label": _("Cost Center"), "options": "Cost Center", diff --git a/erpnext/accounts/report/gross_profit/gross_profit.json b/erpnext/accounts/report/gross_profit/gross_profit.json index 9cfb0627d3..cd6bac2d77 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.json +++ b/erpnext/accounts/report/gross_profit/gross_profit.json @@ -1,24 +1,23 @@ { - "add_total_row": 0, - "apply_user_permissions": 1, - "creation": "2013-02-25 17:03:34", - "disabled": 0, - "docstatus": 0, - "doctype": "Report", - "idx": 3, - "is_standard": "Yes", - "modified": "2017-02-24 20:12:22.464240", - "modified_by": "Administrator", - "module": "Accounts", - "name": "Gross Profit", - "owner": "Administrator", - "ref_doctype": "Sales Invoice", - "report_name": "Gross Profit", - "report_type": "Script Report", + "add_total_row": 1, + "creation": "2013-02-25 17:03:34", + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 3, + "is_standard": "Yes", + "modified": "2020-08-13 11:26:39.112352", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Gross Profit", + "owner": "Administrator", + "ref_doctype": "Sales Invoice", + "report_name": "Gross Profit", + "report_type": "Script Report", "roles": [ { "role": "Accounts Manager" - }, + }, { "role": "Accounts User" } diff --git a/erpnext/accounts/report/utils.py b/erpnext/accounts/report/utils.py index 4a9af490cf..9de8d19f2a 100644 --- a/erpnext/accounts/report/utils.py +++ b/erpnext/accounts/report/utils.py @@ -6,10 +6,6 @@ from erpnext.accounts.doctype.fiscal_year.fiscal_year import get_from_and_to_dat from frappe.utils import cint, get_datetime_str, formatdate, flt __exchange_rates = {} -P_OR_L_ACCOUNTS = list( - sum(frappe.get_list('Account', fields=['name'], or_filters=[{'root_type': 'Income'}, {'root_type': 'Expense'}], as_list=True), ()) -) - def get_currency(filters): """ @@ -73,18 +69,7 @@ def get_rate_as_at(date, from_currency, to_currency): return rate - -def is_p_or_l_account(account_name): - """ - Check if the given `account name` is an `Account` with `root_type` of either 'Income' - or 'Expense'. - :param account_name: - :return: Boolean - """ - return account_name in P_OR_L_ACCOUNTS - - -def convert_to_presentation_currency(gl_entries, currency_info): +def convert_to_presentation_currency(gl_entries, currency_info, company): """ Take a list of GL Entries and change the 'debit' and 'credit' values to currencies in `currency_info`. @@ -96,6 +81,9 @@ def convert_to_presentation_currency(gl_entries, currency_info): presentation_currency = currency_info['presentation_currency'] company_currency = currency_info['company_currency'] + pl_accounts = [d.name for d in frappe.get_list('Account', + filters={'report_type': 'Profit and Loss', 'company': company})] + for entry in gl_entries: account = entry['account'] debit = flt(entry['debit']) @@ -107,7 +95,7 @@ def convert_to_presentation_currency(gl_entries, currency_info): if account_currency != presentation_currency: value = debit or credit - date = currency_info['report_date'] if not is_p_or_l_account(account) else entry['posting_date'] + date = entry['posting_date'] if account in pl_accounts else currency_info['report_date'] converted_value = convert(value, presentation_currency, company_currency, date) if entry.get('debit'): diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 0bd03a8dbe..9d08d9212d 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -146,7 +146,7 @@ class Asset(AccountsController): 'assets': assets, 'purpose': 'Receipt', 'company': self.company, - 'transaction_date': getdate(nowdate()), + 'transaction_date': getdate(self.purchase_date), 'reference_doctype': reference_doctype, 'reference_name': reference_docname }).insert() diff --git a/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py index 3de9526c4f..019cefc0bd 100644 --- a/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py +++ b/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py @@ -11,6 +11,8 @@ from erpnext.stock.doctype.item.test_item import make_item from erpnext.templates.pages.rfq import check_supplier_has_docname_access from erpnext.buying.doctype.request_for_quotation.request_for_quotation import make_supplier_quotation from erpnext.buying.doctype.request_for_quotation.request_for_quotation import create_supplier_quotation +from erpnext.crm.doctype.opportunity.test_opportunity import make_opportunity +from erpnext.crm.doctype.opportunity.opportunity import make_request_for_quotation as make_rfq class TestRequestforQuotation(unittest.TestCase): def test_quote_status(self): @@ -110,6 +112,23 @@ class TestRequestforQuotation(unittest.TestCase): self.assertEqual(supplier_quotation.items[0].qty, 5) self.assertEqual(supplier_quotation.items[0].stock_qty, 10) + def test_make_rfq_from_opportunity(self): + opportunity = make_opportunity(with_items=1) + supplier_data = get_supplier_data() + rfq = make_rfq(opportunity.name) + + self.assertEqual(len(rfq.get("items")), len(opportunity.get("items"))) + rfq.message_for_supplier = 'Please supply the specified items at the best possible rates.' + + for item in rfq.items: + item.warehouse = "_Test Warehouse - _TC" + + for data in supplier_data: + rfq.append('suppliers', data) + + rfq.status = 'Draft' + rfq.submit() + def make_request_for_quotation(**args): """ :param supplier_data: List containing supplier data diff --git a/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.js b/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.js index a76ffeec2e..518d665e7e 100644 --- a/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.js +++ b/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.js @@ -12,7 +12,22 @@ frappe.query_reports["Quoted Item Comparison"] = { "reqd": 1 }, { - reqd: 1, + "fieldname":"from_date", + "label": __("From Date"), + "fieldtype": "Date", + "width": "80", + "reqd": 1, + "default": frappe.datetime.add_months(frappe.datetime.get_today(), -1), + }, + { + "fieldname":"to_date", + "label": __("To Date"), + "fieldtype": "Date", + "width": "80", + "reqd": 1, + "default": frappe.datetime.get_today() + }, + { default: "", options: "Item", label: __("Item"), @@ -45,13 +60,12 @@ frappe.query_reports["Quoted Item Comparison"] = { } }, { - fieldtype: "Link", + fieldtype: "MultiSelectList", label: __("Supplier Quotation"), - options: "Supplier Quotation", fieldname: "supplier_quotation", default: "", - get_query: () => { - return { filters: { "docstatus": ["<", 2] } } + get_data: function(txt) { + return frappe.db.get_link_options('Supplier Quotation', txt, {'docstatus': ["<", 2]}); } }, { @@ -63,9 +77,30 @@ frappe.query_reports["Quoted Item Comparison"] = { get_query: () => { return { filters: { "docstatus": ["<", 2] } } } + }, + { + fieldtype: "Check", + label: __("Include Expired"), + fieldname: "include_expired", + default: 0 } ], + formatter: (value, row, column, data, default_formatter) => { + value = default_formatter(value, row, column, data); + + if(column.fieldname === "valid_till" && data.valid_till){ + if(frappe.datetime.get_diff(data.valid_till, frappe.datetime.nowdate()) <= 1){ + value = `
${value}
`; + } + else if (frappe.datetime.get_diff(data.valid_till, frappe.datetime.nowdate()) <= 7){ + value = `
${value}
`; + } + } + + return value; + }, + onload: (report) => { // Create a button for setting the default supplier report.page.add_inner_button(__("Select Default Supplier"), () => { diff --git a/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.py b/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.py index a33867a525..4426560c16 100644 --- a/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.py +++ b/erpnext/buying/report/quoted_item_comparison/quoted_item_comparison.py @@ -16,44 +16,49 @@ def execute(filters=None): supplier_quotation_data = get_data(filters, conditions) columns = get_columns() - data, chart_data = prepare_data(supplier_quotation_data) + data, chart_data = prepare_data(supplier_quotation_data, filters) + message = get_message() - return columns, data, None, chart_data + return columns, data, message, chart_data def get_conditions(filters): conditions = "" + if filters.get("item_code"): + conditions += " AND sqi.item_code = %(item_code)s" + if filters.get("supplier_quotation"): - conditions += " AND sqi.parent = %(supplier_quotation)s" + conditions += " AND sqi.parent in %(supplier_quotation)s" if filters.get("request_for_quotation"): conditions += " AND sqi.request_for_quotation = %(request_for_quotation)s" if filters.get("supplier"): conditions += " AND sq.supplier in %(supplier)s" + + if not filters.get("include_expired"): + conditions += " AND sq.status != 'Expired'" + return conditions def get_data(filters, conditions): - if not filters.get("item_code"): - return [] - supplier_quotation_data = frappe.db.sql("""SELECT - sqi.parent, sqi.qty, sqi.rate, sqi.uom, sqi.request_for_quotation, - sq.supplier + sqi.parent, sqi.item_code, sqi.qty, sqi.rate, sqi.uom, sqi.request_for_quotation, + sqi.lead_time_days, sq.supplier, sq.valid_till FROM `tabSupplier Quotation Item` sqi, `tabSupplier Quotation` sq WHERE - sqi.item_code = %(item_code)s - AND sqi.parent = sq.name + sqi.parent = sq.name AND sqi.docstatus < 2 AND sq.company = %(company)s - AND sq.status != 'Expired' - {0}""".format(conditions), filters, as_dict=1) + AND sq.transaction_date between %(from_date)s and %(to_date)s + {0} + order by sq.transaction_date, sqi.item_code""".format(conditions), filters, as_dict=1) return supplier_quotation_data -def prepare_data(supplier_quotation_data): - out, suppliers, qty_list = [], [], [] +def prepare_data(supplier_quotation_data, filters): + out, suppliers, qty_list, chart_data = [], [], [], [] supplier_wise_map = defaultdict(list) supplier_qty_price_map = {} @@ -70,20 +75,24 @@ def prepare_data(supplier_quotation_data): exchange_rate = 1 row = { + "item_code": data.get('item_code'), "quotation": data.get("parent"), "qty": data.get("qty"), "price": flt(data.get("rate") * exchange_rate, float_precision), "uom": data.get("uom"), "request_for_quotation": data.get("request_for_quotation"), + "valid_till": data.get('valid_till'), + "lead_time_days": data.get('lead_time_days') } # map for report view of form {'supplier1':[{},{},...]} supplier_wise_map[supplier].append(row) # map for chart preparation of the form {'supplier1': {'qty': 'price'}} - if not supplier in supplier_qty_price_map: - supplier_qty_price_map[supplier] = {} - supplier_qty_price_map[supplier][row["qty"]] = row["price"] + if filters.get("item_code"): + if not supplier in supplier_qty_price_map: + supplier_qty_price_map[supplier] = {} + supplier_qty_price_map[supplier][row["qty"]] = row["price"] suppliers.append(supplier) qty_list.append(data.get("qty")) @@ -97,7 +106,8 @@ def prepare_data(supplier_quotation_data): for entry in supplier_wise_map[supplier]: out.append(entry) - chart_data = prepare_chart_data(suppliers, qty_list, supplier_qty_price_map) + if filters.get("item_code"): + chart_data = prepare_chart_data(suppliers, qty_list, supplier_qty_price_map) return out, chart_data @@ -117,9 +127,10 @@ def prepare_chart_data(suppliers, qty_list, supplier_qty_price_map): data_points_map[qty].append(None) dataset = [] + currency_symbol = frappe.db.get_value("Currency", frappe.db.get_default("currency"), "symbol") for qty in qty_list: datapoints = { - "name": _("Price for Qty ") + str(qty), + "name": currency_symbol + " (Qty " + str(qty) + " )", "values": data_points_map[qty] } dataset.append(datapoints) @@ -140,14 +151,21 @@ def get_columns(): "label": _("Supplier"), "fieldtype": "Link", "options": "Supplier", + "width": 150 + }, + { + "fieldname": "item_code", + "label": _("Item"), + "fieldtype": "Link", + "options": "Item", "width": 200 }, { - "fieldname": "quotation", - "label": _("Supplier Quotation"), + "fieldname": "uom", + "label": _("UOM"), "fieldtype": "Link", - "options": "Supplier Quotation", - "width": 200 + "options": "UOM", + "width": 90 }, { "fieldname": "qty", @@ -163,19 +181,43 @@ def get_columns(): "width": 110 }, { - "fieldname": "uom", - "label": _("UOM"), + "fieldname": "quotation", + "label": _("Supplier Quotation"), "fieldtype": "Link", - "options": "UOM", - "width": 90 + "options": "Supplier Quotation", + "width": 200 + }, + { + "fieldname": "valid_till", + "label": _("Valid Till"), + "fieldtype": "Date", + "width": 100 + }, + { + "fieldname": "lead_time_days", + "label": _("Lead Time (Days)"), + "fieldtype": "Int", + "width": 100 }, { "fieldname": "request_for_quotation", "label": _("Request for Quotation"), "fieldtype": "Link", "options": "Request for Quotation", - "width": 200 + "width": 150 } ] - return columns \ No newline at end of file + return columns + +def get_message(): + return """ + Valid till :    + + + Expires in a week or less + +    + + Expires today / Already Expired + """ \ No newline at end of file diff --git a/erpnext/communication/doctype/call_log/call_log.json b/erpnext/communication/doctype/call_log/call_log.json index cfc08eb084..31e79f17cd 100644 --- a/erpnext/communication/doctype/call_log/call_log.json +++ b/erpnext/communication/doctype/call_log/call_log.json @@ -1,4 +1,5 @@ { + "actions": [], "autoname": "field:id", "creation": "2019-06-05 12:07:02.634534", "doctype": "DocType", @@ -14,6 +15,7 @@ "contact", "contact_name", "column_break_10", + "customer", "lead", "lead_name", "section_break_5", @@ -28,7 +30,8 @@ }, { "fieldname": "section_break_5", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "label": "Call Details" }, { "fieldname": "id", @@ -125,10 +128,19 @@ "in_list_view": 1, "label": "Lead Name", "read_only": 1 + }, + { + "fieldname": "customer", + "fieldtype": "Link", + "label": "Customer", + "options": "Customer", + "read_only": 1 } ], "in_create": 1, - "modified": "2019-08-06 05:46:53.144683", + "index_web_pages_for_search": 1, + "links": [], + "modified": "2020-08-25 17:08:34.085731", "modified_by": "Administrator", "module": "Communication", "name": "Call Log", diff --git a/erpnext/communication/doctype/call_log/call_log.py b/erpnext/communication/doctype/call_log/call_log.py index 5fe3c4edbb..b31b757a37 100644 --- a/erpnext/communication/doctype/call_log/call_log.py +++ b/erpnext/communication/doctype/call_log/call_log.py @@ -16,6 +16,9 @@ class CallLog(Document): self.contact = get_contact_with_phone_number(number) self.lead = get_lead_with_phone_number(number) + contact = frappe.get_doc("Contact", self.contact) + self.customer = contact.get_link_for("Customer") + def after_insert(self): self.trigger_call_popup() diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 791b932695..a4652567ce 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -7,6 +7,7 @@ import json from frappe import _, throw from frappe.utils import (today, flt, cint, fmt_money, formatdate, getdate, add_days, add_months, get_last_day, nowdate, get_link_to_form) +from frappe.model.workflow import get_workflow_name, is_transition_condition_satisfied, WorkflowPermissionError from erpnext.stock.get_item_details import get_conversion_factor, get_item_details from erpnext.setup.utils import get_exchange_rate from erpnext.accounts.utils import get_fiscal_years, validate_fiscal_year, get_account_currency @@ -325,7 +326,7 @@ class AccountsController(TransactionBase): apply_pricing_rule_for_free_items(self, pricing_rule_args.get('free_item_data')) elif pricing_rule_args.get("validate_applied_rule"): - for pricing_rule in get_applied_pricing_rules(item): + for pricing_rule in get_applied_pricing_rules(item.get('pricing_rules')): pricing_rule_doc = frappe.get_cached_doc("Pricing Rule", pricing_rule) for field in ['discount_percentage', 'discount_amount', 'rate']: if item.get(field) < pricing_rule_doc.get(field): @@ -1208,7 +1209,7 @@ def set_purchase_order_defaults(parent_doctype, parent_doctype_name, child_docna set_child_tax_template_and_map(item, child_item, p_doc) return child_item -def check_and_delete_children(parent, data): +def validate_and_delete_children(parent, data): deleted_children = [] updated_item_names = [d.get("docname") for d in data] for item in parent.items: @@ -1235,18 +1236,37 @@ def check_and_delete_children(parent, data): @frappe.whitelist() def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, child_docname="items"): - def check_permissions(doc, perm_type='create'): + def check_doc_permissions(doc, perm_type='create'): try: doc.check_permission(perm_type) - except: - action = "add" if perm_type == 'create' else "update" - frappe.throw(_("You do not have permissions to {} items in a Sales Order.").format(action), title=_("Insufficient Permissions")) + except frappe.PermissionError: + actions = { 'create': 'add', 'write': 'update', 'cancel': 'remove' } + + frappe.throw(_("You do not have permissions to {} items in a {}.") + .format(actions[perm_type], parent_doctype), title=_("Insufficient Permissions")) + + def validate_workflow_conditions(doc): + workflow = get_workflow_name(doc.doctype) + if not workflow: + return + + workflow_doc = frappe.get_doc("Workflow", workflow) + current_state = doc.get(workflow_doc.workflow_state_field) + roles = frappe.get_roles() + + transitions = [] + for transition in workflow_doc.transitions: + if transition.next_state == current_state and transition.allowed in roles: + if not is_transition_condition_satisfied(transition, doc): + continue + transitions.append(transition.as_dict()) + + if not transitions: + frappe.throw(_("You do not have workflow access to update this document."), title=_("Insufficient Workflow Permissions")) def get_new_child_item(item_row): - if parent_doctype == "Sales Order": - return set_sales_order_defaults(parent_doctype, parent_doctype_name, child_docname, item_row) - if parent_doctype == "Purchase Order": - return set_purchase_order_defaults(parent_doctype, parent_doctype_name, child_docname, item_row) + new_child_function = set_sales_order_defaults if parent_doctype == "Sales Order" else set_purchase_order_defaults + return new_child_function(parent_doctype, parent_doctype_name, child_docname, item_row) def validate_quantity(child_item, d): if parent_doctype == "Sales Order" and flt(d.get("qty")) < flt(child_item.delivered_qty): @@ -1259,17 +1279,18 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil sales_doctypes = ['Sales Order', 'Sales Invoice', 'Delivery Note', 'Quotation'] parent = frappe.get_doc(parent_doctype, parent_doctype_name) - - check_and_delete_children(parent, data) + + check_doc_permissions(parent, 'cancel') + validate_and_delete_children(parent, data) for d in data: new_child_flag = False if not d.get("docname"): new_child_flag = True - check_permissions(parent, 'create') + check_doc_permissions(parent, 'create') child_item = get_new_child_item(d) else: - check_permissions(parent, 'write') + check_doc_permissions(parent, 'write') child_item = frappe.get_doc(parent_doctype + ' Item', d.get("docname")) prev_rate, new_rate = flt(child_item.get("rate")), flt(d.get("rate")) @@ -1376,6 +1397,9 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil parent.update_prevdoc_status('submit') parent.update_delivery_status() + parent.reload() + validate_workflow_conditions(parent) + parent.update_blanket_order() parent.update_billing_percentage() parent.set_status() diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 89b48f07ee..ac567b7dea 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -276,6 +276,9 @@ class BuyingController(StockController): qty_to_be_received_map = get_qty_to_be_received(purchase_orders) for item in self.get('items'): + if not item.purchase_order: + continue + # reset raw_material cost item.rm_supp_cost = 0 @@ -288,6 +291,12 @@ class BuyingController(StockController): fg_yet_to_be_received = qty_to_be_received_map.get(item_key) + if not fg_yet_to_be_received: + frappe.throw(_("Row #{0}: Item {1} is already fully received in Purchase Order {2}") + .format(item.idx, frappe.bold(item.item_code), + frappe.utils.get_link_to_form("Purchase Order", item.purchase_order)), + title=_("Limit Crossed")) + transferred_batch_qty_map = get_transferred_batch_qty_map(item.purchase_order, item.item_code) backflushed_batch_qty_map = get_backflushed_batch_qty_map(item.purchase_order, item.item_code) @@ -559,9 +568,19 @@ class BuyingController(StockController): "serial_no": cstr(d.serial_no).strip() }) if self.is_return: - original_incoming_rate = frappe.db.get_value("Stock Ledger Entry", - {"voucher_type": "Purchase Receipt", "voucher_no": self.return_against, - "item_code": d.item_code}, "incoming_rate") + filters = { + "voucher_type": self.doctype, + "voucher_no": self.return_against, + "item_code": d.item_code + } + + if (self.doctype == "Purchase Invoice" and self.update_stock + and d.get("purchase_invoice_item")): + filters["voucher_detail_no"] = d.purchase_invoice_item + elif self.doctype == "Purchase Receipt" and d.get("purchase_receipt_item"): + filters["voucher_detail_no"] = d.purchase_receipt_item + + original_incoming_rate = frappe.db.get_value("Stock Ledger Entry", filters, "incoming_rate") sle.update({ "outgoing_rate": original_incoming_rate diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index 37b7e31e61..c88bf66411 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -497,24 +497,18 @@ def warehouse_query(doctype, txt, searchfield, start, page_len, filters): conditions, bin_conditions = [], [] filter_dict = get_doctype_wise_filters(filters) - sub_query = """ select round(`tabBin`.actual_qty, 2) from `tabBin` - where `tabBin`.warehouse = `tabWarehouse`.name - {bin_conditions} """.format( - bin_conditions=get_filters_cond(doctype, filter_dict.get("Bin"), - bin_conditions, ignore_permissions=True)) - query = """select `tabWarehouse`.name, - CONCAT_WS(" : ", "Actual Qty", ifnull( ({sub_query}), 0) ) as actual_qty - from `tabWarehouse` + CONCAT_WS(" : ", "Actual Qty", ifnull(round(`tabBin`.actual_qty, 2), 0 )) actual_qty + from `tabWarehouse` left join `tabBin` + on `tabBin`.warehouse = `tabWarehouse`.name {bin_conditions} where - `tabWarehouse`.`{key}` like {txt} + `tabWarehouse`.`{key}` like {txt} {fcond} {mcond} - order by - `tabWarehouse`.name desc + order by ifnull(`tabBin`.actual_qty, 0) desc limit {start}, {page_len} """.format( - sub_query=sub_query, + bin_conditions=get_filters_cond(doctype, filter_dict.get("Bin"),bin_conditions, ignore_permissions=True), key=searchfield, fcond=get_filters_cond(doctype, filter_dict.get("Warehouse"), conditions), mcond=get_match_cond(doctype), diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index 3f127a201e..afc5f8179f 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -242,7 +242,8 @@ def make_return_doc(doctype, source_name, target_doc=None): 'type': data.type, 'amount': -1 * paid_amount, 'base_amount': -1 * base_paid_amount, - 'account': data.account + 'account': data.account, + 'default': data.default }) if doc.is_pos: doc.paid_amount = -1 * source.paid_amount @@ -281,6 +282,8 @@ def make_return_doc(doctype, source_name, target_doc=None): target_doc.rejected_warehouse = source_doc.rejected_warehouse target_doc.po_detail = source_doc.po_detail target_doc.pr_detail = source_doc.pr_detail + target_doc.purchase_invoice_item = source_doc.name + elif doctype == "Delivery Note": target_doc.against_sales_order = source_doc.against_sales_order target_doc.against_sales_invoice = source_doc.against_sales_invoice @@ -296,6 +299,7 @@ def make_return_doc(doctype, source_name, target_doc=None): target_doc.so_detail = source_doc.so_detail target_doc.dn_detail = source_doc.dn_detail target_doc.expense_account = source_doc.expense_account + target_doc.sales_invoice_item = source_doc.name if default_warehouse_for_sales_return: target_doc.warehouse = default_warehouse_for_sales_return diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index b696ac39f6..17f3ae53e7 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -217,7 +217,9 @@ class SellingController(StockController): 'target_warehouse': p.target_warehouse, 'company': self.company, 'voucher_type': self.doctype, - 'allow_zero_valuation': d.allow_zero_valuation_rate + 'allow_zero_valuation': d.allow_zero_valuation_rate, + 'sales_invoice_item': d.get("sales_invoice_item"), + 'delivery_note_item': d.get("dn_detail") })) else: il.append(frappe._dict({ @@ -233,7 +235,9 @@ class SellingController(StockController): 'target_warehouse': d.target_warehouse, 'company': self.company, 'voucher_type': self.doctype, - 'allow_zero_valuation': d.allow_zero_valuation_rate + 'allow_zero_valuation': d.allow_zero_valuation_rate, + 'sales_invoice_item': d.get("sales_invoice_item"), + 'delivery_note_item': d.get("dn_detail") })) return il @@ -302,7 +306,11 @@ class SellingController(StockController): d.conversion_factor = get_conversion_factor(d.item_code, d.uom).get("conversion_factor") or 1.0 return_rate = 0 if cint(self.is_return) and self.return_against and self.docstatus==1: - return_rate = self.get_incoming_rate_for_return(d.item_code, self.return_against) + against_document_no = (d.get("sales_invoice_item") + if self.doctype == "Sales Invoice" else d.get("delivery_note_item")) + + return_rate = self.get_incoming_rate_for_return(d.item_code, + self.return_against, against_document_no) # On cancellation or if return entry submission, make stock ledger entry for # target warehouse first, to update serial no values properly diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py index 0dc9878afd..9feac78770 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -255,7 +255,7 @@ class StatusUpdater(Document): args['second_source_condition'] = """ + ifnull((select sum(%(second_source_field)s) from `tab%(second_source_dt)s` where `%(second_join_field)s`="%(detail_id)s" - and (`tab%(second_source_dt)s`.docstatus=1) %(second_source_extra_cond)s), 0) """ % args + and (`tab%(second_source_dt)s`.docstatus=1) %(second_source_extra_cond)s FOR UPDATE), 0)""" % args if args['detail_id']: if not args.get("extra_cond"): args["extra_cond"] = "" diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index e8483da544..394883d239 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -301,14 +301,19 @@ class StockController(AccountsController): return serialized_items - def get_incoming_rate_for_return(self, item_code, against_document): + def get_incoming_rate_for_return(self, item_code, against_document, against_document_no=None): incoming_rate = 0.0 + cond = '' if against_document and item_code: + if against_document_no: + cond = " and voucher_detail_no = %s" %(frappe.db.escape(against_document_no)) + incoming_rate = frappe.db.sql("""select abs(stock_value_difference / actual_qty) from `tabStock Ledger Entry` where voucher_type = %s and voucher_no = %s - and item_code = %s limit 1""", + and item_code = %s {0} limit 1""".format(cond), (self.doctype, against_document, item_code)) + incoming_rate = incoming_rate[0][0] if incoming_rate else 0.0 return incoming_rate diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index f578e7eac4..92cfdb7f1a 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -9,6 +9,7 @@ from frappe.utils import cint, flt, round_based_on_smallest_currency_fraction from erpnext.controllers.accounts_controller import validate_conversion_rate, \ validate_taxes_and_charges, validate_inclusive_tax from erpnext.stock.get_item_details import _get_item_tax_template +from erpnext.accounts.doctype.pricing_rule.utils import get_applied_pricing_rules class calculate_taxes_and_totals(object): def __init__(self, doc): @@ -607,7 +608,7 @@ class calculate_taxes_and_totals(object): base_rate_with_margin = 0.0 if item.price_list_rate: if item.pricing_rules and not self.doc.ignore_pricing_rule: - for d in json.loads(item.pricing_rules): + for d in get_applied_pricing_rules(item.pricing_rules): pricing_rule = frappe.get_cached_doc('Pricing Rule', d) if (pricing_rule.margin_type == 'Amount' and pricing_rule.currency == self.doc.currency)\ diff --git a/erpnext/crm/doctype/opportunity/opportunity.py b/erpnext/crm/doctype/opportunity/opportunity.py index e152850f17..47b05f306b 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.py +++ b/erpnext/crm/doctype/opportunity/opportunity.py @@ -267,6 +267,9 @@ def make_quotation(source_name, target_doc=None): @frappe.whitelist() def make_request_for_quotation(source_name, target_doc=None): + def update_item(obj, target, source_parent): + target.conversion_factor = 1.0 + doclist = get_mapped_doc("Opportunity", source_name, { "Opportunity": { "doctype": "Request for Quotation" @@ -277,7 +280,8 @@ def make_request_for_quotation(source_name, target_doc=None): ["name", "opportunity_item"], ["parent", "opportunity"], ["uom", "uom"] - ] + ], + "postprocess": update_item } }, target_doc) @@ -325,7 +329,7 @@ def auto_close_opportunity(): doc.save() @frappe.whitelist() -def make_opportunity_from_communication(communication, ignore_communication_links=False): +def make_opportunity_from_communication(communication, company, ignore_communication_links=False): from erpnext.crm.doctype.lead.lead import make_lead_from_communication doc = frappe.get_doc("Communication", communication) @@ -337,6 +341,7 @@ def make_opportunity_from_communication(communication, ignore_communication_link opportunity = frappe.get_doc({ "doctype": "Opportunity", + "company": company, "opportunity_from": opportunity_from, "party_name": lead }).insert(ignore_permissions=True) diff --git a/erpnext/crm/doctype/opportunity/test_opportunity.py b/erpnext/crm/doctype/opportunity/test_opportunity.py index 33d90076c4..04cd8a26ca 100644 --- a/erpnext/crm/doctype/opportunity/test_opportunity.py +++ b/erpnext/crm/doctype/opportunity/test_opportunity.py @@ -82,7 +82,8 @@ def make_opportunity(**args): if args.with_items: opp_doc.append('items', { "item_code": args.item_code or "_Test Item", - "qty": args.qty or 1 + "qty": args.qty or 1, + "uom": "_Test UOM" }) opp_doc.insert() diff --git a/erpnext/crm/doctype/social_media_post/social_media_post.js b/erpnext/crm/doctype/social_media_post/social_media_post.js index 3a14f2d2e9..0ce8b44e19 100644 --- a/erpnext/crm/doctype/social_media_post/social_media_post.js +++ b/erpnext/crm/doctype/social_media_post/social_media_post.js @@ -30,14 +30,14 @@ frappe.ui.form.on('Social Media Post', { let color = frm.doc.twitter_post_id ? "green" : "red"; let status = frm.doc.twitter_post_id ? "Posted" : "Not Posted"; html += `
- + Twitter : ${status}
` ; } if (frm.doc.linkedin){ let color = frm.doc.linkedin_post_id ? "green" : "red"; let status = frm.doc.linkedin_post_id ? "Posted" : "Not Posted"; html += `
- + LinkedIn : ${status}
` ; } html = `
${html}
`; diff --git a/erpnext/crm/report/lead_details/lead_details.js b/erpnext/crm/report/lead_details/lead_details.js new file mode 100644 index 0000000000..f92070daf3 --- /dev/null +++ b/erpnext/crm/report/lead_details/lead_details.js @@ -0,0 +1,52 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Lead Details"] = { + "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(), -12), + "reqd": 1 + }, + { + "fieldname":"to_date", + "label": __("To Date"), + "fieldtype": "Date", + "default": frappe.datetime.get_today(), + "reqd": 1 + }, + { + "fieldname":"status", + "label": __("Status"), + "fieldtype": "Select", + options: [ + { "value": "Lead", "label": __("Lead") }, + { "value": "Open", "label": __("Open") }, + { "value": "Replied", "label": __("Replied") }, + { "value": "Opportunity", "label": __("Opportunity") }, + { "value": "Quotation", "label": __("Quotation") }, + { "value": "Lost Quotation", "label": __("Lost Quotation") }, + { "value": "Interested", "label": __("Interested") }, + { "value": "Converted", "label": __("Converted") }, + { "value": "Do Not Contact", "label": __("Do Not Contact") }, + ], + }, + { + "fieldname":"territory", + "label": __("Territory"), + "fieldtype": "Link", + "options": "Territory", + } + ] +}; \ No newline at end of file diff --git a/erpnext/crm/report/lead_details/lead_details.json b/erpnext/crm/report/lead_details/lead_details.json index cdeb6bbe38..7871d0822f 100644 --- a/erpnext/crm/report/lead_details/lead_details.json +++ b/erpnext/crm/report/lead_details/lead_details.json @@ -7,16 +7,15 @@ "doctype": "Report", "idx": 3, "is_standard": "Yes", - "modified": "2020-01-22 16:51:56.591110", + "modified": "2020-07-26 23:59:49.897577", "modified_by": "Administrator", "module": "CRM", "name": "Lead Details", "owner": "Administrator", "prepared_report": 0, - "query": "SELECT\n `tabLead`.name as \"Lead Id:Link/Lead:120\",\n `tabLead`.lead_name as \"Lead Name::120\",\n\t`tabLead`.company_name as \"Company Name::120\",\n\t`tabLead`.status as \"Status::120\",\n\tconcat_ws(', ', \n\t\ttrim(',' from `tabAddress`.address_line1), \n\t\ttrim(',' from tabAddress.address_line2)\n\t) as 'Address::180',\n\t`tabAddress`.state as \"State::100\",\n\t`tabAddress`.pincode as \"Pincode::70\",\n\t`tabAddress`.country as \"Country::100\",\n\t`tabLead`.phone as \"Phone::100\",\n\t`tabLead`.mobile_no as \"Mobile No::100\",\n\t`tabLead`.email_id as \"Email Id::120\",\n\t`tabLead`.lead_owner as \"Lead Owner::120\",\n\t`tabLead`.source as \"Source::120\",\n\t`tabLead`.territory as \"Territory::120\",\n\t`tabLead`.notes as \"Notes::360\",\n `tabLead`.owner as \"Owner:Link/User:120\"\nFROM\n\t`tabLead`\n\tleft join `tabDynamic Link` on (\n\t\t`tabDynamic Link`.link_name=`tabLead`.name \n\t\tand `tabDynamic Link`.parenttype = 'Address'\n\t)\n\tleft join `tabAddress` on (\n\t\t`tabAddress`.name=`tabDynamic Link`.parent\n\t)\nWHERE\n\t`tabLead`.docstatus<2\nORDER BY\n\t`tabLead`.name asc", "ref_doctype": "Lead", "report_name": "Lead Details", - "report_type": "Query Report", + "report_type": "Script Report", "roles": [ { "role": "Sales User" diff --git a/erpnext/crm/report/lead_details/lead_details.py b/erpnext/crm/report/lead_details/lead_details.py new file mode 100644 index 0000000000..eeaaec2bce --- /dev/null +++ b/erpnext/crm/report/lead_details/lead_details.py @@ -0,0 +1,158 @@ +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +from frappe import _ +import frappe + +def execute(filters=None): + columns, data = get_columns(), get_data(filters) + return columns, data + +def get_columns(): + columns = [ + { + "label": _("Lead"), + "fieldname": "name", + "fieldtype": "Link", + "options": "Lead", + "width": 150, + }, + { + "label": _("Lead Name"), + "fieldname": "lead_name", + "fieldtype": "Data", + "width": 120 + }, + { + "fieldname":"status", + "label": _("Status"), + "fieldtype": "Data", + "width": 100 + }, + { + "fieldname":"lead_owner", + "label": _("Lead Owner"), + "fieldtype": "Link", + "options": "User", + "width": 100 + }, + { + "label": _("Territory"), + "fieldname": "territory", + "fieldtype": "Link", + "options": "Territory", + "width": 100 + }, + { + "label": _("Source"), + "fieldname": "source", + "fieldtype": "Data", + "width": 120 + }, + { + "label": _("Email"), + "fieldname": "email_id", + "fieldtype": "Data", + "width": 120 + }, + { + "label": _("Mobile"), + "fieldname": "mobile_no", + "fieldtype": "Data", + "width": 120 + }, + { + "label": _("Phone"), + "fieldname": "phone", + "fieldtype": "Data", + "width": 120 + }, + { + "label": _("Owner"), + "fieldname": "owner", + "fieldtype": "Link", + "options": "user", + "width": 120 + }, + { + "label": _("Company"), + "fieldname": "company", + "fieldtype": "Link", + "options": "Company", + "width": 120 + }, + { + "fieldname":"address", + "label": _("Address"), + "fieldtype": "Data", + "width": 130 + }, + { + "fieldname":"state", + "label": _("State"), + "fieldtype": "Data", + "width": 100 + }, + { + "fieldname":"pincode", + "label": _("Postal Code"), + "fieldtype": "Data", + "width": 90 + }, + { + "fieldname":"country", + "label": _("Country"), + "fieldtype": "Link", + "options": "Country", + "width": 100 + }, + + ] + return columns + +def get_data(filters): + return frappe.db.sql(""" + SELECT + `tabLead`.name, + `tabLead`.lead_name, + `tabLead`.status, + `tabLead`.lead_owner, + `tabLead`.territory, + `tabLead`.source, + `tabLead`.email_id, + `tabLead`.mobile_no, + `tabLead`.phone, + `tabLead`.owner, + `tabLead`.company, + concat_ws(', ', + trim(',' from `tabAddress`.address_line1), + trim(',' from tabAddress.address_line2) + ) AS address, + `tabAddress`.state, + `tabAddress`.pincode, + `tabAddress`.country + FROM + `tabLead` left join `tabDynamic Link` on ( + `tabLead`.name = `tabDynamic Link`.link_name and + `tabDynamic Link`.parenttype = 'Address') + left join `tabAddress` on ( + `tabAddress`.name=`tabDynamic Link`.parent) + WHERE + company = %(company)s + AND `tabLead`.creation BETWEEN %(from_date)s AND %(to_date)s + {conditions} + ORDER BY + `tabLead`.creation asc """.format(conditions=get_conditions(filters)), filters, as_dict=1) + +def get_conditions(filters) : + conditions = [] + + if filters.get("territory"): + conditions.append(" and `tabLead`.territory=%(territory)s") + + if filters.get("status"): + conditions.append(" and `tabLead`.status=%(status)s") + + return " ".join(conditions) if conditions else "" + diff --git a/erpnext/crm/report/lost_opportunity/lost_opportunity.js b/erpnext/crm/report/lost_opportunity/lost_opportunity.js new file mode 100644 index 0000000000..d79f8c8480 --- /dev/null +++ b/erpnext/crm/report/lost_opportunity/lost_opportunity.js @@ -0,0 +1,67 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Lost Opportunity"] = { + "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(), -12), + "reqd": 1 + }, + { + "fieldname":"to_date", + "label": __("To Date"), + "fieldtype": "Date", + "default": frappe.datetime.get_today(), + "reqd": 1 + }, + { + "fieldname":"lost_reason", + "label": __("Lost Reason"), + "fieldtype": "Link", + "options": "Opportunity Lost Reason" + }, + { + "fieldname":"territory", + "label": __("Territory"), + "fieldtype": "Link", + "options": "Territory" + }, + { + "fieldname":"opportunity_from", + "label": __("Opportunity From"), + "fieldtype": "Link", + "options": "DocType", + "get_query": function() { + return { + "filters": { + "name": ["in", ["Customer", "Lead"]], + } + } + } + }, + { + "fieldname":"party_name", + "label": __("Party"), + "fieldtype": "Dynamic Link", + "options": "opportunity_from" + }, + { + "fieldname":"contact_by", + "label": __("Next Contact By"), + "fieldtype": "Link", + "options": "User" + }, + ] +}; \ No newline at end of file diff --git a/erpnext/crm/report/lost_opportunity/lost_opportunity.json b/erpnext/crm/report/lost_opportunity/lost_opportunity.json index e7c5068b86..e7a8e12ba7 100644 --- a/erpnext/crm/report/lost_opportunity/lost_opportunity.json +++ b/erpnext/crm/report/lost_opportunity/lost_opportunity.json @@ -1,13 +1,14 @@ { "add_total_row": 0, "creation": "2018-12-31 16:30:57.188837", + "disable_prepared_report": 0, "disabled": 0, "docstatus": 0, "doctype": "Report", "idx": 0, "is_standard": "Yes", "json": "{\"order_by\": \"`tabOpportunity`.`modified` desc\", \"filters\": [[\"Opportunity\", \"status\", \"=\", \"Lost\"]], \"fields\": [[\"name\", \"Opportunity\"], [\"opportunity_from\", \"Opportunity\"], [\"party_name\", \"Opportunity\"], [\"customer_name\", \"Opportunity\"], [\"opportunity_type\", \"Opportunity\"], [\"status\", \"Opportunity\"], [\"contact_by\", \"Opportunity\"], [\"docstatus\", \"Opportunity\"], [\"lost_reason\", \"Lost Reason Detail\"]], \"add_totals_row\": 0, \"add_total_row\": 0, \"page_length\": 20}", - "modified": "2019-06-26 16:33:08.083618", + "modified": "2020-07-29 15:49:02.848845", "modified_by": "Administrator", "module": "CRM", "name": "Lost Opportunity", @@ -15,7 +16,7 @@ "prepared_report": 0, "ref_doctype": "Opportunity", "report_name": "Lost Opportunity", - "report_type": "Report Builder", + "report_type": "Script Report", "roles": [ { "role": "Sales User" diff --git a/erpnext/crm/report/lost_opportunity/lost_opportunity.py b/erpnext/crm/report/lost_opportunity/lost_opportunity.py new file mode 100644 index 0000000000..1aa4afe186 --- /dev/null +++ b/erpnext/crm/report/lost_opportunity/lost_opportunity.py @@ -0,0 +1,131 @@ +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +from frappe import _ +import frappe + +def execute(filters=None): + columns, data = get_columns(), get_data(filters) + return columns, data + +def get_columns(): + columns = [ + { + "label": _("Opportunity"), + "fieldname": "name", + "fieldtype": "Link", + "options": "Opportunity", + "width": 170, + }, + { + "label": _("Opportunity From"), + "fieldname": "opportunity_from", + "fieldtype": "Link", + "options": "DocType", + "width": 130 + }, + { + "label": _("Party"), + "fieldname":"party_name", + "fieldtype": "Dynamic Link", + "options": "opportunity_from", + "width": 160 + }, + { + "label": _("Customer/Lead Name"), + "fieldname":"customer_name", + "fieldtype": "Data", + "width": 150 + }, + { + "label": _("Opportunity Type"), + "fieldname": "opportunity_type", + "fieldtype": "Data", + "width": 130 + }, + { + "label": _("Lost Reasons"), + "fieldname": "lost_reason", + "fieldtype": "Data", + "width": 220 + }, + { + "label": _("Sales Stage"), + "fieldname": "sales_stage", + "fieldtype": "Link", + "options": "Sales Stage", + "width": 150 + }, + { + "label": _("Territory"), + "fieldname": "territory", + "fieldtype": "Link", + "options": "Territory", + "width": 150 + }, + { + "label": _("Next Contact By"), + "fieldname": "contact_by", + "fieldtype": "Link", + "options": "User", + "width": 150 + } + ] + return columns + +def get_data(filters): + return frappe.db.sql(""" + SELECT + `tabOpportunity`.name, + `tabOpportunity`.opportunity_from, + `tabOpportunity`.party_name, + `tabOpportunity`.customer_name, + `tabOpportunity`.opportunity_type, + `tabOpportunity`.contact_by, + GROUP_CONCAT(`tabOpportunity Lost Reason Detail`.lost_reason separator ', ') lost_reason, + `tabOpportunity`.sales_stage, + `tabOpportunity`.territory + FROM + `tabOpportunity` + {join} + WHERE + `tabOpportunity`.status = 'Lost' and `tabOpportunity`.company = %(company)s + AND `tabOpportunity`.modified BETWEEN %(from_date)s AND %(to_date)s + {conditions} + GROUP BY + `tabOpportunity`.name + ORDER BY + `tabOpportunity`.creation asc """.format(conditions=get_conditions(filters), join=get_join(filters)), filters, as_dict=1) + + +def get_conditions(filters): + conditions = [] + + if filters.get("territory"): + conditions.append(" and `tabOpportunity`.territory=%(territory)s") + + if filters.get("opportunity_from"): + conditions.append(" and `tabOpportunity`.opportunity_from=%(opportunity_from)s") + + if filters.get("party_name"): + conditions.append(" and `tabOpportunity`.party_name=%(party_name)s") + + if filters.get("contact_by"): + conditions.append(" and `tabOpportunity`.contact_by=%(contact_by)s") + + return " ".join(conditions) if conditions else "" + +def get_join(filters): + join = """LEFT JOIN `tabOpportunity Lost Reason Detail` + ON `tabOpportunity Lost Reason Detail`.parenttype = 'Opportunity' and + `tabOpportunity Lost Reason Detail`.parent = `tabOpportunity`.name""" + + if filters.get("lost_reason"): + join = """JOIN `tabOpportunity Lost Reason Detail` + ON `tabOpportunity Lost Reason Detail`.parenttype = 'Opportunity' and + `tabOpportunity Lost Reason Detail`.parent = `tabOpportunity`.name and + `tabOpportunity Lost Reason Detail`.lost_reason = '{0}' + """.format(filters.get("lost_reason")) + + return join \ No newline at end of file diff --git a/erpnext/education/doctype/student/student.js b/erpnext/education/doctype/student/student.js index 1936dcbd3e..fd23ae41ef 100644 --- a/erpnext/education/doctype/student/student.js +++ b/erpnext/education/doctype/student/student.js @@ -3,15 +3,15 @@ frappe.ui.form.on('Student', { setup: function(frm) { - frm.add_fetch("guardian", "guardian_name", "guardian_name"); - frm.add_fetch("student", "title", "full_name"); - frm.add_fetch("student", "gender", "gender"); - frm.add_fetch("student", "date_of_birth", "date_of_birth"); + frm.add_fetch('guardian', 'guardian_name', 'guardian_name'); + frm.add_fetch('student', 'title', 'full_name'); + frm.add_fetch('student', 'gender', 'gender'); + frm.add_fetch('student', 'date_of_birth', 'date_of_birth'); - frm.set_query("student", "siblings", function(doc, cdt, cdn) { + frm.set_query('student', 'siblings', function(doc) { return { - "filters": { - "name": ["!=", doc.name] + 'filters': { + 'name': ['!=', doc.name] } }; }) @@ -25,6 +25,12 @@ frappe.ui.form.on('Student', { {party_type:'Student', party:frm.doc.name}); }); } + + frappe.db.get_value('Education Settings', {name: 'Education Settings'}, 'user_creation_skip', (r) => { + if (cint(r.user_creation_skip) !== 1) { + frm.set_df_property('student_email_id', 'reqd', 1); + } + }); } }); diff --git a/erpnext/education/doctype/student/student.json b/erpnext/education/doctype/student/student.json index ac65c0cd7b..8ba9a7fa11 100644 --- a/erpnext/education/doctype/student/student.json +++ b/erpnext/education/doctype/student/student.json @@ -102,7 +102,6 @@ "fieldtype": "Data", "in_global_search": 1, "label": "Student Email Address", - "reqd": 1, "unique": 1 }, { @@ -255,7 +254,7 @@ ], "image_field": "image", "links": [], - "modified": "2020-07-23 18:14:06.366442", + "modified": "2020-09-07 19:28:08.914568", "modified_by": "Administrator", "module": "Education", "name": "Student", diff --git a/erpnext/education/doctype/student_applicant/student_applicant.js b/erpnext/education/doctype/student_applicant/student_applicant.js index 83621c5725..b4cfdf16e0 100644 --- a/erpnext/education/doctype/student_applicant/student_applicant.js +++ b/erpnext/education/doctype/student_applicant/student_applicant.js @@ -7,7 +7,7 @@ frappe.ui.form.on("Student Applicant", { }, refresh: function(frm) { - if(frm.doc.application_status== "Applied" && frm.doc.docstatus== 1 ) { + if (frm.doc.application_status==="Applied" && frm.doc.docstatus===1 ) { frm.add_custom_button(__("Approve"), function() { frm.set_value("application_status", "Approved"); frm.save_or_update(); @@ -20,10 +20,11 @@ frappe.ui.form.on("Student Applicant", { }, 'Actions'); } - if(frm.doc.application_status== "Approved" && frm.doc.docstatus== 1 ) { + if (frm.doc.application_status === "Approved" && frm.doc.docstatus === 1) { frm.add_custom_button(__("Enroll"), function() { frm.events.enroll(frm) }).addClass("btn-primary"); + frm.add_custom_button(__("Reject"), function() { frm.set_value("application_status", "Rejected"); frm.save_or_update(); @@ -35,7 +36,13 @@ frappe.ui.form.on("Student Applicant", { frappe.hide_msgprint(true); frappe.show_progress(__("Enrolling student"), data.progress[0],data.progress[1]); } - }) + }); + + frappe.db.get_value("Education Settings", {name: "Education Settings"}, "user_creation_skip", (r) => { + if (cint(r.user_creation_skip) !== 1) { + frm.set_df_property("student_email_id", "reqd", 1); + } + }); }, enroll: function(frm) { diff --git a/erpnext/education/doctype/student_applicant/student_applicant.json b/erpnext/education/doctype/student_applicant/student_applicant.json index e5d0bd37de..bca38fb264 100644 --- a/erpnext/education/doctype/student_applicant/student_applicant.json +++ b/erpnext/education/doctype/student_applicant/student_applicant.json @@ -1,1233 +1,292 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 1, - "allow_rename": 0, - "autoname": "naming_series:", - "beta": 0, - "creation": "2015-09-11 11:50:09.740807", - "custom": 0, - "default_print_format": "", - "docstatus": 0, - "doctype": "DocType", - "document_type": "Document", - "editable_grid": 0, - "engine": "InnoDB", + "actions": [], + "allow_import": 1, + "autoname": "naming_series:", + "creation": "2015-09-11 11:50:09.740807", + "doctype": "DocType", + "document_type": "Document", + "engine": "InnoDB", + "field_order": [ + "first_name", + "middle_name", + "last_name", + "program", + "lms_only", + "paid", + "column_break_8", + "naming_series", + "application_status", + "application_date", + "academic_year", + "academic_term", + "student_admission", + "image", + "section_break_4", + "date_of_birth", + "gender", + "blood_group", + "column_break_12", + "student_email_id", + "student_mobile_number", + "nationality", + "home_address", + "address_line_1", + "address_line_2", + "pincode", + "column_break_23", + "city", + "state", + "section_break_20", + "guardians", + "section_break_21", + "siblings", + "section_break_23", + "title", + "amended_from" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "first_name", - "fieldtype": "Data", - "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": "First Name", - "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 - }, + "fieldname": "first_name", + "fieldtype": "Data", + "in_global_search": 1, + "label": "First Name", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "middle_name", - "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": "Middle Name", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "middle_name", + "fieldtype": "Data", + "label": "Middle Name" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "last_name", - "fieldtype": "Data", - "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": "Last Name", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "last_name", + "fieldtype": "Data", + "in_global_search": 1, + "label": "Last Name" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "program", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 1, - "in_global_search": 1, - "in_list_view": 1, - "in_standard_filter": 1, - "label": "Program", - "length": 0, - "no_copy": 0, - "options": "Program", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "program", + "fieldtype": "Link", + "in_filter": 1, + "in_global_search": 1, + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Program", + "options": "Program", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "0", - "fieldname": "lms_only", - "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": "LMS Only", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "default": "0", + "fieldname": "lms_only", + "fieldtype": "Check", + "label": "LMS Only" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "paid", - "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": "Paid", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "default": "0", + "fieldname": "paid", + "fieldtype": "Check", + "label": "Paid" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "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 - }, + "fieldname": "column_break_8", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "", - "fieldname": "naming_series", - "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": "Naming Series", - "length": 0, - "no_copy": 1, - "options": "EDU-APP-.YYYY.-", - "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": 1, - "translatable": 0, - "unique": 0 - }, + "fieldname": "naming_series", + "fieldtype": "Select", + "label": "Naming Series", + "no_copy": 1, + "options": "EDU-APP-.YYYY.-", + "set_only_once": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 1, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:doc.docstatus != 0", - "fieldname": "application_status", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 1, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Application Status", - "length": 0, - "no_copy": 1, - "options": "Applied\nApproved\nRejected\nAdmitted", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "allow_on_submit": 1, + "depends_on": "eval:doc.docstatus != 0", + "fieldname": "application_status", + "fieldtype": "Select", + "in_filter": 1, + "label": "Application Status", + "no_copy": 1, + "options": "Applied\nApproved\nRejected\nAdmitted" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "Today", - "fieldname": "application_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": "Application Date", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "default": "Today", + "fieldname": "application_date", + "fieldtype": "Date", + "label": "Application Date" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "academic_year", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Academic Year", - "length": 0, - "no_copy": 0, - "options": "Academic Year", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "academic_year", + "fieldtype": "Link", + "label": "Academic Year", + "options": "Academic Year" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "academic_term", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Academic Term", - "length": 0, - "no_copy": 0, - "options": "Academic Term", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "academic_term", + "fieldtype": "Link", + "label": "Academic Term", + "options": "Academic Term" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "student_admission", - "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 Admission", - "length": 0, - "no_copy": 0, - "options": "Student Admission", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "student_admission", + "fieldtype": "Link", + "label": "Student Admission", + "options": "Student Admission" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "image", - "fieldtype": "Attach Image", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Image", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "image", + "fieldtype": "Attach Image", + "hidden": 1, + "label": "Image" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": "", - "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, - "label": "Personal Details", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "section_break_4", + "fieldtype": "Section Break", + "label": "Personal Details" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "date_of_birth", - "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": "Date of Birth", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "date_of_birth", + "fieldtype": "Date", + "label": "Date of Birth" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "gender", - "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": "Gender", - "length": 0, - "no_copy": 0, - "options": "\nMale\nFemale", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "gender", + "fieldtype": "Select", + "label": "Gender", + "options": "\nMale\nFemale" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "blood_group", - "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": "Blood Group", - "length": 0, - "no_copy": 0, - "options": "\nA+\nA-\nB+\nB-\nO+\nO-\nAB+\nAB-", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "blood_group", + "fieldtype": "Select", + "label": "Blood Group", + "options": "\nA+\nA-\nB+\nB-\nO+\nO-\nAB+\nAB-" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_12", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_12", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "student_email_id", - "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": "Student Email Address", - "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, + "fieldname": "student_email_id", + "fieldtype": "Data", + "label": "Student Email Address", "unique": 1 - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "student_mobile_number", - "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": "Student Mobile 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 - }, + "fieldname": "student_mobile_number", + "fieldtype": "Data", + "label": "Student Mobile Number" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "nationality", - "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": "Nationality", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "nationality", + "fieldtype": "Data", + "label": "Nationality" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "home_address", - "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": "Home Address", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "home_address", + "fieldtype": "Section Break", + "label": "Home Address" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "address_line_1", - "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": "Address Line 1", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "address_line_1", + "fieldtype": "Data", + "label": "Address Line 1" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "address_line_2", - "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": "Address Line 2", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "address_line_2", + "fieldtype": "Data", + "label": "Address Line 2" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "pincode", - "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": "Pincode", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "pincode", + "fieldtype": "Data", + "label": "Pincode" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_23", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_23", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "city", - "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": "City", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "city", + "fieldtype": "Data", + "label": "City" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "state", - "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": "State", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "state", + "fieldtype": "Data", + "label": "State" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "columns": 0, - "fieldname": "section_break_20", - "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": "Guardian Details", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "collapsible": 1, + "fieldname": "section_break_20", + "fieldtype": "Section Break", + "label": "Guardian Details" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "guardians", - "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": "Guardians", - "length": 0, - "no_copy": 0, - "options": "Student 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 - }, + "fieldname": "guardians", + "fieldtype": "Table", + "label": "Guardians", + "options": "Student Guardian" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "columns": 0, - "fieldname": "section_break_21", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Sibling Details", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "collapsible": 1, + "fieldname": "section_break_21", + "fieldtype": "Section Break", + "label": "Sibling Details" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "siblings", - "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": "Siblings", - "length": 0, - "no_copy": 0, - "options": "Student Sibling", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "siblings", + "fieldtype": "Table", + "label": "Siblings", + "options": "Student Sibling" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "section_break_23", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "section_break_23", + "fieldtype": "Section Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "title", - "fieldtype": "Data", - "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": "Title", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "title", + "fieldtype": "Data", + "hidden": 1, + "label": "Title" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "amended_from", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Amended From", - "length": 0, - "no_copy": 1, - "options": "Student Applicant", - "permlevel": 0, - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Student Applicant", + "print_hide": 1, + "read_only": 1 } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_field": "image", - "image_view": 0, - "in_create": 0, - "is_submittable": 1, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "menu_index": 0, - "modified": "2018-10-05 13:15:59.283862", - "modified_by": "Administrator", - "module": "Education", - "name": "Student Applicant", - "name_case": "", - "owner": "Administrator", + ], + "image_field": "image", + "is_submittable": 1, + "links": [], + "modified": "2020-09-07 19:31:30.063563", + "modified_by": "Administrator", + "module": "Education", + "name": "Student Applicant", + "owner": "Administrator", "permissions": [ { - "amend": 1, - "cancel": 1, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 1, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Academics User", - "set_user_permissions": 0, - "share": 1, - "submit": 1, + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "import": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Academics User", + "share": 1, + "submit": 1, "write": 1 } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "restrict_to_domain": "Education", - "show_name_in_global_search": 1, - "sort_field": "modified", - "sort_order": "DESC", - "title_field": "title", - "track_changes": 0, - "track_seen": 0, - "track_views": 0 -} + ], + "restrict_to_domain": "Education", + "show_name_in_global_search": 1, + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "title" +} \ No newline at end of file diff --git a/erpnext/erpnext_integrations/desk_page/erpnext_integrations/erpnext_integrations.json b/erpnext/erpnext_integrations/desk_page/erpnext_integrations/erpnext_integrations.json new file mode 100644 index 0000000000..8dcc77d174 --- /dev/null +++ b/erpnext/erpnext_integrations/desk_page/erpnext_integrations/erpnext_integrations.json @@ -0,0 +1,40 @@ +{ + "cards": [ + { + "hidden": 0, + "label": "Marketplace", + "links": "[\n {\n \"description\": \"Woocommerce marketplace settings\",\n \"label\": \"Woocommerce Settings\",\n \"name\": \"Woocommerce Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Amazon MWS settings\",\n \"label\": \"Amazon MWS Settings\",\n \"name\": \"Amazon MWS Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Shopify settings\",\n \"label\": \"Shopify Settings\",\n \"name\": \"Shopify Settings\",\n \"type\": \"doctype\"\n }\n]" + }, + { + "hidden": 0, + "label": "Payments", + "links": "[\n {\n \"description\": \"GoCardless payment gateway settings\",\n \"label\": \"GoCardless Settings\",\n \"name\": \"GoCardless Settings\",\n \"type\": \"doctype\"\n }\n]" + }, + { + "hidden": 0, + "label": "Settings", + "links": "[\n {\n \"description\": \"Plaid settings\",\n \"label\": \"Plaid Settings\",\n \"name\": \"Plaid Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Exotel settings\",\n \"label\": \"Exotel Settings\",\n \"name\": \"Exotel Settings\",\n \"type\": \"doctype\"\n }\n]" + } + ], + "category": "Modules", + "charts": [], + "creation": "2020-08-20 19:30:48.138801", + "developer_mode_only": 0, + "disable_user_customization": 0, + "docstatus": 0, + "doctype": "Desk Page", + "extends": "Integrations", + "extends_another_page": 1, + "hide_custom": 1, + "idx": 0, + "is_standard": 1, + "label": "ERPNext Integrations", + "modified": "2020-08-23 16:30:51.494655", + "modified_by": "Administrator", + "module": "ERPNext Integrations", + "name": "ERPNext Integrations", + "owner": "Administrator", + "pin_to_bottom": 0, + "pin_to_top": 0, + "shortcuts": [] +} \ No newline at end of file diff --git a/erpnext/healthcare/doctype/clinical_procedure/test_clinical_procedure.py b/erpnext/healthcare/doctype/clinical_procedure/test_clinical_procedure.py index 207351ff20..4ee5f6bad3 100644 --- a/erpnext/healthcare/doctype/clinical_procedure/test_clinical_procedure.py +++ b/erpnext/healthcare/doctype/clinical_procedure/test_clinical_procedure.py @@ -7,6 +7,8 @@ import unittest import frappe from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import create_healthcare_docs, create_clinical_procedure_template +test_dependencies = ['Item'] + class TestClinicalProcedure(unittest.TestCase): def test_procedure_template_item(self): patient, medical_department, practitioner = create_healthcare_docs() diff --git a/erpnext/healthcare/doctype/lab_test/lab_test.js b/erpnext/healthcare/doctype/lab_test/lab_test.js index 8036c7dc13..f1634c1294 100644 --- a/erpnext/healthcare/doctype/lab_test/lab_test.js +++ b/erpnext/healthcare/doctype/lab_test/lab_test.js @@ -34,10 +34,10 @@ frappe.ui.form.on('Lab Test', { if (frm.doc.docstatus === 1 && frm.doc.status !== 'Approved' && frm.doc.status !== 'Rejected') { frm.add_custom_button(__('Approve'), function () { status_update(1, frm); - }); + }, __('Actions')); frm.add_custom_button(__('Reject'), function () { status_update(0, frm); - }); + }, __('Actions')); } } @@ -179,23 +179,6 @@ var show_lab_tests = function (frm, lab_test_list) { d.show(); }; -cur_frm.cscript.custom_before_submit = function (doc) { - if (doc.normal_test_items) { - for (let result in doc.normal_test_items) { - if (!doc.normal_test_items[result].result_value && !doc.normal_test_items[result].allow_blank && doc.normal_test_items[result].require_result_value) { - frappe.throw(__('Please input all required result values')); - } - } - } - if (doc.descriptive_test_items) { - for (let result in doc.descriptive_test_items) { - if (!doc.descriptive_test_items[result].result_value && !doc.descriptive_test_items[result].allow_blank && doc.descriptive_test_items[result].require_result_value) { - frappe.throw(__('Please input all required result values')); - } - } - } -}; - var make_dialog = function (frm, emailed, printed) { var number = frm.doc.mobile; @@ -203,7 +186,7 @@ var make_dialog = function (frm, emailed, printed) { title: 'Send SMS', width: 400, fields: [ - { fieldname: 'sms_type', fieldtype: 'Select', label: 'Type', options: ['Emailed', 'Printed'] }, + { fieldname: 'result_format', fieldtype: 'Select', label: 'Result Format', options: ['Emailed', 'Printed'] }, { fieldname: 'number', fieldtype: 'Data', label: 'Mobile Number', reqd: 1 }, { fieldname: 'message', fieldtype: 'Small Text', label: 'Message', reqd: 1 } ], @@ -217,22 +200,22 @@ var make_dialog = function (frm, emailed, printed) { dialog.hide(); } }); - if (frm.doc.report_preference == 'Print') { + if (frm.doc.report_preference === 'Print') { dialog.set_values({ - 'sms_type': 'Printed', + 'result_format': 'Printed', 'number': number, 'message': printed }); } else { dialog.set_values({ - 'sms_type': 'Emailed', + 'result_format': 'Emailed', 'number': number, 'message': emailed }); } var fd = dialog.fields_dict; - $(fd.sms_type.input).change(function () { - if (dialog.get_value('sms_type') == 'Emailed') { + $(fd.result_format.input).change(function () { + if (dialog.get_value('result_format') === 'Emailed') { dialog.set_values({ 'number': number, 'message': emailed diff --git a/erpnext/healthcare/doctype/lab_test/lab_test.json b/erpnext/healthcare/doctype/lab_test/lab_test.json index 2eb8014b7e..edf1d911aa 100644 --- a/erpnext/healthcare/doctype/lab_test/lab_test.json +++ b/erpnext/healthcare/doctype/lab_test/lab_test.json @@ -84,7 +84,7 @@ "fieldname": "naming_series", "fieldtype": "Select", "label": "Series", - "options": "LP-", + "options": "HLC-LAB-.YYYY.-", "print_hide": 1, "report_hide": 1, "reqd": 1 @@ -197,11 +197,10 @@ { "fieldname": "status", "fieldtype": "Select", + "in_list_view": 1, "label": "Status", "options": "Draft\nCompleted\nApproved\nRejected\nCancelled", - "print_hide": 1, "read_only": 1, - "report_hide": 1, "search_index": 1 }, { @@ -249,8 +248,8 @@ { "fieldname": "result_date", "fieldtype": "Date", - "hidden": 1, "label": "Result Date", + "read_only": 1, "search_index": 1 }, { @@ -354,7 +353,8 @@ }, { "fieldname": "sb_normal", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "label": "Compound Test Result" }, { "fieldname": "normal_test_items", @@ -369,11 +369,13 @@ { "depends_on": "descriptive_toggle", "fieldname": "organisms_section", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "label": "Organism Test Result" }, { "fieldname": "sb_sensitivity", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "label": "Sensitivity Test Result" }, { "fieldname": "sensitivity_test_items", @@ -383,8 +385,10 @@ "report_hide": 1 }, { + "collapsible": 1, "fieldname": "sb_comments", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "label": "Comments" }, { "fieldname": "lab_test_comment", @@ -531,7 +535,8 @@ }, { "fieldname": "sb_descriptive", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "label": "Descriptive Test Result" }, { "default": "0", @@ -550,7 +555,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2020-07-16 13:35:24.811062", + "modified": "2020-07-30 18:18:38.516215", "modified_by": "Administrator", "module": "Healthcare", "name": "Lab Test", diff --git a/erpnext/healthcare/doctype/lab_test/lab_test.py b/erpnext/healthcare/doctype/lab_test/lab_test.py index 865f4a14e3..2db7743865 100644 --- a/erpnext/healthcare/doctype/lab_test/lab_test.py +++ b/erpnext/healthcare/doctype/lab_test/lab_test.py @@ -6,23 +6,24 @@ from __future__ import unicode_literals import frappe from frappe import _ from frappe.model.document import Document -from frappe.utils import getdate, cstr +from frappe.utils import getdate, cstr, get_link_to_form class LabTest(Document): + def validate(self): + if not self.is_new(): + self.set_secondary_uom_result() + def on_submit(self): + self.validate_result_values() self.db_set('submitted_date', getdate()) self.db_set('status', 'Completed') insert_lab_test_to_medical_record(self) def on_cancel(self): - delete_lab_test_from_medical_record(self) self.db_set('status', 'Cancelled') + delete_lab_test_from_medical_record(self) self.reload() - def validate(self): - if not self.is_new(): - self.set_secondary_uom_result() - def on_update(self): if self.sensitivity_test_items: sensitivity = sorted(self.sensitivity_test_items, key=lambda x: x.antibiotic_sensitivity) @@ -51,7 +52,20 @@ class LabTest(Document): item.secondary_uom_result = float(item.result_value) * float(item.conversion_factor) except: item.secondary_uom_result = '' - frappe.msgprint(_('Result for Secondary UOM not calculated for row #{0}'.format(item.idx)), title = _('Warning')) + frappe.msgprint(_('Row #{0}: Result for Secondary UOM not calculated'.format(item.idx)), title = _('Warning')) + + def validate_result_values(self): + if self.normal_test_items: + for item in self.normal_test_items: + if not item.result_value and not item.allow_blank and item.require_result_value: + frappe.throw(_('Row #{0}: Please enter the result value for {1}').format( + item.idx, frappe.bold(item.lab_test_name)), title=_('Mandatory Results')) + + if self.descriptive_test_items: + for item in self.descriptive_test_items: + if not item.result_value and not item.allow_blank and item.require_result_value: + frappe.throw(_('Row #{0}: Please enter the result value for {1}').format( + item.idx, frappe.bold(item.lab_test_particulars)), title=_('Mandatory Results')) def create_test_from_template(lab_test): @@ -89,7 +103,7 @@ def create_multiple(doctype, docname): lab_test_created = create_lab_test_from_encounter(docname) if lab_test_created: - frappe.msgprint(_('Lab Test(s) {0} created'.format(lab_test_created))) + frappe.msgprint(_('Lab Test(s) {0} created successfully').format(lab_test_created), indicator='green') else: frappe.msgprint(_('No Lab Tests created')) @@ -211,8 +225,9 @@ def create_sample_doc(template, patient, invoice, company = None): 'docstatus': 0, 'sample': template.sample }) + if sample_exists: - # Update Sample Collection by adding quantity + # update sample collection by adding quantity sample_collection = frappe.get_doc('Sample Collection', sample_exists[0][0]) quantity = int(sample_collection.sample_qty) + int(template.sample_qty) if template.sample_details: @@ -238,7 +253,7 @@ def create_sample_doc(template, patient, invoice, company = None): sample_collection.company = company if template.sample_details: - sample_collection.sample_details = 'Test :' + (template.get('lab_test_name') or template.get('template')) + '\n' + 'Collection Detials:\n\t' + template.sample_details + sample_collection.sample_details = _('Test :') + (template.get('lab_test_name') or template.get('template')) + '\n' + 'Collection Detials:\n\t' + template.sample_details sample_collection.save(ignore_permissions=True) return sample_collection @@ -248,26 +263,31 @@ def create_sample_collection(lab_test, template, patient, invoice): sample_collection = create_sample_doc(template, patient, invoice, lab_test.company) if sample_collection: lab_test.sample = sample_collection.name - + sample_collection_doc = get_link_to_form('Sample Collection', sample_collection.name) + frappe.msgprint(_('Sample Collection {0} has been created').format(sample_collection_doc), + title=_('Sample Collection'), indicator='green') return lab_test def load_result_format(lab_test, template, prescription, invoice): if template.lab_test_template_type == 'Single': create_normals(template, lab_test) + elif template.lab_test_template_type == 'Compound': create_compounds(template, lab_test, False) + elif template.lab_test_template_type == 'Descriptive': create_descriptives(template, lab_test) + elif template.lab_test_template_type == 'Grouped': # Iterate for each template in the group and create one result for all. for lab_test_group in template.lab_test_groups: # Template_in_group = None if lab_test_group.lab_test_template: - template_in_group = frappe.get_doc('Lab Test Template', - lab_test_group.lab_test_template) + template_in_group = frappe.get_doc('Lab Test Template', lab_test_group.lab_test_template) if template_in_group: if template_in_group.lab_test_template_type == 'Single': create_normals(template_in_group, lab_test) + elif template_in_group.lab_test_template_type == 'Compound': normal_heading = lab_test.append('normal_test_items') normal_heading.lab_test_name = template_in_group.lab_test_name @@ -275,6 +295,7 @@ def load_result_format(lab_test, template, prescription, invoice): normal_heading.allow_blank = 1 normal_heading.template = template_in_group.name create_compounds(template_in_group, lab_test, True) + elif template_in_group.lab_test_template_type == 'Descriptive': descriptive_heading = lab_test.append('descriptive_test_items') descriptive_heading.lab_test_name = template_in_group.lab_test_name @@ -282,6 +303,7 @@ def load_result_format(lab_test, template, prescription, invoice): descriptive_heading.allow_blank = 1 descriptive_heading.template = template_in_group.name create_descriptives(template_in_group, lab_test) + else: # Lab Test Group - Add New Line normal = lab_test.append('normal_test_items') normal.lab_test_name = lab_test_group.group_event @@ -292,6 +314,7 @@ def load_result_format(lab_test, template, prescription, invoice): normal.allow_blank = lab_test_group.allow_blank normal.require_result_value = 1 normal.template = template.name + if template.lab_test_template_type != 'No Result': if prescription: lab_test.prescription = prescription @@ -302,9 +325,10 @@ def load_result_format(lab_test, template, prescription, invoice): @frappe.whitelist() def get_employee_by_user_id(user_id): - emp_id = frappe.db.get_value('Employee', { 'user_id': user_id }) - employee = frappe.get_doc('Employee', emp_id) - return employee + emp_id = frappe.db.exists('Employee', { 'user_id': user_id }) + if emp_id: + return frappe.get_doc('Employee', emp_id) + return None def insert_lab_test_to_medical_record(doc): table_row = False @@ -325,7 +349,7 @@ def insert_lab_test_to_medical_record(doc): table_row += ' ' + frappe.bold(_('Lab Test Result: ')) + item.result_value if item.normal_range: - table_row += ' ' + _('Normal Range:') + item.normal_range + table_row += ' ' + _('Normal Range: ') + item.normal_range table_row += ' ' + comment elif doc.descriptive_test_items: @@ -356,7 +380,7 @@ def insert_lab_test_to_medical_record(doc): medical_record.save(ignore_permissions = True) def delete_lab_test_from_medical_record(self): - medical_record_id = frappe.db.sql('select name from `tabPatient Medical Record` where reference_name= %s', (self.name)) + medical_record_id = frappe.db.sql('select name from `tabPatient Medical Record` where reference_name=%s', (self.name)) if medical_record_id and medical_record_id[0][0]: frappe.delete_doc('Patient Medical Record', medical_record_id[0][0]) diff --git a/erpnext/healthcare/doctype/lab_test/lab_test_list.js b/erpnext/healthcare/doctype/lab_test/lab_test_list.js index b7f157c38b..7b5b9d922a 100644 --- a/erpnext/healthcare/doctype/lab_test/lab_test_list.js +++ b/erpnext/healthcare/doctype/lab_test/lab_test_list.js @@ -3,13 +3,16 @@ */ frappe.listview_settings['Lab Test'] = { add_fields: ['name', 'status', 'invoiced'], - filters: [['docstatus', '=', '0']], + filters: [['docstatus', '=', '1']], get_indicator: function (doc) { - if (doc.status == 'Approved') { - return [__('Approved'), 'green', 'status, = ,Approved']; - } - if (doc.status == 'Rejected') { + if (doc.status === 'Approved') { + return [__('Approved'), 'green', 'status, =, Approved']; + } else if (doc.status === 'Rejected') { return [__('Rejected'), 'orange', 'status, =, Rejected']; + } else if (doc.status === 'Completed') { + return [__('Completed'), 'green', 'status, =, Completed']; + } else if (doc.status === 'Cancelled') { + return [__('Cancelled'), 'red', 'status, =, Cancelled']; } }, onload: function (listview) { @@ -21,7 +24,7 @@ frappe.listview_settings['Lab Test'] = { var create_multiple_dialog = function (listview) { var dialog = new frappe.ui.Dialog({ - title: 'Create Multiple Lab Test', + title: 'Create Multiple Lab Tests', width: 100, fields: [ { fieldtype: 'Link', label: 'Patient', fieldname: 'patient', options: 'Patient', reqd: 1 }, @@ -41,7 +44,7 @@ var create_multiple_dialog = function (listview) { } } ], - primary_action_label: __('Create Lab Test'), + primary_action_label: __('Create'), primary_action: function () { frappe.call({ method: 'erpnext.healthcare.doctype.lab_test.lab_test.create_multiple', diff --git a/erpnext/healthcare/doctype/lab_test/test_lab_test.py b/erpnext/healthcare/doctype/lab_test/test_lab_test.py index 4131ad99b7..79ab8a4d7f 100644 --- a/erpnext/healthcare/doctype/lab_test/test_lab_test.py +++ b/erpnext/healthcare/doctype/lab_test/test_lab_test.py @@ -3,8 +3,204 @@ # See license.txt from __future__ import unicode_literals import unittest - -# test_records = frappe.get_test_records('Lab Test') +import frappe +from frappe.utils import getdate, nowtime +from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import create_patient +from erpnext.healthcare.doctype.lab_test.lab_test import create_multiple +from erpnext.healthcare.doctype.healthcare_settings.healthcare_settings import get_receivable_account, get_income_account +from erpnext.healthcare.doctype.patient_medical_record.test_patient_medical_record import create_lab_test_template as create_blood_test_template class TestLabTest(unittest.TestCase): - pass + def test_lab_test_item(self): + lab_template = create_lab_test_template() + self.assertTrue(frappe.db.exists('Item', lab_template.item)) + self.assertEqual(frappe.db.get_value('Item Price', {'item_code':lab_template.item}, 'price_list_rate'), lab_template.lab_test_rate) + + lab_template.disabled = 1 + lab_template.save() + self.assertEquals(frappe.db.get_value('Item', lab_template.item, 'disabled'), 1) + + lab_template.reload() + + lab_template.disabled = 0 + lab_template.save() + + def test_descriptive_lab_test(self): + lab_template = create_lab_test_template() + + # blank result value not allowed as per template + lab_test = create_lab_test(lab_template) + lab_test.descriptive_test_items[0].result_value = 12 + lab_test.descriptive_test_items[2].result_value = 1 + lab_test.save() + self.assertRaises(frappe.ValidationError, lab_test.submit) + + def test_sample_collection(self): + frappe.db.set_value('Healthcare Settings', 'Healthcare Settings', 'create_sample_collection_for_lab_test', 1) + lab_template = create_lab_test_template() + + lab_test = create_lab_test(lab_template) + lab_test.descriptive_test_items[0].result_value = 12 + lab_test.descriptive_test_items[1].result_value = 1 + lab_test.descriptive_test_items[2].result_value = 2.3 + lab_test.save() + + # check sample collection created + self.assertTrue(frappe.db.exists('Sample Collection', {'sample': lab_template.sample})) + + frappe.db.set_value('Healthcare Settings', 'Healthcare Settings', 'create_sample_collection_for_lab_test', 0) + lab_test = create_lab_test(lab_template) + lab_test.descriptive_test_items[0].result_value = 12 + lab_test.descriptive_test_items[1].result_value = 1 + lab_test.descriptive_test_items[2].result_value = 2.3 + lab_test.save() + + # sample collection should not be created + lab_test.reload() + self.assertEquals(lab_test.sample, None) + + def test_create_lab_tests_from_sales_invoice(self): + sales_invoice = create_sales_invoice() + create_multiple('Sales Invoice', sales_invoice.name) + sales_invoice.reload() + self.assertIsNotNone(sales_invoice.items[0].reference_dn) + self.assertIsNotNone(sales_invoice.items[1].reference_dn) + + def test_create_lab_tests_from_patient_encounter(self): + patient_encounter = create_patient_encounter() + create_multiple('Patient Encounter', patient_encounter.name) + patient_encounter.reload() + self.assertTrue(patient_encounter.lab_test_prescription[0].lab_test_created) + self.assertTrue(patient_encounter.lab_test_prescription[0].lab_test_created) + + +def create_lab_test_template(test_sensitivity=0, sample_collection=1): + medical_department = create_medical_department() + if frappe.db.exists('Lab Test Template', 'Insulin Resistance'): + return frappe.get_doc('Lab Test Template', 'Insulin Resistance') + template = frappe.new_doc('Lab Test Template') + template.lab_test_name = 'Insulin Resistance' + template.lab_test_template_type = 'Descriptive' + template.lab_test_code = 'Insulin Resistance' + template.lab_test_group = 'Services' + template.department = medical_department + template.is_billable = 1 + template.lab_test_description = 'Insulin Resistance' + template.lab_test_rate = 2000 + + for entry in ['FBS', 'Insulin', 'IR']: + template.append('descriptive_test_templates', { + 'particulars': entry, + 'allow_blank': 1 if entry=='IR' else 0 + }) + + if test_sensitivity: + template.sensitivity = 1 + + if sample_collection: + template.sample = create_lab_test_sample() + template.sample_qty = 5.0 + + template.save() + return template + +def create_medical_department(): + medical_department = frappe.db.exists('Medical Department', '_Test Medical Department') + if not medical_department: + medical_department = frappe.new_doc('Medical Department') + medical_department.department = '_Test Medical Department' + medical_department.save() + medical_department = medical_department.name + + return medical_department + +def create_lab_test(lab_template): + patient = create_patient() + lab_test = frappe.new_doc('Lab Test') + lab_test.template = lab_template.name + lab_test.patient = patient + lab_test.patient_sex = 'Female' + lab_test.save() + + return lab_test + +def create_lab_test_sample(): + blood_sample = frappe.db.exists('Lab Test Sample', 'Blood Sample') + if blood_sample: + return blood_sample + + sample = frappe.new_doc('Lab Test Sample') + sample.sample = 'Blood Sample' + sample.sample_uom = 'U/ml' + sample.save() + + return sample.name + +def create_sales_invoice(): + patient = create_patient() + medical_department = create_medical_department() + insulin_resistance_template = create_lab_test_template() + blood_test_template = create_blood_test_template(medical_department) + + sales_invoice = frappe.new_doc('Sales Invoice') + sales_invoice.patient = patient + sales_invoice.customer = frappe.db.get_value('Patient', patient, 'customer') + sales_invoice.due_date = getdate() + sales_invoice.company = '_Test Company' + sales_invoice.debit_to = get_receivable_account('_Test Company') + + tests = [insulin_resistance_template, blood_test_template] + for entry in tests: + sales_invoice.append('items', { + 'item_code': entry.item, + 'item_name': entry.lab_test_name, + 'description': entry.lab_test_description, + 'qty': 1, + 'uom': 'Nos', + 'conversion_factor': 1, + 'income_account': get_income_account(None, '_Test Company'), + 'rate': entry.lab_test_rate, + 'amount': entry.lab_test_rate + }) + + sales_invoice.set_missing_values() + + sales_invoice.submit() + return sales_invoice + +def create_patient_encounter(): + patient = create_patient() + medical_department = create_medical_department() + insulin_resistance_template = create_lab_test_template() + blood_test_template = create_blood_test_template(medical_department) + + patient_encounter = frappe.new_doc('Patient Encounter') + patient_encounter.patient = patient + patient_encounter.practitioner = create_practitioner() + patient_encounter.encounter_date = getdate() + patient_encounter.encounter_time = nowtime() + + tests = [insulin_resistance_template, blood_test_template] + for entry in tests: + patient_encounter.append('lab_test_prescription', { + 'lab_test_code': entry.item, + 'lab_test_name': entry.lab_test_name + }) + + patient_encounter.submit() + return patient_encounter + + +def create_practitioner(): + practitioner = frappe.db.exists('Healthcare Practitioner', '_Test Healthcare Practitioner') + + if not practitioner: + practitioner = frappe.new_doc('Healthcare Practitioner') + practitioner.first_name = '_Test Healthcare Practitioner' + practitioner.gender = 'Female' + practitioner.op_consulting_charge = 500 + practitioner.inpatient_visit_charge = 500 + practitioner.save(ignore_permissions=True) + practitioner = practitioner.name + + return practitioner diff --git a/erpnext/healthcare/doctype/lab_test_group_template/lab_test_group_template.json b/erpnext/healthcare/doctype/lab_test_group_template/lab_test_group_template.json index beea7a357e..2767f7ec77 100644 --- a/erpnext/healthcare/doctype/lab_test_group_template/lab_test_group_template.json +++ b/erpnext/healthcare/doctype/lab_test_group_template/lab_test_group_template.json @@ -93,7 +93,8 @@ "depends_on": "secondary_uom", "fieldname": "conversion_factor", "fieldtype": "Float", - "label": "Conversion Factor" + "label": "Conversion Factor", + "mandatory_depends_on": "secondary_uom" }, { "default": "0", @@ -106,7 +107,7 @@ ], "istable": 1, "links": [], - "modified": "2020-06-24 10:59:01.921924", + "modified": "2020-07-30 12:36:03.082391", "modified_by": "Administrator", "module": "Healthcare", "name": "Lab Test Group Template", diff --git a/erpnext/healthcare/doctype/lab_test_template/lab_test_template.json b/erpnext/healthcare/doctype/lab_test_template/lab_test_template.json index db64297269..c3fc842047 100644 --- a/erpnext/healthcare/doctype/lab_test_template/lab_test_template.json +++ b/erpnext/healthcare/doctype/lab_test_template/lab_test_template.json @@ -34,14 +34,15 @@ "descriptive_test_templates", "section_break_group", "lab_test_groups", - "medical_coding_section", - "medical_code_standard", - "medical_code", "sb_sample_collection", "sample", "sample_uom", "sample_qty", + "column_break_33", "sample_details", + "medical_coding_section", + "medical_code", + "medical_code_standard", "worksheet_section", "worksheet_instructions", "result_legend_section", @@ -112,7 +113,7 @@ { "default": "1", "depends_on": "eval:doc.lab_test_template_type != 'Grouped'", - "description": "If unchecked, the item wont be appear in Sales Invoice, but can be used in group test creation. ", + "description": "If unchecked, the item will not be available in Sales Invoices for billing but can be used in group test creation. ", "fieldname": "is_billable", "fieldtype": "Check", "label": "Is Billable", @@ -128,6 +129,7 @@ "mandatory_depends_on": "eval:doc.is_billable == 1" }, { + "collapsible": 1, "fieldname": "medical_coding_section", "fieldtype": "Section Break", "label": "Medical Coding" @@ -184,7 +186,7 @@ "depends_on": "eval:doc.lab_test_template_type == 'Descriptive'", "fieldname": "section_break_special", "fieldtype": "Section Break", - "label": "Descriptive" + "label": "Descriptive Test" }, { "default": "0", @@ -196,7 +198,7 @@ "depends_on": "eval:doc.lab_test_template_type == 'Grouped'", "fieldname": "section_break_group", "fieldtype": "Section Break", - "label": "Group" + "label": "Group Tests" }, { "fieldname": "lab_test_groups", @@ -217,7 +219,6 @@ "no_copy": 1 }, { - "collapsible": 1, "fieldname": "sb_sample_collection", "fieldtype": "Section Break", "label": "Sample Collection" @@ -311,10 +312,14 @@ "fieldname": "descriptive_test_templates", "fieldtype": "Table", "options": "Descriptive Test Template" + }, + { + "fieldname": "column_break_33", + "fieldtype": "Column Break" } ], "links": [], - "modified": "2020-07-13 12:57:09.925436", + "modified": "2020-07-30 14:32:40.449818", "modified_by": "Administrator", "module": "Healthcare", "name": "Lab Test Template", diff --git a/erpnext/healthcare/doctype/lab_test_template/lab_test_template.py b/erpnext/healthcare/doctype/lab_test_template/lab_test_template.py index 6f0d08cf85..543dee27eb 100644 --- a/erpnext/healthcare/doctype/lab_test_template/lab_test_template.py +++ b/erpnext/healthcare/doctype/lab_test_template/lab_test_template.py @@ -15,7 +15,8 @@ class LabTestTemplate(Document): def validate(self): if self.is_billable and (not self.lab_test_rate or self.lab_test_rate <= 0.0): - frappe.throw(_("Standard Selling Rate should be greater than zero.")) + frappe.throw(_('Standard Selling Rate should be greater than zero.')) + self.validate_conversion_factor() self.enable_disable_item() @@ -42,7 +43,9 @@ class LabTestTemplate(Document): # Remove template reference from item and disable item if self.item: try: - frappe.delete_doc('Item', self.item) + item = self.item + self.db_set('item', '') + frappe.delete_doc('Item', item) except Exception: frappe.throw(_('Not permitted. Please disable the Lab Test Template')) @@ -63,26 +66,26 @@ class LabTestTemplate(Document): 'standard_rate': self.lab_test_rate, 'description': self.lab_test_description }) - item.save() + item.flags.ignore_mandatory = True + item.save(ignore_permissions=True) def item_price_exists(self): item_price = frappe.db.exists({'doctype': 'Item Price', 'item_code': self.lab_test_code}) if item_price: return item_price[0][0] - else: - return False + return False def validate_conversion_factor(self): - if self.lab_test_template_type == "Single" and self.secondary_uom and not self.conversion_factor: - frappe.throw(_("Conversion Factor is mandatory")) - if self.lab_test_template_type == "Compound": + if self.lab_test_template_type == 'Single' and self.secondary_uom and not self.conversion_factor: + frappe.throw(_('Conversion Factor is mandatory')) + if self.lab_test_template_type == 'Compound': for item in self.normal_test_templates: if item.secondary_uom and not item.conversion_factor: - frappe.throw(_("Conversion Factor is mandatory")) - if self.lab_test_template_type == "Grouped": + frappe.throw(_('Row #{0}: Conversion Factor is mandatory').format(item.idx)) + if self.lab_test_template_type == 'Grouped': for group in self.lab_test_groups: - if group.template_or_new_line == "Add New Line" and group.secondary_uom and not group.conversion_factor: - frappe.throw(_("Conversion Factor is mandatory")) + if group.template_or_new_line == 'Add New Line' and group.secondary_uom and not group.conversion_factor: + frappe.throw(_('Row #{0}: Conversion Factor is mandatory').format(group.idx)) def create_item_from_template(doc): @@ -101,9 +104,9 @@ def create_item_from_template(doc): 'include_item_in_manufacturing': 0, 'show_in_website': 0, 'is_pro_applicable': 0, - 'disabled': 0 if doc.is_billable and not doc.disabled else doc.disabled, + 'disabled': 0 if doc.is_billable and not doc.disabled else doc.disabled, 'stock_uom': uom - }).insert(ignore_permissions = True, ignore_mandatory = True) + }).insert(ignore_permissions=True, ignore_mandatory=True) # Insert item price if doc.is_billable and doc.lab_test_rate != 0.0: @@ -123,7 +126,7 @@ def make_item_price(item, price_list_name, item_price): 'price_list': price_list_name, 'item_code': item, 'price_list_rate': item_price - }).insert(ignore_permissions = True, ignore_mandatory = True) + }).insert(ignore_permissions=True, ignore_mandatory=True) @frappe.whitelist() def change_test_code_from_template(lab_test_code, doc): @@ -132,8 +135,8 @@ def change_test_code_from_template(lab_test_code, doc): if frappe.db.exists({'doctype': 'Item', 'item_code': lab_test_code}): frappe.throw(_('Lab Test Item {0} already exist').format(lab_test_code)) else: - rename_doc('Item', doc.name, lab_test_code, ignore_permissions = True) + rename_doc('Item', doc.name, lab_test_code, ignore_permissions=True) frappe.db.set_value('Lab Test Template', doc.name, 'lab_test_code', lab_test_code) frappe.db.set_value('Lab Test Template', doc.name, 'lab_test_name', lab_test_code) - rename_doc('Lab Test Template', doc.name, lab_test_code, ignore_permissions = True) + rename_doc('Lab Test Template', doc.name, lab_test_code, ignore_permissions=True) return lab_test_code diff --git a/erpnext/healthcare/doctype/lab_test_template/lab_test_template_dashboard.py b/erpnext/healthcare/doctype/lab_test_template/lab_test_template_dashboard.py new file mode 100644 index 0000000000..94dfeea7a4 --- /dev/null +++ b/erpnext/healthcare/doctype/lab_test_template/lab_test_template_dashboard.py @@ -0,0 +1,13 @@ +from __future__ import unicode_literals +from frappe import _ + +def get_data(): + return { + 'fieldname': 'template', + 'transactions': [ + { + 'label': _('Lab Tests'), + 'items': ['Lab Test'] + } + ] + } diff --git a/erpnext/healthcare/doctype/lab_test_template/lab_test_template_list.js b/erpnext/healthcare/doctype/lab_test_template/lab_test_template_list.js index a3417ebdfc..08fc2cddda 100644 --- a/erpnext/healthcare/doctype/lab_test_template/lab_test_template_list.js +++ b/erpnext/healthcare/doctype/lab_test_template/lab_test_template_list.js @@ -3,5 +3,5 @@ */ frappe.listview_settings['Lab Test Template'] = { add_fields: ['lab_test_name', 'lab_test_code', 'lab_test_rate'], - filters: [['disabled', '=', 0]] + filters: [['disabled', '=', 'No']] }; diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js index f7ed31bfea..2d6b64532b 100644 --- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js +++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js @@ -226,7 +226,9 @@ let check_and_set_availability = function(frm) { primary_action_label: __('Book'), primary_action: function() { frm.set_value('appointment_time', selected_slot); - frm.set_value('duration', duration); + if (!frm.doc.duration) { + frm.set_value('duration', duration); + } frm.set_value('practitioner', d.get_value('practitioner')); frm.set_value('department', d.get_value('department')); frm.set_value('appointment_date', d.get_value('appointment_date')); diff --git a/erpnext/healthcare/doctype/patient_medical_record/test_patient_medical_record.py b/erpnext/healthcare/doctype/patient_medical_record/test_patient_medical_record.py index e5a5e4c010..aa85a23113 100644 --- a/erpnext/healthcare/doctype/patient_medical_record/test_patient_medical_record.py +++ b/erpnext/healthcare/doctype/patient_medical_record/test_patient_medical_record.py @@ -34,7 +34,7 @@ class TestPatientMedicalRecord(unittest.TestCase): self.assertTrue(medical_rec) template = create_lab_test_template(medical_department) - lab_test = create_lab_test(template, patient) + lab_test = create_lab_test(template.name, patient) # check for lab test medical_rec = frappe.db.exists('Patient Medical Record', {'status': 'Open', 'reference_name': lab_test.name}) self.assertTrue(medical_rec) @@ -66,7 +66,7 @@ def create_vital_signs(appointment): def create_lab_test_template(medical_department): if frappe.db.exists('Lab Test Template', 'Blood Test'): - return 'Blood Test' + return frappe.get_doc('Lab Test Template', 'Blood Test') template = frappe.new_doc('Lab Test Template') template.lab_test_name = 'Blood Test' @@ -76,7 +76,7 @@ def create_lab_test_template(medical_department): template.is_billable = 1 template.lab_test_rate = 2000 template.save() - return template.name + return template def create_lab_test(template, patient): lab_test = frappe.new_doc('Lab Test') diff --git a/erpnext/healthcare/doctype/sample_collection/sample_collection.js b/erpnext/healthcare/doctype/sample_collection/sample_collection.js index 2f5278b2d5..0390391235 100644 --- a/erpnext/healthcare/doctype/sample_collection/sample_collection.js +++ b/erpnext/healthcare/doctype/sample_collection/sample_collection.js @@ -3,29 +3,29 @@ frappe.ui.form.on('Sample Collection', { refresh: function(frm) { - if(frappe.defaults.get_default("create_sample_collection_for_lab_test")){ - frm.add_custom_button(__("View Lab Tests"), function() { - frappe.route_options = {"sample": frm.doc.name}; - frappe.set_route("List", "Lab Test"); + if (frappe.defaults.get_default('create_sample_collection_for_lab_test')) { + frm.add_custom_button(__('View Lab Tests'), function() { + frappe.route_options = {'sample': frm.doc.name}; + frappe.set_route('List', 'Lab Test'); }); } } }); -frappe.ui.form.on("Sample Collection", "patient", function(frm) { +frappe.ui.form.on('Sample Collection', 'patient', function(frm) { if(frm.doc.patient){ frappe.call({ - "method": "erpnext.healthcare.doctype.patient.patient.get_patient_detail", + 'method': 'erpnext.healthcare.doctype.patient.patient.get_patient_detail', args: { patient: frm.doc.patient }, callback: function (data) { var age = null; - if(data.message.dob){ + if (data.message.dob){ age = calculate_age(data.message.dob); } - frappe.model.set_value(frm.doctype,frm.docname, "patient_age", age); - frappe.model.set_value(frm.doctype,frm.docname, "patient_sex", data.message.sex); + frappe.model.set_value(frm.doctype,frm.docname, 'patient_age', age); + frappe.model.set_value(frm.doctype,frm.docname, 'patient_sex', data.message.sex); } }); } @@ -36,5 +36,5 @@ var calculate_age = function(birth) { var age = new Date(); age.setTime(ageMS); var years = age.getFullYear() - 1970; - return years + " Year(s) " + age.getMonth() + " Month(s) " + age.getDate() + " Day(s)"; + return years + ' Year(s) ' + age.getMonth() + ' Month(s) ' + age.getDate() + ' Day(s)'; }; diff --git a/erpnext/healthcare/doctype/sample_collection/sample_collection.json b/erpnext/healthcare/doctype/sample_collection/sample_collection.json index 016cfbc3ae..83383e3445 100644 --- a/erpnext/healthcare/doctype/sample_collection/sample_collection.json +++ b/erpnext/healthcare/doctype/sample_collection/sample_collection.json @@ -9,8 +9,10 @@ "document_type": "Document", "engine": "InnoDB", "field_order": [ + "patient_details_section", "naming_series", "patient", + "patient_name", "patient_age", "patient_sex", "column_break_4", @@ -25,15 +27,17 @@ "collected_by", "collected_time", "num_print", - "amended_from", "section_break_15", - "sample_details" + "sample_details", + "amended_from" ], "fields": [ { "fetch_from": "patient.inpatient_record", "fieldname": "inpatient_record", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "label": "Inpatient Record", "options": "Inpatient Record", "read_only": 1 @@ -42,6 +46,8 @@ "bold": 1, "fieldname": "naming_series", "fieldtype": "Select", + "hide_days": 1, + "hide_seconds": 1, "label": "Series", "no_copy": 1, "options": "HLC-SC-.YYYY.-", @@ -52,6 +58,8 @@ "default": "0", "fieldname": "invoiced", "fieldtype": "Check", + "hide_days": 1, + "hide_seconds": 1, "label": "Invoiced", "no_copy": 1, "read_only": 1, @@ -61,41 +69,60 @@ "fetch_from": "inpatient_record.patient", "fieldname": "patient", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "ignore_user_permissions": 1, "in_standard_filter": 1, "label": "Patient", "options": "Patient", + "reqd": 1, "search_index": 1 }, { "fieldname": "column_break_4", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "hide_days": 1, + "hide_seconds": 1 }, { "fieldname": "patient_age", "fieldtype": "Data", - "label": "Age" + "hide_days": 1, + "hide_seconds": 1, + "label": "Age", + "read_only": 1 }, { "fetch_from": "patient.sex", "fieldname": "patient_sex", - "fieldtype": "Data", - "label": "Gender" + "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, + "label": "Gender", + "options": "Gender", + "read_only": 1 }, { "fieldname": "company", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "in_standard_filter": 1, "label": "Company", "options": "Company" }, { "fieldname": "section_break_6", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "hide_days": 1, + "hide_seconds": 1, + "label": "Sample Details" }, { "fieldname": "sample", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "ignore_user_permissions": 1, "in_list_view": 1, "in_standard_filter": 1, @@ -108,16 +135,23 @@ "fetch_from": "sample.sample_uom", "fieldname": "sample_uom", "fieldtype": "Data", + "hide_days": 1, + "hide_seconds": 1, "in_list_view": 1, - "label": "UOM" + "label": "UOM", + "read_only": 1 }, { "fieldname": "column_break_10", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "hide_days": 1, + "hide_seconds": 1 }, { "fieldname": "collected_by", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "ignore_user_permissions": 1, "label": "Collected By", "options": "User" @@ -125,20 +159,27 @@ { "fieldname": "collected_time", "fieldtype": "Datetime", - "label": "Collected Time" + "hide_days": 1, + "hide_seconds": 1, + "label": "Collected On" }, { "allow_on_submit": 1, "default": "1", + "description": "Number of prints required for labelling the samples", "fieldname": "num_print", "fieldtype": "Int", - "label": "No. of print", + "hide_days": 1, + "hide_seconds": 1, + "label": "No. of prints", "print_hide": 1, "report_hide": 1 }, { "fieldname": "amended_from", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "label": "Amended From", "no_copy": 1, "options": "Sample Collection", @@ -147,25 +188,43 @@ }, { "fieldname": "section_break_15", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "hide_days": 1, + "hide_seconds": 1 }, { "default": "0", "fieldname": "sample_qty", "fieldtype": "Float", + "hide_days": 1, + "hide_seconds": 1, "in_list_view": 1, "label": "Quantity" }, { "fieldname": "sample_details", "fieldtype": "Long Text", + "hide_days": 1, + "hide_seconds": 1, "ignore_xss_filter": 1, "label": "Collection Details" + }, + { + "fieldname": "patient_details_section", + "fieldtype": "Section Break", + "label": "Patient Details" + }, + { + "fetch_from": "patient.patient_name", + "fieldname": "patient_name", + "fieldtype": "Data", + "label": "Patient Name", + "read_only": 1 } ], "is_submittable": 1, "links": [], - "modified": "2020-05-25 14:36:46.990469", + "modified": "2020-07-30 16:53:13.076104", "modified_by": "Administrator", "module": "Healthcare", "name": "Sample Collection", diff --git a/erpnext/healthcare/doctype/sample_collection/sample_collection.py b/erpnext/healthcare/doctype/sample_collection/sample_collection.py index 2c64320fac..461f809507 100644 --- a/erpnext/healthcare/doctype/sample_collection/sample_collection.py +++ b/erpnext/healthcare/doctype/sample_collection/sample_collection.py @@ -3,7 +3,12 @@ # For license information, please see license.txt from __future__ import unicode_literals +import frappe from frappe.model.document import Document +from frappe.utils import flt +from frappe import _ class SampleCollection(Document): - pass + def validate(self): + if flt(self.sample_qty) <= 0: + frappe.throw(_('Sample Quantity cannot be negative or 0'), title=_('Invalid Quantity')) diff --git a/erpnext/healthcare/report/lab_test_report/lab_test_report.js b/erpnext/healthcare/report/lab_test_report/lab_test_report.js index 3128f819bb..7754e2e196 100644 --- a/erpnext/healthcare/report/lab_test_report/lab_test_report.js +++ b/erpnext/healthcare/report/lab_test_report/lab_test_report.js @@ -4,29 +4,54 @@ frappe.query_reports["Lab Test Report"] = { "filters": [ { - "fieldname":"from_date", + "fieldname": "from_date", "label": __("From Date"), "fieldtype": "Date", - "default": frappe.datetime.now_date(), - "width": "80" + "default": frappe.datetime.add_months(frappe.datetime.get_today(), -1), + "reqd": 1 }, { - "fieldname":"to_date", + "fieldname": "to_date", "label": __("To Date"), "fieldtype": "Date", - "default": frappe.datetime.now_date() + "default": frappe.datetime.now_date(), + "reqd": 1 }, { - "fieldname":"patient", + "fieldname": "company", + "label": __("Company"), + "fieldtype": "Link", + "default": frappe.defaults.get_default("Company"), + "options": "Company" + }, + { + "fieldname": "template", + "label": __("Lab Test Template"), + "fieldtype": "Link", + "options": "Lab Test Template" + }, + { + "fieldname": "patient", "label": __("Patient"), "fieldtype": "Link", "options": "Patient" }, { - "fieldname":"department", + "fieldname": "department", "label": __("Medical Department"), "fieldtype": "Link", "options": "Medical Department" + }, + { + "fieldname": "status", + "label": __("Status"), + "fieldtype": "Select", + "options": "\nCompleted\nApproved\nRejected" + }, + { + "fieldname": "invoiced", + "label": __("Invoiced"), + "fieldtype": "Check" } ] }; diff --git a/erpnext/healthcare/report/lab_test_report/lab_test_report.json b/erpnext/healthcare/report/lab_test_report/lab_test_report.json index 30e5a5fd56..aeb42897b8 100644 --- a/erpnext/healthcare/report/lab_test_report/lab_test_report.json +++ b/erpnext/healthcare/report/lab_test_report/lab_test_report.json @@ -1,30 +1,31 @@ { - "add_total_row": 1, - "creation": "2013-04-23 18:15:29", - "disabled": 0, - "docstatus": 0, - "doctype": "Report", - "idx": 1, - "is_standard": "Yes", - "modified": "2018-08-06 11:41:50.218737", - "modified_by": "Administrator", - "module": "Healthcare", - "name": "Lab Test Report", - "owner": "Administrator", - "prepared_report": 0, - "ref_doctype": "Lab Test", - "report_name": "Lab Test Report", - "report_type": "Script Report", + "add_total_row": 0, + "creation": "2013-04-23 18:15:29", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 1, + "is_standard": "Yes", + "modified": "2020-07-30 18:53:20.102873", + "modified_by": "Administrator", + "module": "Healthcare", + "name": "Lab Test Report", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Lab Test", + "report_name": "Lab Test Report", + "report_type": "Script Report", "roles": [ { "role": "Laboratory User" - }, + }, { "role": "Nursing User" - }, + }, { "role": "LabTest Approver" - }, + }, { "role": "Healthcare Administrator" } diff --git a/erpnext/healthcare/report/lab_test_report/lab_test_report.py b/erpnext/healthcare/report/lab_test_report/lab_test_report.py index 17f25fa7a7..2e59bed037 100644 --- a/erpnext/healthcare/report/lab_test_report/lab_test_report.py +++ b/erpnext/healthcare/report/lab_test_report/lab_test_report.py @@ -8,51 +8,204 @@ from frappe import msgprint, _ def execute(filters=None): if not filters: filters = {} - lab_test_list = get_lab_test(filters) + data, columns = [], [] + columns = get_columns() + lab_test_list = get_lab_tests(filters) if not lab_test_list: - msgprint(_("No record found")) + msgprint(_('No records found')) return columns, lab_test_list data = [] for lab_test in lab_test_list: - row = [ lab_test.lab_test_name, lab_test.patient, lab_test.practitioner, lab_test.invoiced, lab_test.status, lab_test.result_date, lab_test.department] + row = frappe._dict({ + 'test': lab_test.name, + 'template': lab_test.template, + 'company': lab_test.company, + 'patient': lab_test.patient, + 'patient_name': lab_test.patient_name, + 'practitioner': lab_test.practitioner, + 'employee': lab_test.employee, + 'status': lab_test.status, + 'invoiced': lab_test.invoiced, + 'result_date': lab_test.result_date, + 'department': lab_test.department + }) data.append(row) - return columns, data + chart = get_chart_data(data) + report_summary = get_report_summary(data) + return columns, data, None, chart, report_summary def get_columns(): - columns = [ - _("Test") + ":Data:120", - _("Patient") + ":Link/Patient:180", - _("Healthcare Practitioner") + ":Link/Healthcare Practitioner:120", - _("Invoiced") + ":Check:100", - _("Status") + ":Data:120", - _("Result Date") + ":Date:120", - _("Department") + ":Data:120", + return [ + { + 'fieldname': 'test', + 'label': _('Lab Test'), + 'fieldtype': 'Link', + 'options': 'Lab Test', + 'width': '120' + }, + { + 'fieldname': 'template', + 'label': _('Lab Test Template'), + 'fieldtype': 'Link', + 'options': 'Lab Test Template', + 'width': '120' + }, + { + 'fieldname': 'company', + 'label': _('Company'), + 'fieldtype': 'Link', + 'options': 'Company', + 'width': '120' + }, + { + 'fieldname': 'patient', + 'label': _('Patient'), + 'fieldtype': 'Link', + 'options': 'Patient', + 'width': '120' + }, + { + 'fieldname': 'patient_name', + 'label': _('Patient Name'), + 'fieldtype': 'Data', + 'width': '120' + }, + { + 'fieldname': 'employee', + 'label': _('Lab Technician'), + 'fieldtype': 'Link', + 'options': 'Employee', + 'width': '120' + }, + { + 'fieldname': 'status', + 'label': _('Status'), + 'fieldtype': 'Data', + 'width': '100' + }, + { + 'fieldname': 'invoiced', + 'label': _('Invoiced'), + 'fieldtype': 'Check', + 'width': '100' + }, + { + 'fieldname': 'result_date', + 'label': _('Result Date'), + 'fieldtype': 'Date', + 'width': '100' + }, + { + 'fieldname': 'practitioner', + 'label': _('Requesting Practitioner'), + 'fieldtype': 'Link', + 'options': 'Healthcare Practitioner', + 'width': '120' + }, + { + 'fieldname': 'department', + 'label': _('Medical Department'), + 'fieldtype': 'Link', + 'options': 'Medical Department', + 'width': '100' + } ] - return columns +def get_lab_tests(filters): + conditions = get_conditions(filters) + data = frappe.get_all( + doctype='Lab Test', + fields=['name', 'template', 'company', 'patient', 'patient_name', 'practitioner', 'employee', 'status', 'invoiced', 'result_date', 'department'], + filters=conditions, + order_by='submitted_date desc' + ) + return data def get_conditions(filters): - conditions = "" + conditions = { + 'docstatus': ('=', 1) + } - if filters.get("patient"): - conditions += "and patient = %(patient)s" - if filters.get("from_date"): - conditions += "and result_date >= %(from_date)s" - if filters.get("to_date"): - conditions += " and result_date <= %(to_date)s" - if filters.get("department"): - conditions += " and department = %(department)s" + if filters.get('from_date') and filters.get('to_date'): + conditions['result_date'] = ('between', (filters.get('from_date'), filters.get('to_date'))) + filters.pop('from_date') + filters.pop('to_date') + + for key, value in filters.items(): + if filters.get(key): + conditions[key] = value return conditions -def get_lab_test(filters): - conditions = get_conditions(filters) - return frappe.db.sql("""select name, patient, lab_test_name, patient_name, status, result_date, practitioner, invoiced, department - from `tabLab Test` - where docstatus<2 %s order by submitted_date desc, name desc""" % - conditions, filters, as_dict=1) +def get_chart_data(data): + if not data: + return None + + labels = ['Completed', 'Approved', 'Rejected'] + + status_wise_data = { + 'Completed': 0, + 'Approved': 0, + 'Rejected': 0 + } + + datasets = [] + + for entry in data: + status_wise_data[entry.status] += 1 + + datasets.append({ + 'name': 'Lab Test Status', + 'values': [status_wise_data.get('Completed'), status_wise_data.get('Approved'), status_wise_data.get('Rejected')] + }) + + chart = { + 'data': { + 'labels': labels, + 'datasets': datasets + }, + 'type': 'donut', + 'height': 300, + } + + return chart + + +def get_report_summary(data): + if not data: + return None + + total_lab_tests = len(data) + invoiced_lab_tests, unbilled_lab_tests = 0, 0 + + for entry in data: + if entry.invoiced: + invoiced_lab_tests += 1 + else: + unbilled_lab_tests += 1 + + return [ + { + 'value': total_lab_tests, + 'indicator': 'Blue', + 'label': 'Total Lab Tests', + 'datatype': 'Int', + }, + { + 'value': invoiced_lab_tests, + 'indicator': 'Green', + 'label': 'Invoiced Lab Tests', + 'datatype': 'Int', + }, + { + 'value': unbilled_lab_tests, + 'indicator': 'Red', + 'label': 'Unbilled Lab Tests', + 'datatype': 'Int', + } + ] diff --git a/erpnext/hr/doctype/attendance/attendance.py b/erpnext/hr/doctype/attendance/attendance.py index 45b7060610..373b94008e 100644 --- a/erpnext/hr/doctype/attendance/attendance.py +++ b/erpnext/hr/doctype/attendance/attendance.py @@ -98,7 +98,8 @@ def add_attendance(events, start, end, conditions=None): e = { "name": d.name, "doctype": "Attendance", - "date": d.attendance_date, + "start": d.attendance_date, + "end": d.attendance_date, "title": cstr(d.status), "docstatus": d.docstatus } diff --git a/erpnext/hr/doctype/attendance/attendance_calendar.js b/erpnext/hr/doctype/attendance/attendance_calendar.js index 104f09d69f..4566489696 100644 --- a/erpnext/hr/doctype/attendance/attendance_calendar.js +++ b/erpnext/hr/doctype/attendance/attendance_calendar.js @@ -1,12 +1,6 @@ // Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt frappe.views.calendar["Attendance"] = { - field_map: { - "start": "attendance_date", - "end": "attendance_date", - "id": "name", - "docstatus": 1 - }, options: { header: { left: 'prev,next today', diff --git a/erpnext/hr/doctype/job_offer/job_offer.py b/erpnext/hr/doctype/job_offer/job_offer.py index 3d68bc8d8e..c397a3f5ca 100644 --- a/erpnext/hr/doctype/job_offer/job_offer.py +++ b/erpnext/hr/doctype/job_offer/job_offer.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals import frappe +from frappe.utils import cint from frappe.model.document import Document from frappe.model.mapper import get_mapped_doc from frappe import _ @@ -24,8 +25,7 @@ class JobOffer(Document): check_vacancies = frappe.get_single("HR Settings").check_vacancies if staffing_plan and check_vacancies: job_offers = self.get_job_offer(staffing_plan.from_date, staffing_plan.to_date) - - if not staffing_plan.get("vacancies") or staffing_plan.vacancies - len(job_offers) <= 0: + if not staffing_plan.get("vacancies") or cint(staffing_plan.vacancies) - len(job_offers) <= 0: error_variable = 'for ' + frappe.bold(self.designation) if staffing_plan.get("parent"): error_variable = frappe.bold(get_link_to_form("Staffing Plan", staffing_plan.parent)) @@ -65,7 +65,7 @@ def get_staffing_plan_detail(designation, company, offer_date): AND %s between sp.from_date and sp.to_date """, (designation, company, offer_date), as_dict=1) - return frappe._dict(detail[0]) if detail else None + return frappe._dict(detail[0]) if (detail and detail[0].parent) else None @frappe.whitelist() def make_employee(source_name, target_doc=None): diff --git a/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.json b/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.json index a5ac3f3d47..4abba5f2d4 100644 --- a/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.json +++ b/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.json @@ -1,5 +1,4 @@ { - "actions": [], "creation": "2019-05-09 15:47:39.760406", "doctype": "DocType", "engine": "InnoDB", @@ -54,6 +53,7 @@ { "fieldname": "transaction_type", "fieldtype": "Link", + "in_standard_filter": 1, "label": "Transaction Type", "options": "DocType" }, @@ -109,9 +109,9 @@ } ], "in_create": 1, + "index_web_pages_for_search": 1, "is_submittable": 1, - "links": [], - "modified": "2020-02-27 14:40:10.502605", + "modified": "2020-09-04 12:16:36.569066", "modified_by": "Administrator", "module": "HR", "name": "Leave Ledger Entry", diff --git a/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry_list.js b/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry_list.js new file mode 100644 index 0000000000..889325bf2b --- /dev/null +++ b/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry_list.js @@ -0,0 +1,13 @@ +frappe.listview_settings['Leave Ledger Entry'] = { + onload: function(listview) { + if(listview.page.fields_dict.transaction_type) { + listview.page.fields_dict.transaction_type.get_query = function() { + return { + "filters": { + "name": ["in", ["Leave Allocation", "Leave Application", "Leave Encashment"]], + } + }; + }; + } + } +}; diff --git a/erpnext/hr/doctype/shift_assignment/shift_assignment.py b/erpnext/hr/doctype/shift_assignment/shift_assignment.py index f8b73349c1..2c385e80f4 100644 --- a/erpnext/hr/doctype/shift_assignment/shift_assignment.py +++ b/erpnext/hr/doctype/shift_assignment/shift_assignment.py @@ -103,7 +103,7 @@ def add_assignments(events, start, end, conditions=None): "doctype": "Shift Assignment", "start_date": d.start_date, "end_date": d.end_date if d.end_date else nowdate(), - "title": cstr(d.employee_name) + \ + "title": cstr(d.employee_name) + ": "+ \ cstr(d.shift_type), "docstatus": d.docstatus } diff --git a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py index db1d191758..1b92358184 100644 --- a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py +++ b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py @@ -132,6 +132,9 @@ def get_conditions(filters): if filters.get('employee'): conditions['name'] = filters.get('employee') + if filters.get('company'): + conditions['company'] = filters.get('company') + return conditions def get_department_leave_approver_map(department=None): diff --git a/erpnext/loan_management/doctype/loan/loan.js b/erpnext/loan_management/doctype/loan/loan.js index ffef60b6b0..9b4c21770e 100644 --- a/erpnext/loan_management/doctype/loan/loan.js +++ b/erpnext/loan_management/doctype/loan/loan.js @@ -73,8 +73,8 @@ frappe.ui.form.on('Loan', { loan_type: function(frm) { frm.toggle_reqd("repayment_method", frm.doc.is_term_loan); - frm.toggle_display("repayment_method", 1 - frm.doc.is_term_loan); - frm.toggle_display("repayment_periods", s1 - frm.doc.is_term_loan); + frm.toggle_display("repayment_method", frm.doc.is_term_loan); + frm.toggle_display("repayment_periods", frm.doc.is_term_loan); }, @@ -119,12 +119,10 @@ frappe.ui.form.on('Loan', { create_loan_security_unpledge: function(frm) { frappe.call({ - method: "erpnext.loan_management.doctype.loan.loan.create_loan_security_unpledge", + method: "erpnext.loan_management.doctype.loan.loan.unpledge_security", args : { "loan": frm.doc.name, - "applicant_type": frm.doc.applicant_type, - "applicant": frm.doc.applicant, - "company": frm.doc.company + "as_dict": 1 }, callback: function(r) { if (r.message) diff --git a/erpnext/loan_management/doctype/loan/loan.py b/erpnext/loan_management/doctype/loan/loan.py index e20b484fc0..d1b7589a17 100644 --- a/erpnext/loan_management/doctype/loan/loan.py +++ b/erpnext/loan_management/doctype/loan/loan.py @@ -7,7 +7,7 @@ import frappe, math, json import erpnext from frappe import _ from frappe.utils import flt, rounded, add_months, nowdate, getdate, now_datetime - +from erpnext.loan_management.doctype.loan_security_unpledge.loan_security_unpledge import get_pledged_security_qty from erpnext.controllers.accounts_controller import AccountsController class Loan(AccountsController): @@ -223,30 +223,56 @@ def make_repayment_entry(loan, applicant_type, applicant, loan_type, company, as return repayment_entry @frappe.whitelist() -def create_loan_security_unpledge(loan, applicant_type, applicant, company, as_dict=1): - loan_security_pledge_details = frappe.db.sql(""" - SELECT p.loan_security, sum(p.qty) as qty - FROM `tabLoan Security Pledge` lsp , `tabPledge` p - WHERE p.parent = lsp.name AND lsp.loan = %s AND lsp.docstatus = 1 - GROUP BY p.loan_security - """,(loan), as_dict=1) +def unpledge_security(loan=None, loan_security_pledge=None, as_dict=0, save=0, submit=0, approve=0): + # if loan is passed it will be considered as full unpledge + if loan: + pledge_qty_map = get_pledged_security_qty(loan) + loan_doc = frappe.get_doc('Loan', loan) + unpledge_request = create_loan_security_unpledge(pledge_qty_map, loan_doc.name, loan_doc.company, + loan_doc.applicant_type, loan_doc.applicant) + # will unpledge qty based on loan security pledge + elif loan_security_pledge: + security_map = {} + pledge_doc = frappe.get_doc('Loan Security Pledge', loan_security_pledge) + for security in pledge_doc.securities: + security_map.setdefault(security.loan_security, security.qty) + unpledge_request = create_loan_security_unpledge(security_map, pledge_doc.loan, + pledge_doc.company, pledge_doc.applicant_type, pledge_doc.applicant) + + if save: + unpledge_request.save() + + if submit: + unpledge_request.submit() + + if approve: + if unpledge_request.docstatus == 1: + unpledge_request.status = 'Approved' + unpledge_request.save() + else: + frappe.throw(_('Only submittted unpledge requests can be approved')) + + if as_dict: + return unpledge_request + else: + return unpledge_request + +def create_loan_security_unpledge(unpledge_map, loan, company, applicant_type, applicant): unpledge_request = frappe.new_doc("Loan Security Unpledge") unpledge_request.applicant_type = applicant_type unpledge_request.applicant = applicant unpledge_request.loan = loan unpledge_request.company = company - for loan_security in loan_security_pledge_details: - unpledge_request.append('securities', { - "loan_security": loan_security.loan_security, - "qty": loan_security.qty - }) + for security, qty in unpledge_map.items(): + if qty: + unpledge_request.append('securities', { + "loan_security": security, + "qty": qty + }) - if as_dict: - return unpledge_request.as_dict() - else: - return unpledge_request + return unpledge_request diff --git a/erpnext/loan_management/doctype/loan/test_loan.py b/erpnext/loan_management/doctype/loan/test_loan.py index 23815d5982..5a4a19a0fb 100644 --- a/erpnext/loan_management/doctype/loan/test_loan.py +++ b/erpnext/loan_management/doctype/loan/test_loan.py @@ -14,9 +14,11 @@ from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_ process_loan_interest_accrual_for_term_loans) from erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual import days_in_year from erpnext.loan_management.doctype.process_loan_security_shortfall.process_loan_security_shortfall import create_process_loan_security_shortfall -from erpnext.loan_management.doctype.loan.loan import create_loan_security_unpledge +from erpnext.loan_management.doctype.loan.loan import unpledge_security from erpnext.loan_management.doctype.loan_security_unpledge.loan_security_unpledge import get_pledged_security_qty from erpnext.loan_management.doctype.loan_application.loan_application import create_pledge +from erpnext.loan_management.doctype.loan_disbursement.loan_disbursement import get_disbursal_amount +from erpnext.loan_management.doctype.loan_repayment.loan_repayment import calculate_amounts class TestLoan(unittest.TestCase): def setUp(self): @@ -193,18 +195,13 @@ class TestLoan(unittest.TestCase): make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date) process_loan_interest_accrual_for_demand_loans(posting_date = last_date) - repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5), + repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 6), "Loan Closure", flt(loan.loan_amount + accrued_interest_amount)) repayment_entry.submit() - amounts = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['paid_interest_amount', - 'paid_principal_amount']) + amount = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['sum(paid_interest_amount)']) - unaccrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * 6) \ - / (days_in_year(get_datetime(first_date).year) * 100) - - self.assertEquals(flt(amounts[0] + unaccrued_interest_amount, 3), - flt(accrued_interest_amount, 3)) + self.assertEquals(flt(amount, 2),flt(accrued_interest_amount, 2)) self.assertEquals(flt(repayment_entry.penalty_amount, 5), 0) loan.load_from_db() @@ -306,13 +303,10 @@ class TestLoan(unittest.TestCase): "Loan Closure", flt(loan.loan_amount + accrued_interest_amount)) repayment_entry.submit() - amounts = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['paid_interest_amount', - 'paid_principal_amount']) - loan.load_from_db() self.assertEquals(loan.status, "Loan Closure Requested") - unpledge_request = create_loan_security_unpledge(loan.name, loan.applicant_type, loan.applicant, loan.company, as_dict=0) + unpledge_request = unpledge_security(loan=loan.name, save=1) unpledge_request.submit() unpledge_request.status = 'Approved' unpledge_request.save() @@ -323,6 +317,102 @@ class TestLoan(unittest.TestCase): self.assertEqual(loan.status, 'Closed') self.assertEquals(sum(pledged_qty.values()), 0) + amounts = amounts = calculate_amounts(loan.name, add_days(last_date, 6), "Regular Repayment") + self.assertEqual(amounts['pending_principal_amount'], 0) + self.assertEqual(amounts['payable_principal_amount'], 0) + self.assertEqual(amounts['interest_amount'], 0) + + def test_disbursal_check_with_shortfall(self): + pledges = [{ + "loan_security": "Test Security 2", + "qty": 8000.00, + "haircut": 50, + }] + + loan_application = create_loan_application('_Test Company', self.applicant2, + 'Stock Loan', pledges, "Repay Over Number of Periods", 12) + + create_pledge(loan_application) + + loan = create_loan_with_security(self.applicant2, "Stock Loan", "Repay Over Number of Periods", 12, loan_application) + loan.submit() + + #Disbursing 7,00,000 from the allowed 10,00,000 according to security pledge + make_loan_disbursement_entry(loan.name, 700000) + + frappe.db.sql("""UPDATE `tabLoan Security Price` SET loan_security_price = 100 + where loan_security='Test Security 2'""") + + create_process_loan_security_shortfall() + loan_security_shortfall = frappe.get_doc("Loan Security Shortfall", {"loan": loan.name}) + self.assertTrue(loan_security_shortfall) + + self.assertEqual(get_disbursal_amount(loan.name), 0) + + frappe.db.sql(""" UPDATE `tabLoan Security Price` SET loan_security_price = 250 + where loan_security='Test Security 2'""") + + def test_disbursal_check_without_shortfall(self): + pledges = [{ + "loan_security": "Test Security 2", + "qty": 8000.00, + "haircut": 50, + }] + + loan_application = create_loan_application('_Test Company', self.applicant2, + 'Stock Loan', pledges, "Repay Over Number of Periods", 12) + + create_pledge(loan_application) + + loan = create_loan_with_security(self.applicant2, "Stock Loan", "Repay Over Number of Periods", 12, loan_application) + loan.submit() + + #Disbursing 7,00,000 from the allowed 10,00,000 according to security pledge + make_loan_disbursement_entry(loan.name, 700000) + + self.assertEqual(get_disbursal_amount(loan.name), 300000) + + def test_pending_loan_amount_after_closure_request(self): + pledge = [{ + "loan_security": "Test Security 1", + "qty": 4000.00 + }] + + loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge) + create_pledge(loan_application) + + loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date=get_first_day(nowdate())) + loan.submit() + + self.assertEquals(loan.loan_amount, 1000000) + + first_date = '2019-10-01' + last_date = '2019-10-30' + + no_of_days = date_diff(last_date, first_date) + 1 + + no_of_days += 6 + + accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \ + / (days_in_year(get_datetime(first_date).year) * 100) + + make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date) + process_loan_interest_accrual_for_demand_loans(posting_date = last_date) + + amounts = calculate_amounts(loan.name, add_days(last_date, 6), "Regular Repayment") + + repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 6), + "Loan Closure", flt(loan.loan_amount + accrued_interest_amount)) + repayment_entry.submit() + + amounts = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['paid_interest_amount', + 'paid_principal_amount']) + + loan.load_from_db() + self.assertEquals(loan.status, "Loan Closure Requested") + + amounts = calculate_amounts(loan.name, add_days(last_date, 6), "Regular Repayment") + self.assertEquals(amounts['pending_principal_amount'], 0.0) def create_loan_accounts(): if not frappe.db.exists("Account", "Loans and Advances (Assets) - _TC"): diff --git a/erpnext/loan_management/doctype/loan_application/loan_application.js b/erpnext/loan_management/doctype/loan_application/loan_application.js index b56fce1d7c..1365274971 100644 --- a/erpnext/loan_management/doctype/loan_application/loan_application.js +++ b/erpnext/loan_management/doctype/loan_application/loan_application.js @@ -33,18 +33,18 @@ frappe.ui.form.on('Loan Application', { if (frm.doc.is_secured_loan) { frappe.db.get_value("Loan Security Pledge", {"loan_application": frm.doc.name, "docstatus": 1}, "name", (r) => { - if (!r) { + if (Object.keys(r).length === 0) { frm.add_custom_button(__('Loan Security Pledge'), function() { - frm.trigger('create_loan_security_pledge') + frm.trigger('create_loan_security_pledge'); },__('Create')) } }); } frappe.db.get_value("Loan", {"loan_application": frm.doc.name, "docstatus": 1}, "name", (r) => { - if (!r) { + if (Object.keys(r).length === 0) { frm.add_custom_button(__('Loan'), function() { - frm.trigger('create_loan') + frm.trigger('create_loan'); },__('Create')) } else { frm.set_df_property('status', 'read_only', 1); @@ -54,7 +54,7 @@ frappe.ui.form.on('Loan Application', { }, create_loan: function(frm) { if (frm.doc.status != "Approved") { - frappe.throw(__("Cannot create loan until application is approved")) + frappe.throw(__("Cannot create loan until application is approved")); } frappe.model.open_mapped_doc({ diff --git a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py index 6c27e12134..260fada893 100644 --- a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py +++ b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py @@ -67,28 +67,10 @@ class LoanDisbursement(AccountsController): disbursed_amount = self.disbursed_amount + loan_details.disbursed_amount total_payment = loan_details.total_payment - if disbursed_amount > loan_details.loan_amount and loan_details.is_term_loan: - frappe.throw(_("Disbursed Amount cannot be greater than loan amount")) + possible_disbursal_amount = get_disbursal_amount(self.against_loan) - if loan_details.status == 'Disbursed': - pending_principal_amount = flt(loan_details.total_payment) - flt(loan_details.total_interest_payable) \ - - flt(loan_details.total_principal_paid) - else: - pending_principal_amount = loan_details.disbursed_amount - - security_value = 0.0 - if loan_details.is_secured_loan: - security_value = get_total_pledged_security_value(self.against_loan) - - if not security_value: - security_value = loan_details.loan_amount - - if pending_principal_amount + self.disbursed_amount > flt(security_value): - allowed_amount = security_value - pending_principal_amount - if allowed_amount < 0: - allowed_amount = 0 - - frappe.throw(_("Disbursed Amount cannot be greater than {0}").format(allowed_amount)) + if self.disbursed_amount > possible_disbursal_amount: + frappe.throw(_("Disbursed Amount cannot be greater than {0}").format(possible_disbursal_amount)) if loan_details.status == "Disbursed" and not loan_details.is_term_loan: process_loan_interest_accrual_for_demand_loans(posting_date=add_days(self.disbursement_date, -1), @@ -176,3 +158,32 @@ def get_total_pledged_security_value(loan): security_value += (loan_security_price_map.get(security) * qty * hair_cut_map.get(security))/100 return security_value + +@frappe.whitelist() +def get_disbursal_amount(loan): + loan_details = frappe.get_all("Loan", fields = ["loan_amount", "disbursed_amount", "total_payment", + "total_principal_paid", "total_interest_payable", "status", "is_term_loan", "is_secured_loan"], + filters= { "name": loan })[0] + + if loan_details.is_secured_loan and frappe.get_all('Loan Security Shortfall', filters={'loan': loan, + 'status': 'Pending'}): + return 0 + + if loan_details.status == 'Disbursed': + pending_principal_amount = flt(loan_details.total_payment) - flt(loan_details.total_interest_payable) \ + - flt(loan_details.total_principal_paid) + else: + pending_principal_amount = flt(loan_details.disbursed_amount) + + security_value = 0.0 + if loan_details.is_secured_loan: + security_value = get_total_pledged_security_value(loan) + + if not security_value and not loan_details.is_secured_loan: + security_value = flt(loan_details.loan_amount) + + disbursal_amount = flt(security_value) - flt(pending_principal_amount) + + return disbursal_amount + + diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py index c5111fdc93..2d959bf3be 100644 --- a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py +++ b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py @@ -85,8 +85,11 @@ def calculate_accrual_amount_for_demand_loans(loan, posting_date, process_loan_i if no_of_days <= 0: return - pending_principal_amount = flt(loan.total_payment) - flt(loan.total_interest_payable) \ - - flt(loan.total_principal_paid) + if loan.status == 'Disbursed': + pending_principal_amount = flt(loan.total_payment) - flt(loan.total_interest_payable) \ + - flt(loan.total_principal_paid) + else: + pending_principal_amount = loan.disbursed_amount interest_per_day = (pending_principal_amount * loan.rate_of_interest) / (days_in_year(get_datetime(posting_date).year) * 100) payable_interest = interest_per_day * no_of_days @@ -107,7 +110,7 @@ def calculate_accrual_amount_for_demand_loans(loan, posting_date, process_loan_i def make_accrual_interest_entry_for_demand_loans(posting_date, process_loan_interest, open_loans=None, loan_type=None): query_filters = { - "status": "Disbursed", + "status": ('in', ['Disbursed', 'Partially Disbursed']), "docstatus": 1 } @@ -118,8 +121,9 @@ def make_accrual_interest_entry_for_demand_loans(posting_date, process_loan_inte if not open_loans: open_loans = frappe.get_all("Loan", - fields=["name", "total_payment", "total_amount_paid", "loan_account", "interest_income_account", "is_term_loan", - "disbursement_date", "applicant_type", "applicant", "rate_of_interest", "total_interest_payable", "repayment_start_date"], + fields=["name", "total_payment", "total_amount_paid", "loan_account", "interest_income_account", + "is_term_loan", "status", "disbursement_date", "disbursed_amount", "applicant_type", "applicant", + "rate_of_interest", "total_interest_payable", "total_principal_paid", "repayment_start_date"], filters=query_filters) for loan in open_loans: @@ -209,7 +213,8 @@ def get_last_accural_date_in_current_month(loan): WHERE loan = %s""", (loan.name)) if last_posting_date[0][0]: - return last_posting_date[0][0] + # interest for last interest accrual date is already booked, so add 1 day + return add_days(last_posting_date[0][0], 1) else: return loan.disbursement_date diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index 9605045777..97dbc44bf1 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -13,6 +13,7 @@ from frappe.utils import date_diff, add_days, getdate, add_months, get_first_day from erpnext.controllers.accounts_controller import AccountsController from erpnext.accounts.general_ledger import make_gl_entries from erpnext.loan_management.doctype.loan_security_shortfall.loan_security_shortfall import update_shortfall_status +from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import process_loan_interest_accrual_for_demand_loans class LoanRepayment(AccountsController): @@ -22,6 +23,9 @@ class LoanRepayment(AccountsController): self.validate_amount() self.allocate_amounts(amounts['pending_accrual_entries']) + def before_submit(self): + self.book_unaccrued_interest() + def on_submit(self): self.update_paid_amount() self.make_gl_entries() @@ -72,6 +76,26 @@ class LoanRepayment(AccountsController): msg = _("Amount of {0} is required for Loan closure").format(self.payable_amount) frappe.throw(msg) + def book_unaccrued_interest(self): + if self.payment_type == 'Loan Closure': + total_interest_paid = 0 + for payment in self.repayment_details: + total_interest_paid += payment.paid_interest_amount + + if total_interest_paid < self.interest_payable: + if not self.is_term_loan: + process = process_loan_interest_accrual_for_demand_loans(posting_date=self.posting_date, + loan=self.against_loan) + + lia = frappe.db.get_value('Loan Interest Accrual', {'process_loan_interest_accrual': + process}, ['name', 'interest_amount', 'payable_principal_amount'], as_dict=1) + + self.append('repayment_details', { + 'loan_interest_accrual': lia.name, + 'paid_interest_amount': lia.interest_amount, + 'paid_principal_amount': lia.payable_principal_amount + }) + def update_paid_amount(self): precision = cint(frappe.db.get_default("currency_precision")) or 2 @@ -116,6 +140,7 @@ class LoanRepayment(AccountsController): def allocate_amounts(self, paid_entries): self.set('repayment_details', []) self.principal_amount_paid = 0 + total_interest_paid = 0 interest_paid = self.amount_paid - self.penalty_amount if self.amount_paid - self.penalty_amount > 0 and paid_entries: @@ -137,12 +162,17 @@ class LoanRepayment(AccountsController): interest_paid = 0 paid_principal=0 + total_interest_paid += interest_amount self.append('repayment_details', { 'loan_interest_accrual': lia, 'paid_interest_amount': interest_amount, 'paid_principal_amount': paid_principal }) + if self.payment_type == 'Loan Closure' and total_interest_paid < self.interest_payable: + unaccrued_interest = self.interest_payable - total_interest_paid + interest_paid -= unaccrued_interest + if interest_paid: self.principal_amount_paid += interest_paid @@ -281,7 +311,7 @@ def get_amounts(amounts, against_loan, posting_date, payment_type): due_date = add_days(entry.posting_date, 1) no_of_late_days = date_diff(posting_date, - add_days(due_date, loan_type_details.grace_period_in_days)) + add_days(due_date, loan_type_details.grace_period_in_days)) if no_of_late_days > 0 and (not against_loan_doc.repay_from_salary): penalty_amount += (entry.interest_amount * (loan_type_details.penalty_interest_rate / 100) * no_of_late_days)/365 @@ -297,7 +327,10 @@ def get_amounts(amounts, against_loan, posting_date, payment_type): if not final_due_date: final_due_date = add_days(due_date, loan_type_details.grace_period_in_days) - pending_principal_amount = against_loan_doc.total_payment - against_loan_doc.total_principal_paid - against_loan_doc.total_interest_payable + if against_loan_doc.status in ('Disbursed', 'Loan Closure Requested', 'Closed'): + pending_principal_amount = against_loan_doc.total_payment - against_loan_doc.total_principal_paid - against_loan_doc.total_interest_payable + else: + pending_principal_amount = against_loan_doc.disbursed_amount if payment_type == "Loan Closure": if due_date: diff --git a/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.json b/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.json index 4572e99299..7dd5725e2e 100644 --- a/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.json +++ b/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.json @@ -21,6 +21,10 @@ "total_security_value", "column_break_11", "maximum_loan_value", + "more_information_section", + "reference_no", + "column_break_18", + "description", "amended_from" ], "fields": [ @@ -129,11 +133,34 @@ "label": "Applicant Type", "options": "Employee\nMember\nCustomer", "reqd": 1 + }, + { + "collapsible": 1, + "fieldname": "more_information_section", + "fieldtype": "Section Break", + "label": "More Information" + }, + { + "allow_on_submit": 1, + "fieldname": "reference_no", + "fieldtype": "Data", + "label": "Reference No" + }, + { + "fieldname": "column_break_18", + "fieldtype": "Column Break" + }, + { + "allow_on_submit": 1, + "fieldname": "description", + "fieldtype": "Text", + "label": "Description" } ], + "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-07-02 23:38:24.002382", + "modified": "2020-09-04 22:38:19.894488", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Security Pledge", diff --git a/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py b/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py index 02efe240bd..0f42bde3c4 100644 --- a/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py +++ b/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals import frappe -from frappe.utils import get_datetime +from frappe.utils import get_datetime, flt from frappe.model.document import Document from six import iteritems from erpnext.loan_management.doctype.loan_security_unpledge.loan_security_unpledge import get_pledged_security_qty @@ -51,13 +51,19 @@ def check_for_ltv_shortfall(process_loan_security_shortfall): "valid_upto": (">=", update_time) }, as_list=1)) - loans = frappe.get_all('Loan', fields=['name', 'loan_amount', 'total_principal_paid'], - filters={'status': 'Disbursed', 'is_secured_loan': 1}) + loans = frappe.get_all('Loan', fields=['name', 'loan_amount', 'total_principal_paid', 'total_payment', + 'total_interest_payable', 'disbursed_amount', 'status'], + filters={'status': ('in',['Disbursed','Partially Disbursed']), 'is_secured_loan': 1}) loan_security_map = {} for loan in loans: - outstanding_amount = loan.loan_amount - loan.total_principal_paid + if loan.status == 'Disbursed': + outstanding_amount = flt(loan.total_payment) - flt(loan.total_interest_payable) \ + - flt(loan.total_principal_paid) + else: + outstanding_amount = loan.disbursed_amount + pledged_securities = get_pledged_security_qty(loan.name) ltv_ratio = '' security_value = 0.0 diff --git a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.json b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.json index aece46ffda..2e2b2518d2 100644 --- a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.json +++ b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.json @@ -16,6 +16,10 @@ "status", "loan_security_details_section", "securities", + "more_information_section", + "reference_no", + "column_break_13", + "description", "amended_from" ], "fields": [ @@ -95,11 +99,34 @@ "label": "Applicant Type", "options": "Employee\nMember\nCustomer", "reqd": 1 + }, + { + "collapsible": 1, + "fieldname": "more_information_section", + "fieldtype": "Section Break", + "label": "More Information" + }, + { + "allow_on_submit": 1, + "fieldname": "reference_no", + "fieldtype": "Data", + "label": "Reference No" + }, + { + "fieldname": "column_break_13", + "fieldtype": "Column Break" + }, + { + "allow_on_submit": 1, + "fieldname": "description", + "fieldtype": "Text", + "label": "Description" } ], + "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-05-05 07:23:18.440058", + "modified": "2020-09-04 22:39:57.756146", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Security Unpledge", diff --git a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py index 5e9d82aa91..b3eb6001e4 100644 --- a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py +++ b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py @@ -17,7 +17,6 @@ class LoanSecurityUnpledge(Document): self.validate_unpledge_qty() def on_cancel(self): - self.update_loan_security_pledge(cancel=1) self.update_loan_status(cancel=1) self.db_set('status', 'Requested') @@ -43,13 +42,14 @@ class LoanSecurityUnpledge(Document): "valid_upto": (">=", get_datetime()) }, as_list=1)) - loan_amount, principal_paid = frappe.get_value("Loan", self.loan, ['loan_amount', 'total_principal_paid']) - pending_principal_amount = loan_amount - principal_paid + total_payment, principal_paid, interest_payable = frappe.get_value("Loan", self.loan, ['total_payment', 'total_principal_paid', + 'total_interest_payable']) + + pending_principal_amount = flt(total_payment) - flt(interest_payable) - flt(principal_paid) security_value = 0 for security in self.securities: - pledged_qty = pledge_qty_map.get(security.loan_security) - + pledged_qty = pledge_qty_map.get(security.loan_security, 0) if security.qty > pledged_qty: frappe.throw(_("""Row {0}: {1} {2} of {3} is pledged against Loan {4}. You are trying to unpledge more""").format(security.idx, pledged_qty, security.uom, @@ -58,16 +58,23 @@ class LoanSecurityUnpledge(Document): qty_after_unpledge = pledged_qty - security.qty ltv_ratio = ltv_ratio_map.get(security.loan_security_type) - security_value += qty_after_unpledge * loan_security_price_map.get(security.loan_security) + current_price = loan_security_price_map.get(security.loan_security) + if not current_price: + frappe.throw(_("No valid Loan Security Price found for {0}").format(frappe.bold(security.loan_security))) - if not security_value and pending_principal_amount > 0: + security_value += qty_after_unpledge * current_price + + if not security_value and flt(pending_principal_amount, 2) > 0: frappe.throw("Cannot Unpledge, loan to value ratio is breaching") - if security_value and (pending_principal_amount/security_value) * 100 > ltv_ratio: + if security_value and flt(pending_principal_amount/security_value) * 100 > ltv_ratio: frappe.throw("Cannot Unpledge, loan to value ratio is breaching") def on_update_after_submit(self): - if self.status == "Approved": + self.approve() + + def approve(self): + if self.status == "Approved" and not self.unpledge_time: self.update_loan_status() self.db_set('unpledge_time', get_datetime()) diff --git a/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.py b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.py index cd3cf7ec96..0fa96860d0 100644 --- a/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.py +++ b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.py @@ -36,6 +36,8 @@ def process_loan_interest_accrual_for_demand_loans(posting_date=None, loan_type= loan_process.submit() + return loan_process.name + def process_loan_interest_accrual_for_term_loans(posting_date=None, loan_type=None, loan=None): if not term_loan_accrual_pending(posting_date or nowdate()): @@ -49,6 +51,8 @@ def process_loan_interest_accrual_for_term_loans(posting_date=None, loan_type=No loan_process.submit() + return loan_process.name + def term_loan_accrual_pending(date): pending_accrual = frappe.db.get_value('Repayment Schedule', { 'payment_date': ('<=', date), diff --git a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py index add7bbfa57..cba6a2d014 100644 --- a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py +++ b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py @@ -67,16 +67,16 @@ class MaintenanceSchedule(TransactionBase): for key in scheduled_date: description =frappe._("Reference: {0}, Item Code: {1} and Customer: {2}").format(self.name, d.item_code, self.customer) - frappe.get_doc({ + event = frappe.get_doc({ "doctype": "Event", "owner": email_map.get(d.sales_person, self.owner), "subject": description, "description": description, "starts_on": cstr(key["scheduled_date"]) + " 10:00:00", "event_type": "Private", - "ref_type": self.doctype, - "ref_name": self.name - }).insert(ignore_permissions=1) + }) + event.add_participant(self.doctype, self.name) + event.insert(ignore_permissions=1) frappe.db.set(self, 'status', 'Submitted') diff --git a/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py b/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py index d8ae17b4c7..3c307e920f 100644 --- a/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py +++ b/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py @@ -2,6 +2,7 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt from __future__ import unicode_literals +from frappe.utils.data import get_datetime, add_days import frappe import unittest @@ -9,4 +10,39 @@ import unittest # test_records = frappe.get_test_records('Maintenance Schedule') class TestMaintenanceSchedule(unittest.TestCase): - pass + def test_events_should_be_created_and_deleted(self): + ms = make_maintenance_schedule() + ms.generate_schedule() + ms.submit() + + all_events = get_events(ms) + self.assertTrue(len(all_events) > 0) + + ms.cancel() + events_after_cancel = get_events(ms) + self.assertTrue(len(events_after_cancel) == 0) + +def get_events(ms): + return frappe.get_all("Event Participants", filters={ + "reference_doctype": ms.doctype, + "reference_docname": ms.name, + "parenttype": "Event" + }) + +def make_maintenance_schedule(): + ms = frappe.new_doc("Maintenance Schedule") + ms.company = "_Test Company" + ms.customer = "_Test Customer" + ms.transaction_date = get_datetime() + + ms.append("items", { + "item_code": "_Test Item", + "start_date": get_datetime(), + "end_date": add_days(get_datetime(), 32), + "periodicity": "Weekly", + "no_of_visits": 4, + "sales_person": "Sales Team", + }) + ms.insert(ignore_permissions=True) + + return ms diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index c51f655a66..3189433837 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -494,7 +494,7 @@ class BOM(WebsiteGenerator): 'image' : d.image, 'stock_uom' : d.stock_uom, 'stock_qty' : flt(d.stock_qty), - 'rate' : flt(d.base_rate) / flt(d.conversion_factor), + 'rate' : flt(d.base_rate) / (flt(d.conversion_factor) or 1.0), 'include_item_in_manufacturing': d.include_item_in_manufacturing })) diff --git a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py index e6c10ad12b..742d18c4cd 100644 --- a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py +++ b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py @@ -90,6 +90,7 @@ def update_latest_price_in_all_boms(): update_cost() def replace_bom(args): + frappe.db.auto_commit_on_many_writes = 1 args = frappe._dict(args) doc = frappe.get_doc("BOM Update Tool") @@ -97,6 +98,8 @@ def replace_bom(args): doc.new_bom = args.new_bom doc.replace_bom() + frappe.db.auto_commit_on_many_writes = 0 + def update_cost(): frappe.db.auto_commit_on_many_writes = 1 bom_list = get_boms_in_bottom_up_order() diff --git a/erpnext/manufacturing/doctype/job_card/job_card.js b/erpnext/manufacturing/doctype/job_card/job_card.js index bab0dfb6b4..b051b3243f 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.js +++ b/erpnext/manufacturing/doctype/job_card/job_card.js @@ -2,6 +2,17 @@ // For license information, please see license.txt frappe.ui.form.on('Job Card', { + setup: function(frm) { + frm.set_query('operation', function() { + return { + query: 'erpnext.manufacturing.doctype.job_card.job_card.get_operations', + filters: { + 'work_order': frm.doc.work_order + } + }; + }); + }, + refresh: function(frm) { frappe.flags.pause_job = 0; frappe.flags.resume_job = 0; @@ -20,12 +31,60 @@ frappe.ui.form.on('Job Card', { } } + frm.trigger("toggle_operation_number"); + if (frm.doc.docstatus == 0 && (frm.doc.for_quantity > frm.doc.total_completed_qty || !frm.doc.for_quantity) - && (!frm.doc.items.length || frm.doc.for_quantity == frm.doc.transferred_qty)) { + && (frm.doc.items || !frm.doc.items.length || frm.doc.for_quantity == frm.doc.transferred_qty)) { frm.trigger("prepare_timer_buttons"); } }, + operation: function(frm) { + frm.trigger("toggle_operation_number"); + + if (frm.doc.operation && frm.doc.work_order) { + frappe.call({ + method: "erpnext.manufacturing.doctype.job_card.job_card.get_operation_details", + args: { + "work_order":frm.doc.work_order, + "operation":frm.doc.operation + }, + callback: function (r) { + if (r.message) { + if (r.message.length == 1) { + frm.set_value("operation_id", r.message[0].name); + } else { + let args = []; + + r.message.forEach((row) => { + args.push({ "label": row.idx, "value": row.name }); + }); + + let description = __("Operation {0} added multiple times in the work order {1}", + [frm.doc.operation, frm.doc.work_order]); + + frm.set_df_property("operation_row_number", "options", args); + frm.set_df_property("operation_row_number", "description", description); + } + + frm.trigger("toggle_operation_number"); + } + } + }) + } + }, + + operation_row_number(frm) { + if (frm.doc.operation_row_number) { + frm.set_value("operation_id", frm.doc.operation_row_number); + } + }, + + toggle_operation_number(frm) { + frm.toggle_display("operation_row_number", !frm.doc.operation_id && frm.doc.operation); + frm.toggle_reqd("operation_row_number", !frm.doc.operation_id && frm.doc.operation); + }, + prepare_timer_buttons: function(frm) { frm.trigger("make_dashboard"); if (!frm.doc.job_started) { @@ -35,9 +94,9 @@ frappe.ui.form.on('Job Card', { fieldname: 'employee'}, d => { if (d.employee) { frm.set_value("employee", d.employee); + } else { + frm.events.start_job(frm); } - - frm.events.start_job(frm); }, __("Enter Value"), __("Start")); } else { frm.events.start_job(frm); @@ -82,9 +141,7 @@ frappe.ui.form.on('Job Card', { frm.set_value('current_time' , 0); } - frm.save("Save", () => {}, "", () => { - frm.doc.time_logs.pop(-1); - }); + frm.save(); }, complete_job: function(frm, completed_time, completed_qty) { @@ -116,6 +173,8 @@ frappe.ui.form.on('Job Card', { employee: function(frm) { if (frm.doc.job_started && !frm.doc.current_time) { frm.trigger("reset_timer"); + } else { + frm.events.start_job(frm); } }, diff --git a/erpnext/manufacturing/doctype/job_card/job_card.json b/erpnext/manufacturing/doctype/job_card/job_card.json index fba670c1c1..087ab6b484 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.json +++ b/erpnext/manufacturing/doctype/job_card/job_card.json @@ -11,6 +11,7 @@ "bom_no", "workstation", "operation", + "operation_row_number", "column_break_4", "posting_date", "company", @@ -291,11 +292,15 @@ "no_copy": 1, "print_hide": 1, "read_only": 1 + }, + { + "fieldname": "operation_row_number", + "fieldtype": "Select", + "label": "Operation Row Number" } ], "is_submittable": 1, - "links": [], - "modified": "2020-04-20 15:14:00.273441", + "modified": "2020-08-24 15:21:21.398267", "modified_by": "Administrator", "module": "Manufacturing", "name": "Job Card", @@ -347,7 +352,6 @@ "write": 1 } ], - "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", "title_field": "operation", diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index c29d4ba3d5..8855e0acf5 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -15,10 +15,13 @@ from erpnext.manufacturing.doctype.manufacturing_settings.manufacturing_settings class OverlapError(frappe.ValidationError): pass +class OperationMismatchError(frappe.ValidationError): pass + class JobCard(Document): def validate(self): self.validate_time_logs() self.set_status() + self.validate_operation_id() def validate_time_logs(self): self.total_completed_qty = 0.0 @@ -209,11 +212,10 @@ class JobCard(Document): for_quantity, time_in_mins = 0, 0 from_time_list, to_time_list = [], [] - field = "operation_id" if self.operation_id else "operation" + field = "operation_id" data = frappe.get_all('Job Card', fields = ["sum(total_time_in_mins) as time_in_mins", "sum(total_completed_qty) as completed_qty"], - filters = {"docstatus": 1, "work_order": self.work_order, - "workstation": self.workstation, field: self.get(field)}) + filters = {"docstatus": 1, "work_order": self.work_order, field: self.get(field)}) if data and len(data) > 0: for_quantity = data[0].completed_qty @@ -226,14 +228,13 @@ class JobCard(Document): FROM `tabJob Card` jc, `tabJob Card Time Log` jctl WHERE jctl.parent = jc.name and jc.work_order = %s - and jc.workstation = %s and jc.{0} = %s and jc.docstatus = 1 - """.format(field), (self.work_order, self.workstation, self.get(field)), as_dict=1) + and jc.{0} = %s and jc.docstatus = 1 + """.format(field), (self.work_order, self.get(field)), as_dict=1) wo = frappe.get_doc('Work Order', self.work_order) - work_order_field = "name" if field == "operation_id" else field for data in wo.operations: - if data.get(work_order_field) == self.get(field) and data.workstation == self.workstation: + if data.get("name") == self.get(field): data.completed_qty = for_quantity data.actual_operation_time = time_in_mins data.actual_start_time = time_data[0].start_time if time_data else None @@ -306,6 +307,37 @@ class JobCard(Document): if update_status: self.db_set('status', self.status) + def validate_operation_id(self): + if (self.get("operation_id") and self.get("operation_row_number") and self.operation and self.work_order and + frappe.get_cached_value("Work Order Operation", self.operation_row_number, "name") != self.operation_id): + work_order = frappe.bold(get_link_to_form("Work Order", self.work_order)) + frappe.throw(_("Operation {0} does not belong to the work order {1}") + .format(frappe.bold(self.operation), work_order), OperationMismatchError) + +@frappe.whitelist() +def get_operation_details(work_order, operation): + if work_order and operation: + return frappe.get_all("Work Order Operation", fields = ["name", "idx"], + filters = { + "parent": work_order, + "operation": operation + } + ) + +@frappe.whitelist() +def get_operations(doctype, txt, searchfield, start, page_len, filters): + if filters.get("work_order"): + args = {"parent": filters.get("work_order")} + if txt: + args["operation"] = ("like", "%{0}%".format(txt)) + + return frappe.get_all("Work Order Operation", + filters = args, + fields = ["distinct operation as operation"], + limit_start = start, + limit_page_length = page_len, + order_by="idx asc", as_list=1) + @frappe.whitelist() def make_material_request(source_name, target_doc=None): def update_item(obj, target, source_parent): diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py index ca05fea0f6..b6a6c33d37 100644 --- a/erpnext/manufacturing/doctype/job_card/test_job_card.py +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py @@ -4,6 +4,72 @@ from __future__ import unicode_literals import unittest +import frappe +from frappe.utils import random_string +from erpnext.manufacturing.doctype.workstation.test_workstation import make_workstation +from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record +from erpnext.manufacturing.doctype.job_card.job_card import OperationMismatchError class TestJobCard(unittest.TestCase): - pass + def test_job_card(self): + data = frappe.get_cached_value('BOM', + {'docstatus': 1, 'with_operations': 1, 'company': '_Test Company'}, ['name', 'item']) + + if data: + bom, bom_item = data + + work_order = make_wo_order_test_record(item=bom_item, qty=1, bom_no=bom) + + job_cards = frappe.get_all('Job Card', + filters = {'work_order': work_order.name}, fields = ["operation_id", "name"]) + + if job_cards: + job_card = job_cards[0] + frappe.db.set_value("Job Card", job_card.name, "operation_row_number", job_card.operation_id) + + doc = frappe.get_doc("Job Card", job_card.name) + doc.operation_id = "Test Data" + self.assertRaises(OperationMismatchError, doc.save) + + for d in job_cards: + frappe.delete_doc("Job Card", d.name) + + def test_job_card_with_different_work_station(self): + data = frappe.get_cached_value('BOM', + {'docstatus': 1, 'with_operations': 1, 'company': '_Test Company'}, ['name', 'item']) + + if data: + bom, bom_item = data + + work_order = make_wo_order_test_record(item=bom_item, qty=1, bom_no=bom) + + job_cards = frappe.get_all('Job Card', + filters = {'work_order': work_order.name}, + fields = ["operation_id", "workstation", "name", "for_quantity"]) + + job_card = job_cards[0] + + if job_card: + workstation = frappe.db.get_value("Workstation", + {"name": ("not in", [job_card.workstation])}, "name") + + if not workstation or job_card.workstation == workstation: + workstation = make_workstation(workstation_name=random_string(5)).name + + doc = frappe.get_doc("Job Card", job_card.name) + doc.workstation = workstation + doc.append("time_logs", { + "from_time": "2009-01-01 12:06:25", + "to_time": "2009-01-01 12:37:25", + "time_in_mins": "31.00002", + "completed_qty": job_card.for_quantity + }) + doc.submit() + + completed_qty = frappe.db.get_value("Work Order Operation", job_card.operation_id, "completed_qty") + self.assertEqual(completed_qty, job_card.for_quantity) + + doc.cancel() + + for d in job_cards: + frappe.delete_doc("Job Card", d.name) \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index 2260befb3f..b7c7c32869 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -7,7 +7,7 @@ import unittest import frappe from frappe.utils import flt, time_diff_in_hours, now, add_months, cint, today from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory -from erpnext.manufacturing.doctype.work_order.work_order import (make_stock_entry, +from erpnext.manufacturing.doctype.work_order.work_order import (make_stock_entry, ItemHasVariantError, stop_unstop, StockOverProductionError, OverProductionError, CapacityError) from erpnext.stock.doctype.stock_entry import test_stock_entry from erpnext.stock.utils import get_bin diff --git a/erpnext/manufacturing/doctype/workstation/test_workstation.py b/erpnext/manufacturing/doctype/workstation/test_workstation.py index 2169260854..8266cf7b77 100644 --- a/erpnext/manufacturing/doctype/workstation/test_workstation.py +++ b/erpnext/manufacturing/doctype/workstation/test_workstation.py @@ -20,3 +20,18 @@ class TestWorkstation(unittest.TestCase): "_Test Workstation 1", "Operation 1", "2013-02-02 05:00:00", "2013-02-02 20:00:00") self.assertRaises(WorkstationHolidayError, check_if_within_operating_hours, "_Test Workstation 1", "Operation 1", "2013-02-01 10:00:00", "2013-02-02 20:00:00") + +def make_workstation(**args): + args = frappe._dict(args) + + try: + doc = frappe.get_doc({ + "doctype": "Workstation", + "workstation_name": args.workstation_name + }) + + doc.insert() + + return doc + except frappe.DuplicateEntryError: + return frappe.get_doc("Workstation", args.workstation_name) \ No newline at end of file diff --git a/erpnext/non_profit/doctype/member/member.json b/erpnext/non_profit/doctype/member/member.json index bb73a843ee..77cdb94b3d 100644 --- a/erpnext/non_profit/doctype/member/member.json +++ b/erpnext/non_profit/doctype/member/member.json @@ -133,7 +133,8 @@ { "fieldname": "email_id", "fieldtype": "Data", - "label": "Email Address" + "label": "Email Address", + "options": "Email" }, { "fieldname": "subscription_id", @@ -176,7 +177,7 @@ ], "image_field": "image", "links": [], - "modified": "2020-04-07 14:20:33.215700", + "modified": "2020-08-06 10:06:01.153564", "modified_by": "Administrator", "module": "Non Profit", "name": "Member", diff --git a/erpnext/non_profit/doctype/member/member.py b/erpnext/non_profit/doctype/member/member.py index c52082ca23..44b975e9e9 100644 --- a/erpnext/non_profit/doctype/member/member.py +++ b/erpnext/non_profit/doctype/member/member.py @@ -9,6 +9,7 @@ from frappe.model.document import Document from frappe.contacts.address_and_contact import load_address_and_contact from frappe.utils import cint from frappe.integrations.utils import get_payment_gateway_controller +from erpnext.non_profit.doctype.membership_type.membership_type import get_membership_type class Member(Document): def onload(self): @@ -74,19 +75,23 @@ def get_or_create_member(user_details): return create_member(user_details) def create_member(user_details): + user_details = frappe._dict(user_details) member = frappe.new_doc("Member") member.update({ "member_name": user_details.fullname, "email_id": user_details.email, - "pan_number": user_details.pan, + "pan_number": user_details.pan or None, "membership_type": user_details.plan_id, - "customer": create_customer(user_details) + "subscription_id": user_details.subscription_id or None }) member.insert(ignore_permissions=True) + member.customer = create_customer(user_details, member.name) + member.save(ignore_permissions=True) + return member -def create_customer(user_details): +def create_customer(user_details, member=None): customer = frappe.new_doc("Customer") customer.customer_name = user_details.fullname customer.customer_type = "Individual" @@ -107,7 +112,13 @@ def create_customer(user_details): "link_name": customer.name }) - contact.save() + if member: + contact.append("links", { + "link_doctype": "Member", + "link_name": member + }) + + contact.save(ignore_permissions=True) except frappe.DuplicateEntryError: return customer.name @@ -139,12 +150,31 @@ def create_member_subscription_order(user_details): user_details = frappe._dict(user_details) member = get_or_create_member(user_details) - if not member: - member = create_member(user_details) subscription = member.setup_subscription() member.subscription_id = subscription.get('subscription_id') member.save(ignore_permissions=True) - return subscription \ No newline at end of file + return subscription + +@frappe.whitelist() +def register_member(fullname, email, rzpay_plan_id, subscription_id, pan=None, mobile=None): + plan = get_membership_type(rzpay_plan_id) + if not plan: + raise frappe.DoesNotExistError + + member = frappe.db.exists("Member", {'email': email, 'subscription_id': subscription_id }) + if member: + return member + else: + member = create_member(dict( + fullname=fullname, + email=email, + plan_id=plan, + subscription_id=subscription_id, + pan=pan, + mobile=mobile + )) + + return member.name \ No newline at end of file diff --git a/erpnext/non_profit/doctype/membership/membership.js b/erpnext/non_profit/doctype/membership/membership.js index 554549a0bd..ee8a8c0a7b 100644 --- a/erpnext/non_profit/doctype/membership/membership.js +++ b/erpnext/non_profit/doctype/membership/membership.js @@ -8,6 +8,24 @@ frappe.ui.form.on('Membership', { }) }, + refresh: function(frm) { + !frm.doc.invoice && frm.add_custom_button("Generate Invoice", () => { + frm.call("generate_invoice", { + save: true + }).then(() => { + frm.reload_doc(); + }); + }); + + frappe.db.get_single_value("Membership Settings", "send_email").then(val => { + if (val) frm.add_custom_button("Send Acknowledgement", () => { + frm.call("send_acknowlement").then(() => { + frm.reload_doc(); + }); + }); + }) + }, + onload: function(frm) { frm.add_fetch('membership_type', 'amount', 'amount'); } diff --git a/erpnext/non_profit/doctype/membership/membership.json b/erpnext/non_profit/doctype/membership/membership.json index 238f4c31fd..95bb3a5d84 100644 --- a/erpnext/non_profit/doctype/membership/membership.json +++ b/erpnext/non_profit/doctype/membership/membership.json @@ -19,10 +19,10 @@ "paid", "currency", "amount", + "invoice", "razorpay_details_section", "subscription_id", - "payment_id", - "webhook_payload" + "payment_id" ], "fields": [ { @@ -118,17 +118,15 @@ "read_only": 1 }, { - "fieldname": "webhook_payload", - "fieldtype": "Code", - "hidden": 1, - "label": "Webhook Payload", - "options": "JSON", - "read_only": 1 + "fieldname": "invoice", + "fieldtype": "Link", + "label": "Invoice", + "options": "Sales Invoice" } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2020-07-27 14:28:11.532696", + "modified": "2020-07-31 13:57:02.328995", "modified_by": "Administrator", "module": "Non Profit", "name": "Membership", diff --git a/erpnext/non_profit/doctype/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py index 729e111e57..f058004ff9 100644 --- a/erpnext/non_profit/doctype/membership/membership.py +++ b/erpnext/non_profit/doctype/membership/membership.py @@ -10,6 +10,7 @@ from datetime import datetime from frappe.model.document import Document from frappe.email import sendmail_to_system_managers from frappe.utils import add_days, add_years, nowdate, getdate, add_months, get_link_to_form +from erpnext.non_profit.doctype.member.member import create_member from frappe import _ import erpnext @@ -57,11 +58,95 @@ class Membership(Document): self.load_from_db() self.db_set('paid', 1) + def generate_invoice(self, save=True): + if not (self.paid or self.currency or self.amount): + frappe.throw(_("The payment for this membership is not paid. To generate invoice fill the payment details")) + + if self.invoice: + frappe.throw(_("An invoice is already linked to this document")) + + member = frappe.get_doc("Member", self.member) + plan = frappe.get_doc("Membership Type", self.membership_type) + settings = frappe.get_doc("Membership Settings") + + if not member.customer: + frappe.throw(_("No customer linked to member {}", [member.name])) + + if not settings.debit_account: + frappe.throw(_("You need to set Debit Account in Membership Settings")) + + if not settings.company: + frappe.throw(_("You need to set Default Company for invoicing in Membership Settings")) + + invoice = make_invoice(self, member, plan, settings) + self.invoice = invoice.name + + if save: + self.save() + + return invoice + + def send_acknowlement(self): + settings = frappe.get_doc("Membership Settings") + if not settings.send_email: + frappe.throw(_("You need to enable Send Acknowledge Email in Membership Settings")) + + member = frappe.get_doc("Member", self.member) + plan = frappe.get_doc("Membership Type", self.membership_type) + email = member.email_id if member.email_id else member.email + attachments = [frappe.attach_print("Membership", self.name, print_format=settings.membership_print_format)] + + if self.invoice and settings.send_invoice: + attachments.append(frappe.attach_print("Sales Invoice", self.invoice, print_format=settings.inv_print_format)) + + email_template = frappe.get_doc("Email Template", settings.email_template) + context = { "doc": self, "member": member} + + email_args = { + "recipients": [email], + "message": frappe.render_template(email_template.get("response"), context), + "subject": frappe.render_template(email_template.get("subject"), context), + "attachments": attachments, + "reference_doctype": self.doctype, + "reference_name": self.name + } + + if not frappe.flags.in_test: + frappe.enqueue(method=frappe.sendmail, queue='short', timeout=300, is_async=True, **email_args) + else: + frappe.sendmail(**email_args) + + def generate_and_send_invoice(self): + invoice = self.generate_invoice(False) + self.send_acknowlement() + +def make_invoice(membership, member, plan, settings): + invoice = frappe.get_doc({ + 'doctype': 'Sales Invoice', + 'customer': member.customer, + 'debit_to': settings.debit_account, + 'currency': membership.currency, + 'is_pos': 0, + 'items': [ + { + 'item_code': plan.linked_item, + 'rate': membership.amount, + 'qty': 1 + } + ] + }) + + invoice.insert(ignore_permissions=True) + invoice.submit() + + return invoice + def get_member_based_on_subscription(subscription_id, email): members = frappe.get_all("Member", filters={ 'subscription_id': subscription_id, 'email_id': email }, order_by="creation desc") + try: return frappe.get_doc("Member", members[0]['name']) except: @@ -77,16 +162,15 @@ def verify_signature(data): controller.verify_signature(data, signature, key) - @frappe.whitelist(allow_guest=True) def trigger_razorpay_subscription(*args, **kwargs): data = frappe.request.get_data(as_text=True) try: verify_signature(data) except Exception as e: - signature = frappe.request.headers.get('X-Razorpay-Signature') - log = "{0} \n\n {1} \n\n {2} \n\n {3}".format(e, frappe.get_traceback(), signature, data) - frappe.log_error(e, "Webhook Verification Error") + log = frappe.log_error(e, "Webhook Verification Error") + notify_failure(log) + return { 'status': 'Failed', 'reason': e} if isinstance(data, six.string_types): data = json.loads(data) @@ -99,35 +183,42 @@ def trigger_razorpay_subscription(*args, **kwargs): payment = frappe._dict(payment) try: - data_json = json.dumps(data, indent=4, sort_keys=True) + if not data.event == "subscription.charged": + return + member = get_member_based_on_subscription(subscription.id, payment.email) - except Exception as e: - error_log = frappe.log_error(frappe.get_traceback() + '\n' + data_json , _("Membership Webhook Failed")) - notify_failure(error_log) - return { status: 'Failed' } + if not member: + member = create_member(frappe._dict({ + 'fullname': payment.email, + 'email': payment.email, + 'plan_id': get_plan_from_razorpay_id(subscription.plan_id) + })) - if not member: - return { status: 'Failed' } - try: - if data.event == "subscription.activated": + member.subscription_id = subscription.id member.customer_id = payment.customer_id - elif data.event == "subscription.charged": - membership = frappe.new_doc("Membership") - membership.update({ - "member": member.name, - "membership_status": "Current", - "membership_type": member.membership_type, - "currency": "INR", - "paid": 1, - "payment_id": payment.id, - "webhook_payload": data_json, - "from_date": datetime.fromtimestamp(subscription.current_start), - "to_date": datetime.fromtimestamp(subscription.current_end), - "amount": payment.amount / 100 # Convert to rupees from paise - }) - membership.insert(ignore_permissions=True) + if subscription.notes and type(subscription.notes) == dict: + notes = '\n'.join("{}: {}".format(k, v) for k, v in subscription.notes.items()) + member.add_comment("Comment", notes) + elif subscription.notes and type(subscription.notes) == str: + member.add_comment("Comment", subscription.notes) - # Update these values anyway + + # Update Membership + membership = frappe.new_doc("Membership") + membership.update({ + "member": member.name, + "membership_status": "Current", + "membership_type": member.membership_type, + "currency": "INR", + "paid": 1, + "payment_id": payment.id, + "from_date": datetime.fromtimestamp(subscription.current_start), + "to_date": datetime.fromtimestamp(subscription.current_end), + "amount": payment.amount / 100 # Convert to rupees from paise + }) + membership.insert(ignore_permissions=True) + + # Update membership values member.subscription_start = datetime.fromtimestamp(subscription.start_at) member.subscription_end = datetime.fromtimestamp(subscription.end_at) member.subscription_activated = 1 @@ -135,9 +226,9 @@ def trigger_razorpay_subscription(*args, **kwargs): except Exception as e: log = frappe.log_error(e, "Error creating membership entry") notify_failure(log) - return { status: 'Failed' } + return { 'status': 'Failed', 'reason': e} - return { status: 'Success' } + return { 'status': 'Success' } def notify_failure(log): @@ -152,3 +243,11 @@ Administrator""".format(get_link_to_form("Error Log", log.name)) sendmail_to_system_managers("[Important] [ERPNext] Razorpay membership webhook failed , please check.", content) except: pass + +def get_plan_from_razorpay_id(plan_id): + plan = frappe.get_all("Membership Type", filters={'razorpay_plan_id': plan_id}, order_by="creation desc") + + try: + return plan[0]['name'] + except: + return None \ No newline at end of file diff --git a/erpnext/non_profit/doctype/membership_settings/membership_settings.js b/erpnext/non_profit/doctype/membership_settings/membership_settings.js index 8c0e3a4fa7..1d894027b0 100644 --- a/erpnext/non_profit/doctype/membership_settings/membership_settings.js +++ b/erpnext/non_profit/doctype/membership_settings/membership_settings.js @@ -10,7 +10,39 @@ frappe.ui.form.on("Membership Settings", { }) }); } + + frm.set_query('inv_print_format', function(doc) { + return { + filters: { + "doc_type": "Sales Invoice" + } + }; + }); + + frm.set_query('membership_print_format', function(doc) { + return { + filters: { + "doc_type": "Membership" + } + }; + }); + + frm.set_query('debit_account', function(doc) { + return { + filters: { + 'account_type': 'Receivable', + 'is_group': 0, + 'company': frm.doc.company + } + }; + }); + + let docs_url = "https://docs.erpnext.com/docs/user/manual/en/non_profit/membership"; + + frm.set_intro(__("You can learn more about memberships in the manual. ") + `${__('ERPNext Docs')}`, true); + frm.trigger("add_generate_button"); + frm.trigger("add_copy_buttonn"); }, add_generate_button: function(frm) { @@ -27,4 +59,12 @@ frappe.ui.form.on("Membership Settings", { }); }); }, + + add_copy_buttonn: function(frm) { + if (frm.doc.webhook_secret) { + frm.add_custom_button(__("Copy Webhook URL"), () => { + frappe.utils.copy_to_clipboard(`https://${frappe.boot.sitename}/api/method/erpnext.non_profit.doctype.membership.membership.trigger_razorpay_subscription`); + }); + } + } }); diff --git a/erpnext/non_profit/doctype/membership_settings/membership_settings.json b/erpnext/non_profit/doctype/membership_settings/membership_settings.json index 52b9d01088..5b6bab5b0a 100644 --- a/erpnext/non_profit/doctype/membership_settings/membership_settings.json +++ b/erpnext/non_profit/doctype/membership_settings/membership_settings.json @@ -9,7 +9,17 @@ "razorpay_settings_section", "billing_cycle", "billing_frequency", - "webhook_secret" + "webhook_secret", + "column_break_6", + "enable_auto_invoicing", + "company", + "debit_account", + "column_break_9", + "send_email", + "send_invoice", + "membership_print_format", + "inv_print_format", + "email_template" ], "fields": [ { @@ -41,11 +51,79 @@ "fieldtype": "Password", "label": "Webhook Secret", "read_only": 1 + }, + { + "fieldname": "column_break_6", + "fieldtype": "Section Break", + "label": "Invoicing" + }, + { + "default": "0", + "fieldname": "enable_auto_invoicing", + "fieldtype": "Check", + "label": "Enable Auto Invoicing", + "mandatory_depends_on": "eval:doc.send_invoice" + }, + { + "depends_on": "eval:doc.enable_auto_invoicing", + "fieldname": "debit_account", + "fieldtype": "Link", + "label": "Debit Account", + "mandatory_depends_on": "eval:doc.enable_auto_invoicing", + "options": "Account" + }, + { + "fieldname": "column_break_9", + "fieldtype": "Column Break" + }, + { + "depends_on": "eval:doc.enable_auto_invoicing", + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "mandatory_depends_on": "eval:doc.enable_auto_invoicing", + "options": "Company" + }, + { + "default": "0", + "depends_on": "eval:doc.enable_auto_invoicing && doc.send_email", + "fieldname": "send_invoice", + "fieldtype": "Check", + "label": "Send Invoice with Email" + }, + { + "default": "0", + "fieldname": "send_email", + "fieldtype": "Check", + "label": "Send Membership Acknowledgement" + }, + { + "depends_on": "eval: doc.send_invoice", + "fieldname": "inv_print_format", + "fieldtype": "Link", + "label": "Invoice Print Format", + "mandatory_depends_on": "eval: doc.send_invoice", + "options": "Print Format" + }, + { + "depends_on": "eval:doc.send_email", + "fieldname": "membership_print_format", + "fieldtype": "Link", + "label": "Membership Print Format", + "options": "Print Format" + }, + { + "depends_on": "eval:doc.send_email", + "fieldname": "email_template", + "fieldtype": "Link", + "label": "Email Template", + "mandatory_depends_on": "eval:doc.send_email", + "options": "Email Template" } ], "issingle": 1, "links": [], - "modified": "2020-05-22 12:38:27.103759", + "modified": "2020-08-05 17:26:37.287395", "modified_by": "Administrator", "module": "Non Profit", "name": "Membership Settings", @@ -60,6 +138,23 @@ "role": "System Manager", "share": 1, "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "Non Profit Manager", + "share": 1, + "write": 1 + }, + { + "email": 1, + "print": 1, + "read": 1, + "role": "Non Profit Member", + "share": 1 } ], "quick_entry": 1, diff --git a/erpnext/non_profit/doctype/membership_type/membership_type.js b/erpnext/non_profit/doctype/membership_type/membership_type.js index 226981dc78..43311a2c96 100644 --- a/erpnext/non_profit/doctype/membership_type/membership_type.js +++ b/erpnext/non_profit/doctype/membership_type/membership_type.js @@ -5,6 +5,10 @@ frappe.ui.form.on('Membership Type', { refresh: function(frm) { frappe.db.get_single_value("Membership Settings", "enable_razorpay").then(val => { if (val) frm.set_df_property('razorpay_plan_id', 'hidden', false); - }) + }); + + frappe.db.get_single_value("Membership Settings", "enable_auto_invoicing").then(val => { + if (val) frm.set_df_property('linked_item', 'hidden', false); + }); } }); diff --git a/erpnext/non_profit/doctype/membership_type/membership_type.json b/erpnext/non_profit/doctype/membership_type/membership_type.json index 319078fd6c..6ce1ecde12 100644 --- a/erpnext/non_profit/doctype/membership_type/membership_type.json +++ b/erpnext/non_profit/doctype/membership_type/membership_type.json @@ -8,7 +8,8 @@ "field_order": [ "membership_type", "amount", - "razorpay_plan_id" + "razorpay_plan_id", + "linked_item" ], "fields": [ { @@ -33,10 +34,17 @@ "hidden": 1, "label": "Razorpay Plan ID", "unique": 1 + }, + { + "fieldname": "linked_item", + "fieldtype": "Link", + "label": "Linked Item", + "options": "Item", + "unique": 1 } ], "links": [], - "modified": "2020-03-30 12:54:07.850857", + "modified": "2020-08-05 15:21:43.595745", "modified_by": "Administrator", "module": "Non Profit", "name": "Membership Type", diff --git a/erpnext/non_profit/doctype/membership_type/membership_type.py b/erpnext/non_profit/doctype/membership_type/membership_type.py index ed6b549600..b95b04316f 100644 --- a/erpnext/non_profit/doctype/membership_type/membership_type.py +++ b/erpnext/non_profit/doctype/membership_type/membership_type.py @@ -4,6 +4,10 @@ from __future__ import unicode_literals from frappe.model.document import Document +import frappe class MembershipType(Document): pass + +def get_membership_type(razorpay_id): + return frappe.db.exists("Membership Type", {"razorpay_plan_id": razorpay_id}) \ No newline at end of file diff --git a/erpnext/patches.txt b/erpnext/patches.txt index e17e949b3b..9ce570e6d0 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -632,7 +632,7 @@ execute:frappe.reload_doc('desk', 'doctype', 'dashboard_chart_source') execute:frappe.reload_doc('desk', 'doctype', 'dashboard_chart') execute:frappe.reload_doc('desk', 'doctype', 'dashboard_chart_field') erpnext.patches.v12_0.remove_bank_remittance_custom_fields -erpnext.patches.v12_0.generate_leave_ledger_entries +erpnext.patches.v12_0.generate_leave_ledger_entries #27-08-2020 execute:frappe.delete_doc_if_exists("Report", "Loan Repayment") erpnext.patches.v12_0.move_credit_limit_to_customer_credit_limit erpnext.patches.v12_0.add_variant_of_in_item_attribute_table @@ -697,7 +697,7 @@ erpnext.patches.v12_0.update_bom_in_so_mr execute:frappe.delete_doc("Report", "Department Analytics") execute:frappe.rename_doc("Desk Page", "Loan Management", "Loan", force=True) erpnext.patches.v12_0.update_uom_conversion_factor -execute:frappe.delete_doc_if_exists("Page", "pos") #29-05-2020 +erpnext.patches.v13_0.replace_pos_page_with_point_of_sale_page erpnext.patches.v13_0.delete_old_purchase_reports erpnext.patches.v12_0.set_italian_import_supplier_invoice_permissions erpnext.patches.v13_0.update_subscription @@ -718,7 +718,11 @@ erpnext.patches.v13_0.delete_report_requested_items_to_order erpnext.patches.v12_0.update_item_tax_template_company erpnext.patches.v13_0.move_branch_code_to_bank_account erpnext.patches.v13_0.healthcare_lab_module_rename_doctypes +erpnext.patches.v13_0.add_standard_navbar_items #4 erpnext.patches.v13_0.stock_entry_enhancements erpnext.patches.v12_0.update_state_code_for_daman_and_diu erpnext.patches.v12_0.rename_lost_reason_detail +erpnext.patches.v13_0.drop_razorpay_payload_column erpnext.patches.v13_0.update_start_end_date_for_old_shift_assignment +erpnext.patches.v13_0.setting_custom_roles_for_some_regional_reports +erpnext.patches.v13_0.change_default_pos_print_format diff --git a/erpnext/patches/v12_0/create_irs_1099_field_united_states.py b/erpnext/patches/v12_0/create_irs_1099_field_united_states.py index 43bd0ccdd7..7feaffdf40 100644 --- a/erpnext/patches/v12_0/create_irs_1099_field_united_states.py +++ b/erpnext/patches/v12_0/create_irs_1099_field_united_states.py @@ -7,6 +7,7 @@ def execute(): frappe.reload_doc('accounts', 'doctype', 'allowed_to_transact_with', force=True) frappe.reload_doc('accounts', 'doctype', 'pricing_rule_detail', force=True) frappe.reload_doc('crm', 'doctype', 'lost_reason_detail', force=True) + frappe.reload_doc('setup', 'doctype', 'quotation_lost_reason_detail', force=True) company = frappe.get_all('Company', filters = {'country': 'United States'}) if not company: diff --git a/erpnext/patches/v12_0/generate_leave_ledger_entries.py b/erpnext/patches/v12_0/generate_leave_ledger_entries.py index c5bec19fed..342c12996d 100644 --- a/erpnext/patches/v12_0/generate_leave_ledger_entries.py +++ b/erpnext/patches/v12_0/generate_leave_ledger_entries.py @@ -36,8 +36,7 @@ def generate_allocation_ledger_entries(): for allocation in allocation_list: if not frappe.db.exists("Leave Ledger Entry", {'transaction_type': 'Leave Allocation', 'transaction_name': allocation.name}): - allocation.update(dict(doctype="Leave Allocation")) - allocation_obj = frappe.get_doc(allocation) + allocation_obj = frappe.get_doc("Leave Allocation", allocation) allocation_obj.create_leave_ledger_entry() def generate_application_leave_ledger_entries(): @@ -46,8 +45,7 @@ def generate_application_leave_ledger_entries(): for application in leave_applications: if not frappe.db.exists("Leave Ledger Entry", {'transaction_type': 'Leave Application', 'transaction_name': application.name}): - application.update(dict(doctype="Leave Application")) - frappe.get_doc(application).create_leave_ledger_entry() + frappe.get_doc("Leave Application", application.name).create_leave_ledger_entry() def generate_encashment_leave_ledger_entries(): ''' fix ledger entries for missing leave encashment transaction ''' @@ -55,8 +53,7 @@ def generate_encashment_leave_ledger_entries(): for encashment in leave_encashments: if not frappe.db.exists("Leave Ledger Entry", {'transaction_type': 'Leave Encashment', 'transaction_name': encashment.name}): - encashment.update(dict(doctype="Leave Encashment")) - frappe.get_doc(encashment).create_leave_ledger_entry() + frappe.get_doc("Leave Enchashment", encashment).create_leave_ledger_entry() def generate_expiry_allocation_ledger_entries(): ''' fix ledger entries for missing leave allocation transaction ''' @@ -65,24 +62,16 @@ def generate_expiry_allocation_ledger_entries(): for allocation in allocation_list: if not frappe.db.exists("Leave Ledger Entry", {'transaction_type': 'Leave Allocation', 'transaction_name': allocation.name, 'is_expired': 1}): - allocation.update(dict(doctype="Leave Allocation")) - allocation_obj = frappe.get_doc(allocation) + allocation_obj = frappe.get_doc("Leave Allocation", allocation) if allocation_obj.to_date <= getdate(today()): expire_allocation(allocation_obj) def get_allocation_records(): - return frappe.get_all("Leave Allocation", filters={ - "docstatus": 1 - }, fields=['name', 'employee', 'leave_type', 'new_leaves_allocated', - 'unused_leaves', 'from_date', 'to_date', 'carry_forward' - ], order_by='to_date ASC') + return frappe.get_all("Leave Allocation", filters={"docstatus": 1}, + fields=['name'], order_by='to_date ASC') def get_leaves_application_records(): - return frappe.get_all("Leave Application", filters={ - "docstatus": 1 - }, fields=['name', 'employee', 'leave_type', 'total_leave_days', 'from_date', 'to_date']) + return frappe.get_all("Leave Application", filters={"docstatus": 1}, fields=['name']) def get_leave_encashment_records(): - return frappe.get_all("Leave Encashment", filters={ - "docstatus": 1 - }, fields=['name', 'employee', 'leave_type', 'encashable_days', 'encashment_date']) + return frappe.get_all("Leave Encashment", filters={"docstatus": 1}, fields=['name']) diff --git a/erpnext/patches/v12_0/rename_lost_reason_detail.py b/erpnext/patches/v12_0/rename_lost_reason_detail.py index 044d0232e0..d0dc356bd0 100644 --- a/erpnext/patches/v12_0/rename_lost_reason_detail.py +++ b/erpnext/patches/v12_0/rename_lost_reason_detail.py @@ -3,6 +3,7 @@ import frappe def execute(): if frappe.db.exists("DocType", "Lost Reason Detail"): + frappe.reload_doc("crm", "doctype", "opportunity_lost_reason") frappe.reload_doc("crm", "doctype", "opportunity_lost_reason_detail") frappe.reload_doc("setup", "doctype", "quotation_lost_reason_detail") @@ -10,8 +11,8 @@ def execute(): frappe.db.sql("""INSERT INTO `tabQuotation Lost Reason Detail` SELECT * FROM `tabLost Reason Detail` WHERE `parenttype` = 'Quotation'""") - frappe.db.sql("""INSERT INTO `tabQuotation Lost Reason` (`name`, `creation`, `modified`, `modified_by`, `owner`, `docstatus`, `parent`, `parentfield`, `parenttype`, `idx`, `_comments`, `_assign`, `_user_tags`, `_liked_by`, `order_lost_reason`) - SELECT o.`name`, o.`creation`, o.`modified`, o.`modified_by`, o.`owner`, o.`docstatus`, o.`parent`, o.`parentfield`, o.`parenttype`, o.`idx`, o.`_comments`, o.`_assign`, o.`_user_tags`, o.`_liked_by`, o.`lost_reason` + frappe.db.sql("""INSERT INTO `tabQuotation Lost Reason` (`name`, `creation`, `modified`, `modified_by`, `owner`, `docstatus`, `parent`, `parentfield`, `parenttype`, `idx`, `_comments`, `_assign`, `_user_tags`, `_liked_by`, `order_lost_reason`) + SELECT o.`name`, o.`creation`, o.`modified`, o.`modified_by`, o.`owner`, o.`docstatus`, o.`parent`, o.`parentfield`, o.`parenttype`, o.`idx`, o.`_comments`, o.`_assign`, o.`_user_tags`, o.`_liked_by`, o.`lost_reason` FROM `tabOpportunity Lost Reason` o LEFT JOIN `tabQuotation Lost Reason` q ON q.name = o.name WHERE q.name IS NULL""") - + frappe.delete_doc("DocType", "Lost Reason Detail") \ No newline at end of file diff --git a/erpnext/patches/v13_0/add_standard_navbar_items.py b/erpnext/patches/v13_0/add_standard_navbar_items.py new file mode 100644 index 0000000000..d05b258db0 --- /dev/null +++ b/erpnext/patches/v13_0/add_standard_navbar_items.py @@ -0,0 +1,7 @@ +from __future__ import unicode_literals +# import frappe +from erpnext.setup.install import add_standard_navbar_items + +def execute(): + # Add standard navbar items for ERPNext in Navbar Settings + add_standard_navbar_items() diff --git a/erpnext/patches/v13_0/change_default_pos_print_format.py b/erpnext/patches/v13_0/change_default_pos_print_format.py new file mode 100644 index 0000000000..605a29e477 --- /dev/null +++ b/erpnext/patches/v13_0/change_default_pos_print_format.py @@ -0,0 +1,8 @@ +from __future__ import unicode_literals +import frappe + +def execute(): + frappe.db.sql( + """UPDATE `tabPOS Profile` profile + SET profile.`print_format` = 'POS Invoice' + WHERE profile.`print_format` = 'Point of Sale'""") \ No newline at end of file diff --git a/erpnext/patches/v13_0/drop_razorpay_payload_column.py b/erpnext/patches/v13_0/drop_razorpay_payload_column.py new file mode 100644 index 0000000000..8980fd0039 --- /dev/null +++ b/erpnext/patches/v13_0/drop_razorpay_payload_column.py @@ -0,0 +1,7 @@ +from __future__ import unicode_literals +import frappe + +def execute(): + if frappe.db.exists("DocType", "Membership"): + if 'webhook_payload' in frappe.db.get_table_columns("Membership"): + frappe.db.sql("alter table `tabMembership` drop column webhook_payload") \ No newline at end of file diff --git a/erpnext/patches/v13_0/replace_pos_page_with_point_of_sale_page.py b/erpnext/patches/v13_0/replace_pos_page_with_point_of_sale_page.py new file mode 100644 index 0000000000..390e217cad --- /dev/null +++ b/erpnext/patches/v13_0/replace_pos_page_with_point_of_sale_page.py @@ -0,0 +1,6 @@ +from __future__ import unicode_literals +import frappe + +def execute(): + if frappe.db.exists("Page", "point-of-sale"): + frappe.rename_doc("Page", "pos", "point-of-sale", 1, 1) \ No newline at end of file diff --git a/erpnext/patches/v13_0/setting_custom_roles_for_some_regional_reports.py b/erpnext/patches/v13_0/setting_custom_roles_for_some_regional_reports.py new file mode 100644 index 0000000000..ecc7822e1d --- /dev/null +++ b/erpnext/patches/v13_0/setting_custom_roles_for_some_regional_reports.py @@ -0,0 +1,10 @@ +from __future__ import unicode_literals +import frappe +from erpnext.regional.india.setup import add_custom_roles_for_reports + +def execute(): + company = frappe.get_all('Company', filters = {'country': 'India'}) + if not company: + return + + add_custom_roles_for_reports() \ No newline at end of file diff --git a/erpnext/patches/v13_0/stock_entry_enhancements.py b/erpnext/patches/v13_0/stock_entry_enhancements.py index dcc4f956f7..0bdcc9c0e8 100644 --- a/erpnext/patches/v13_0/stock_entry_enhancements.py +++ b/erpnext/patches/v13_0/stock_entry_enhancements.py @@ -24,4 +24,8 @@ def execute(): if not frappe.db.exists('Warehouse Type', 'Transit'): doc = frappe.new_doc('Warehouse Type') doc.name = 'Transit' - doc.insert() \ No newline at end of file + doc.insert() + + frappe.reload_doc("stock", "doctype", "stock_entry_type") + frappe.delete_doc_if_exists("Stock Entry Type", "Send to Warehouse") + frappe.delete_doc_if_exists("Stock Entry Type", "Receive at Warehouse") \ No newline at end of file diff --git a/erpnext/patches/v13_0/update_start_end_date_for_old_shift_assignment.py b/erpnext/patches/v13_0/update_start_end_date_for_old_shift_assignment.py index 7c07b987f3..0f521cb57a 100644 --- a/erpnext/patches/v13_0/update_start_end_date_for_old_shift_assignment.py +++ b/erpnext/patches/v13_0/update_start_end_date_for_old_shift_assignment.py @@ -7,4 +7,7 @@ import frappe def execute(): frappe.reload_doc('hr', 'doctype', 'shift_assignment') - frappe.db.sql("update `tabShift Assignment` set end_date=date, start_date=date where date IS NOT NULL and start_date IS NULL and end_date IS NULL;") + if frappe.db.has_column('Shift Assignment', 'date'): + frappe.db.sql("""update `tabShift Assignment` + set end_date=date, start_date=date + where date IS NOT NULL and start_date IS NULL and end_date IS NULL;""") diff --git a/erpnext/payroll/doctype/additional_salary/additional_salary.py b/erpnext/payroll/doctype/additional_salary/additional_salary.py index ef174bdea2..e3dc9070ec 100644 --- a/erpnext/payroll/doctype/additional_salary/additional_salary.py +++ b/erpnext/payroll/doctype/additional_salary/additional_salary.py @@ -5,8 +5,8 @@ from __future__ import unicode_literals import frappe from frappe.model.document import Document -from frappe import _ -from frappe.utils import getdate, date_diff +from frappe import _, bold +from frappe.utils import getdate, date_diff, comma_and, formatdate class AdditionalSalary(Document): @@ -22,9 +22,37 @@ class AdditionalSalary(Document): def validate(self): self.validate_dates() + self.validate_recurring_additional_salary_overlap() if self.amount < 0: frappe.throw(_("Amount should not be less than zero.")) + def validate_recurring_additional_salary_overlap(self): + if self.is_recurring: + additional_salaries = frappe.db.sql(""" + SELECT + name + FROM `tabAdditional Salary` + WHERE + employee=%s + AND name <> %s + AND docstatus=1 + AND is_recurring=1 + AND salary_component = %s + AND to_date >= %s + AND from_date <= %s""", + (self.employee, self.name, self.salary_component, self.from_date, self.to_date), as_dict = 1) + + additional_salaries = [salary.name for salary in additional_salaries] + + if additional_salaries and len(additional_salaries): + frappe.throw(_("Additional Salary: {0} already exist for Salary Component: {1} for period {2} and {3}").format( + bold(comma_and(additional_salaries)), + bold(self.salary_component), + bold(formatdate(self.from_date)), + bold(formatdate(self.to_date) + ))) + + def validate_dates(self): date_of_joining, relieving_date = frappe.db.get_value("Employee", self.employee, ["date_of_joining", "relieving_date"]) diff --git a/erpnext/portal/doctype/homepage/homepage.js b/erpnext/portal/doctype/homepage/homepage.js index ca34d69576..c7c66e0055 100644 --- a/erpnext/portal/doctype/homepage/homepage.js +++ b/erpnext/portal/doctype/homepage/homepage.js @@ -21,34 +21,6 @@ frappe.ui.form.on('Homepage', { }); frappe.ui.form.on('Homepage Featured Product', { - item_code: function(frm, cdt, cdn) { - var featured_product = frappe.model.get_doc(cdt, cdn); - if (featured_product.item_code) { - frappe.call({ - method: 'frappe.client.get_value', - args: { - 'doctype': 'Item', - 'filters': {'name': featured_product.item_code}, - 'fieldname': [ - 'item_name', - 'web_long_description', - 'description', - 'image', - 'thumbnail' - ] - }, - callback: function(r) { - if (!r.exc) { - $.extend(featured_product, r.message); - if (r.message.web_long_description) { - featured_product.description = r.message.web_long_description; - } - frm.refresh_field('products'); - } - } - }); - } - }, view: function(frm, cdt, cdn){ var child= locals[cdt][cdn] diff --git a/erpnext/portal/doctype/homepage_featured_product/homepage_featured_product.json b/erpnext/portal/doctype/homepage_featured_product/homepage_featured_product.json index c8b4ae9b74..01c32efec9 100644 --- a/erpnext/portal/doctype/homepage_featured_product/homepage_featured_product.json +++ b/erpnext/portal/doctype/homepage_featured_product/homepage_featured_product.json @@ -1,301 +1,116 @@ { - "allow_copy": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "hash", - "beta": 0, - "creation": "2016-04-22 05:57:06.261401", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "Document", - "editable_grid": 1, + "actions": [], + "autoname": "hash", + "creation": "2016-04-22 05:57:06.261401", + "doctype": "DocType", + "document_type": "Document", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "item_code", + "col_break1", + "item_name", + "view", + "section_break_5", + "description", + "column_break_7", + "image", + "thumbnail", + "route" + ], "fields": [ { - "allow_on_submit": 0, - "bold": 1, - "collapsible": 0, - "fieldname": "item_code", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 1, - "in_list_view": 1, - "label": "Item Code", - "length": 0, - "no_copy": 0, - "oldfieldname": "item_code", - "oldfieldtype": "Link", - "options": "Item", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": "150px", - "read_only": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 1, - "set_only_once": 0, - "unique": 0, + "bold": 1, + "fieldname": "item_code", + "fieldtype": "Link", + "in_filter": 1, + "in_list_view": 1, + "label": "Item Code", + "oldfieldname": "item_code", + "oldfieldtype": "Link", + "options": "Item", + "print_width": "150px", + "reqd": 1, + "search_index": 1, "width": "150px" - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "col_break1", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "col_break1", + "fieldtype": "Column Break" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "item_name", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "label": "Item Name", - "length": 0, - "no_copy": 0, - "oldfieldname": "item_name", - "oldfieldtype": "Data", - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 1, - "print_hide_if_no_value": 0, - "print_width": "150", - "read_only": 1, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0, + "fetch_from": "item_code.item_name", + "fetch_if_empty": 1, + "fieldname": "item_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Item Name", + "oldfieldname": "item_name", + "oldfieldtype": "Data", + "print_hide": 1, + "print_width": "150", + "read_only": 1, + "reqd": 1, "width": "150" - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "view", - "fieldtype": "Button", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "label": "View", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "view", + "fieldtype": "Button", + "in_list_view": 1, + "label": "View" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "fieldname": "section_break_5", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Description", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "collapsible": 1, + "fieldname": "section_break_5", + "fieldtype": "Section Break", + "label": "Description" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "description", - "fieldtype": "Text Editor", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 1, - "in_list_view": 1, - "label": "Description", - "length": 0, - "no_copy": 0, - "oldfieldname": "description", - "oldfieldtype": "Small Text", - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": "300px", - "read_only": 1, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0, + "fetch_from": "item_code.web_long_description", + "fieldname": "description", + "fieldtype": "Text Editor", + "in_filter": 1, + "in_list_view": 1, + "label": "Description", + "oldfieldname": "description", + "oldfieldtype": "Small Text", + "print_width": "300px", "width": "300px" - }, + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "column_break_7", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "column_break_7", + "fieldtype": "Column Break" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "image", - "fieldtype": "Attach Image", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Image", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fetch_from": "item_code.website_image", + "fetch_if_empty": 1, + "fieldname": "image", + "fieldtype": "Attach Image", + "label": "Image" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "thumbnail", - "fieldtype": "Attach Image", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "Thumbnail", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "thumbnail", + "fieldtype": "Attach Image", + "hidden": 1, + "label": "Thumbnail" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "route", - "fieldtype": "Small Text", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "label": "route", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 + "fieldname": "route", + "fieldtype": "Small Text", + "label": "route", + "read_only": 1 } - ], - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2016-08-09 06:09:34.731971", - "modified_by": "Administrator", - "module": "Portal", - "name": "Homepage Featured Product", - "name_case": "", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_seen": 0 + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2020-08-25 15:27:49.573537", + "modified_by": "Administrator", + "module": "Portal", + "name": "Homepage Featured Product", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC" } \ No newline at end of file diff --git a/erpnext/portal/product_configurator/utils.py b/erpnext/portal/product_configurator/utils.py index 9eef16bed3..9ba4cdc514 100644 --- a/erpnext/portal/product_configurator/utils.py +++ b/erpnext/portal/product_configurator/utils.py @@ -13,13 +13,15 @@ def get_field_filter_data(): for f in fields: doctype = f.get_link_doctype() - # apply enable/disable filter + # apply enable/disable/show_in_website filter meta = frappe.get_meta(doctype) filters = {} if meta.has_field('enabled'): filters['enabled'] = 1 if meta.has_field('disabled'): filters['disabled'] = 0 + if meta.has_field('show_in_website'): + filters['show_in_website'] = 1 values = [d.name for d in frappe.get_all(doctype, filters)] filter_data.append([f, values]) diff --git a/erpnext/public/js/communication.js b/erpnext/public/js/communication.js index 9432d42175..26e5ab8b32 100644 --- a/erpnext/public/js/communication.js +++ b/erpnext/public/js/communication.js @@ -7,7 +7,7 @@ frappe.ui.form.on("Communication", { }, setup_custom_buttons: (frm) => { - let confirm_msg = "Are you sure you want to create {0} from this email"; + let confirm_msg = "Are you sure you want to create {0} from this email?"; if(frm.doc.reference_doctype !== "Issue") { frm.add_custom_button(__("Issue"), () => { frappe.confirm(__(confirm_msg, [__("Issue")]), () => { @@ -62,17 +62,36 @@ frappe.ui.form.on("Communication", { }, make_opportunity_from_communication: (frm) => { - return frappe.call({ - method: "erpnext.crm.doctype.opportunity.opportunity.make_opportunity_from_communication", - args: { - communication: frm.doc.name - }, - freeze: true, - callback: (r) => { - if(r.message) { - frm.reload_doc() + const fields = [{ + fieldtype: 'Link', + label: __('Select a Company'), + fieldname: 'company', + options: 'Company', + reqd: 1, + default: frappe.defaults.get_user_default("Company") + }]; + + frappe.prompt(fields, data => { + frappe.call({ + method: "erpnext.crm.doctype.opportunity.opportunity.make_opportunity_from_communication", + args: { + communication: frm.doc.name, + company: data.company + }, + freeze: true, + callback: (r) => { + if(r.message) { + frm.reload_doc(); + frappe.show_alert({ + message: __("Opportunity {0} created", + ['' + r.message + '']), + indicator: 'green' + }); + } } - } - }) + }); + }, + 'Create an Opportunity', + 'Create'); } -}); \ No newline at end of file +}); diff --git a/erpnext/public/js/conf.js b/erpnext/public/js/conf.js index 9870f81910..2af9140f9e 100644 --- a/erpnext/public/js/conf.js +++ b/erpnext/public/js/conf.js @@ -3,32 +3,6 @@ frappe.provide('erpnext'); -// add toolbar icon -$(document).bind('toolbar_setup', function() { - frappe.app.name = "ERPNext"; - - frappe.help_feedback_link = '

Feedback

' - - - $('[data-link="docs"]').attr("href", "https://erpnext.com/docs") - $('[data-link="issues"]').attr("href", "https://github.com/frappe/erpnext/issues") - - - // default documentation goes to erpnext - // $('[data-link-type="documentation"]').attr('data-path', '/erpnext/manual/index'); - - // additional help links for erpnext - var $help_menu = $('.dropdown-help ul .documentation-links'); - $('
  • '+__('Documentation')+'
  • ').insertBefore($help_menu); - $('
  • '+__('User Forum')+'
  • ').insertBefore($help_menu); - $('
  • '+__('Report an Issue')+'
  • ').insertBefore($help_menu); - -}); - // preferred modules for breadcrumbs $.extend(frappe.breadcrumbs.preferred, { "Item Group": "Stock", diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js index a4cc68b3e2..cb76c87b62 100644 --- a/erpnext/public/js/controllers/buying.js +++ b/erpnext/public/js/controllers/buying.js @@ -503,11 +503,11 @@ erpnext.buying.get_items_from_product_bundle = function(frm) { if(!r.exc && r.message) { remove_empty_first_row(frm); - for ( var i=0; i< r.message.length; i++ ) { + for (var i=0; i< r.message.length; i++) { var d = frm.add_child("items"); var item = r.message[i]; - for ( var key in item) { - if ( !is_null(item[key]) ) { + for (var key in item) { + if (!is_null(item[key]) && key !== "doctype") { d[key] = item[key]; } } diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 3bc70216db..be30086d34 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -673,23 +673,14 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ ); } - frappe.db.get_value('Sales Invoice Payment', {'parent': this.frm.doc.pos_profile, 'default': 1}, - ['mode_of_payment', 'account', 'type'], (value) => { - if (this.frm.is_dirty()) { - frappe.model.clear_table(this.frm.doc, 'payments'); - if (value) { - let row = frappe.model.add_child(this.frm.doc, 'Sales Invoice Payment', 'payments'); - row.mode_of_payment = value.mode_of_payment; - row.type = value.type; - row.account = value.account; - row.default = 1; - row.amount = total_amount_to_pay; - } else { - this.frm.set_value('is_pos', 1); - } - this.frm.refresh_fields(); - } - }, 'Sales Invoice'); + this.frm.doc.payments.find(pay => { + if (pay.default) { + pay.amount = total_amount_to_pay; + } else { + pay.amount = 0.0 + } + }); + this.frm.refresh_fields(); this.calculate_paid_amount(); }, diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 436a232a55..792235f7a3 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -781,10 +781,23 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ else var date = this.frm.doc.transaction_date; if (frappe.meta.get_docfield(this.frm.doctype, "shipping_address") && - in_list(['Purchase Order', 'Purchase Receipt', 'Purchase Invoice'], this.frm.doctype)){ + in_list(['Purchase Order', 'Purchase Receipt', 'Purchase Invoice'], this.frm.doctype)) { erpnext.utils.get_shipping_address(this.frm, function(){ set_party_account(set_pricing); }) + + // Get default company billing address in Purchase Invoice, Order and Receipt + frappe.call({ + 'method': 'frappe.contacts.doctype.address.address.get_default_address', + 'args': { + 'doctype': 'Company', + 'name': this.frm.doc.company + }, + 'callback': function(r) { + me.frm.set_value('billing_address', r.message); + } + }); + } else { set_party_account(set_pricing); } diff --git a/erpnext/public/less/products.less b/erpnext/public/less/products.less index 79f57b331a..5e744ceac5 100644 --- a/erpnext/public/less/products.less +++ b/erpnext/public/less/products.less @@ -22,6 +22,8 @@ } .filter-options { + margin-left: -5px; + padding-left: 5px; max-height: 300px; overflow: auto; } diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index 290694a789..cbcd6e3203 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -73,6 +73,19 @@ def add_custom_roles_for_reports(): ] )).insert() + for report_name in ('HSN-wise-summary of outward supplies', 'GSTR-1', 'GSTR-2'): + + if not frappe.db.get_value('Custom Role', dict(report=report_name)): + frappe.get_doc(dict( + doctype='Custom Role', + report=report_name, + roles= [ + dict(role='Accounts User'), + dict(role='Accounts Manager'), + dict(role='Auditor') + ] + )).insert() + def add_permissions(): for doctype in ('GST HSN Code', 'GST Settings', 'GSTR 3B Report', 'Lower Deduction Certificate'): add_permission(doctype, 'All', 0) diff --git a/erpnext/regional/india/taxes.js b/erpnext/regional/india/taxes.js index fbccc6b078..3b6a28f52c 100644 --- a/erpnext/regional/india/taxes.js +++ b/erpnext/regional/india/taxes.js @@ -6,6 +6,9 @@ erpnext.setup_auto_gst_taxation = (doctype) => { shipping_address: function(frm) { frm.trigger('get_tax_template'); }, + supplier_address: function(frm) { + frm.trigger('get_tax_template'); + }, tax_category: function(frm) { frm.trigger('get_tax_template'); }, diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 2c81748c86..69e47a43c4 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -674,6 +674,9 @@ def update_grand_total_for_rcm(doc, method): if country != 'India': return + if not doc.total_taxes_and_charges: + return + if doc.reverse_charge == 'Y': gst_accounts = get_gst_accounts(doc.company) gst_account_list = gst_accounts.get('cgst_account') + gst_accounts.get('sgst_account') \ @@ -721,7 +724,7 @@ def make_regional_gl_entries(gl_entries, doc): country = frappe.get_cached_value('Company', doc.company, 'country') if country != 'India': - return + return gl_entries if doc.reverse_charge == 'Y': gst_accounts = get_gst_accounts(doc.company) @@ -732,6 +735,7 @@ def make_regional_gl_entries(gl_entries, doc): if tax.category not in ("Total", "Valuation and Total"): continue + dr_or_cr = "credit" if tax.add_deduct_tax == "Add" else "debit" if flt(tax.base_tax_amount_after_discount_amount) and tax.account_head in gst_account_list: account_currency = get_account_currency(tax.account_head) @@ -741,8 +745,8 @@ def make_regional_gl_entries(gl_entries, doc): "cost_center": tax.cost_center, "posting_date": doc.posting_date, "against": doc.supplier, - "credit": tax.base_tax_amount_after_discount_amount, - "credits_in_account_currency": tax.base_tax_amount_after_discount_amount \ + dr_or_cr: tax.base_tax_amount_after_discount_amount, + dr_or_cr + "_in_account_currency": tax.base_tax_amount_after_discount_amount \ if account_currency==doc.company_currency \ else tax.tax_amount_after_discount_amount }, account_currency, item=tax) diff --git a/erpnext/regional/report/gstr_1/gstr_1.json b/erpnext/regional/report/gstr_1/gstr_1.json index 2012bb8840..75aed8cffc 100644 --- a/erpnext/regional/report/gstr_1/gstr_1.json +++ b/erpnext/regional/report/gstr_1/gstr_1.json @@ -7,7 +7,7 @@ "doctype": "Report", "idx": 0, "is_standard": "Yes", - "modified": "2019-06-30 19:33:59.769385", + "modified": "2019-09-03 19:33:59.769385", "modified_by": "Administrator", "module": "Regional", "name": "GSTR-1", @@ -16,15 +16,5 @@ "ref_doctype": "GL Entry", "report_name": "GSTR-1", "report_type": "Script Report", - "roles": [ - { - "role": "Accounts User" - }, - { - "role": "Accounts Manager" - }, - { - "role": "Auditor" - } - ] + "roles": [] } \ No newline at end of file diff --git a/erpnext/regional/report/gstr_2/gstr_2.json b/erpnext/regional/report/gstr_2/gstr_2.json index 929ed914d8..b70d0f9416 100644 --- a/erpnext/regional/report/gstr_2/gstr_2.json +++ b/erpnext/regional/report/gstr_2/gstr_2.json @@ -1,29 +1,19 @@ { - "add_total_row": 0, - "apply_user_permissions": 1, - "creation": "2018-01-29 12:59:55.650445", - "disabled": 0, - "docstatus": 0, - "doctype": "Report", - "idx": 0, - "is_standard": "Yes", - "modified": "2018-01-29 12:59:55.650445", - "modified_by": "Administrator", - "module": "Regional", - "name": "GSTR-2", - "owner": "Administrator", - "ref_doctype": "GL Entry", - "report_name": "GSTR-2", - "report_type": "Script Report", - "roles": [ - { - "role": "Accounts User" - }, - { - "role": "Accounts Manager" - }, - { - "role": "Auditor" - } - ] + "add_total_row": 0, + "apply_user_permissions": 1, + "creation": "2018-01-29 12:59:55.650445", + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 0, + "is_standard": "Yes", + "modified": "2018-09-03 12:59:55.650445", + "modified_by": "Administrator", + "module": "Regional", + "name": "GSTR-2", + "owner": "Administrator", + "ref_doctype": "GL Entry", + "report_name": "GSTR-2", + "report_type": "Script Report", + "roles": [] } \ No newline at end of file diff --git a/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.js b/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.js index dfdf9dc095..b757d53aa2 100644 --- a/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.js +++ b/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.js @@ -46,5 +46,28 @@ frappe.query_reports["HSN-wise-summary of outward supplies"] = { ], onload: (report) => { fetch_gstins(report); + + report.page.add_inner_button(__("Download JSON"), function () { + var filters = report.get_values(); + + frappe.call({ + method: 'erpnext.regional.report.hsn_wise_summary_of_outward_supplies.hsn_wise_summary_of_outward_supplies.get_json', + args: { + data: report.data, + report_name: report.report_name, + filters: filters + }, + callback: function(r) { + if (r.message) { + const args = { + cmd: 'erpnext.regional.report.hsn_wise_summary_of_outward_supplies.hsn_wise_summary_of_outward_supplies.download_json_file', + data: r.message.data, + report_name: r.message.report_name + }; + open_url_post(frappe.request.url, args); + } + } + }); + }); } }; diff --git a/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.json b/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.json index 124a720134..cc6ad574af 100644 --- a/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.json +++ b/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.json @@ -1,28 +1,18 @@ { - "add_total_row": 0, - "creation": "2018-04-26 10:49:29.159400", - "disabled": 0, - "docstatus": 0, - "doctype": "Report", - "idx": 0, - "is_standard": "Yes", - "modified": "2019-04-26 12:59:38.603649", - "modified_by": "Administrator", - "module": "Regional", - "name": "HSN-wise-summary of outward supplies", - "owner": "Administrator", - "ref_doctype": "Sales Invoice", - "report_name": "HSN-wise-summary of outward supplies", - "report_type": "Script Report", - "roles": [ - { - "role": "Accounts User" - }, - { - "role": "Accounts Manager" - }, - { - "role": "Auditor" - } - ] + "add_total_row": 0, + "creation": "2018-04-26 10:49:29.159400", + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 0, + "is_standard": "Yes", + "modified": "2019-09-03 12:59:38.603649", + "modified_by": "Administrator", + "module": "Regional", + "name": "HSN-wise-summary of outward supplies", + "owner": "Administrator", + "ref_doctype": "Sales Invoice", + "report_name": "HSN-wise-summary of outward supplies", + "report_type": "Script Report", + "roles": [] } \ No newline at end of file diff --git a/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py b/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py index a3ed4cebb1..59389ce326 100644 --- a/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py +++ b/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py @@ -4,11 +4,13 @@ from __future__ import unicode_literals import frappe, erpnext from frappe import _ -from frappe.utils import flt +from frappe.utils import flt, getdate, cstr from frappe.model.meta import get_field_precision from frappe.utils.xlsxutils import handle_html from six import iteritems import json +from erpnext.regional.india.utils import get_gst_accounts +from erpnext.regional.report.gstr_1.gstr_1 import get_company_gstin_number def execute(filters=None): return _execute(filters) @@ -141,7 +143,7 @@ def get_tax_accounts(item_list, columns, company_currency, doctype="Sales Invoic tax_details = frappe.db.sql(""" select - parent, description, item_wise_tax_detail, + parent, account_head, item_wise_tax_detail, base_tax_amount_after_discount_amount from `tab%s` where @@ -153,11 +155,11 @@ def get_tax_accounts(item_list, columns, company_currency, doctype="Sales Invoic """ % (tax_doctype, '%s', ', '.join(['%s']*len(invoice_item_row)), conditions), tuple([doctype] + list(invoice_item_row))) - for parent, description, item_wise_tax_detail, tax_amount in tax_details: - description = handle_html(description) - if description not in tax_columns and tax_amount: + for parent, account_head, item_wise_tax_detail, tax_amount in tax_details: + + if account_head not in tax_columns and tax_amount: # as description is text editor earlier and markup can break the column convention in reports - tax_columns.append(description) + tax_columns.append(account_head) if item_wise_tax_detail: try: @@ -175,17 +177,17 @@ def get_tax_accounts(item_list, columns, company_currency, doctype="Sales Invoic for d in item_row_map.get(parent, {}).get(item_code, []): item_tax_amount = tax_amount if item_tax_amount: - itemised_tax.setdefault((parent, item_code), {})[description] = frappe._dict({ + itemised_tax.setdefault((parent, item_code), {})[account_head] = frappe._dict({ "tax_amount": flt(item_tax_amount, tax_amount_precision) }) except ValueError: continue tax_columns.sort() - for desc in tax_columns: + for account_head in tax_columns: columns.append({ - "label": desc, - "fieldname": frappe.scrub(desc), + "label": account_head, + "fieldname": frappe.scrub(account_head), "fieldtype": "Float", "width": 110 }) @@ -212,3 +214,76 @@ def get_merged_data(columns, data): return result +@frappe.whitelist() +def get_json(filters, report_name, data): + filters = json.loads(filters) + report_data = json.loads(data) + gstin = filters.get('company_gstin') or get_company_gstin_number(filters["company"]) + + if not filters.get('from_date') or not filters.get('to_date'): + frappe.throw(_("Please enter From Date and To Date to generate JSON")) + + fp = "%02d%s" % (getdate(filters["to_date"]).month, getdate(filters["to_date"]).year) + + gst_json = {"version": "GST2.3.4", + "hash": "hash", "gstin": gstin, "fp": fp} + + gst_json["hsn"] = { + "data": get_hsn_wise_json_data(filters, report_data) + } + + return { + 'report_name': report_name, + 'data': gst_json + } + +@frappe.whitelist() +def download_json_file(): + '''download json content in a file''' + data = frappe._dict(frappe.local.form_dict) + frappe.response['filename'] = frappe.scrub("{0}".format(data['report_name'])) + '.json' + frappe.response['filecontent'] = data['data'] + frappe.response['content_type'] = 'application/json' + frappe.response['type'] = 'download' + +def get_hsn_wise_json_data(filters, report_data): + + filters = frappe._dict(filters) + gst_accounts = get_gst_accounts(filters.company) + data = [] + count = 1 + + for hsn in report_data: + row = { + "num": count, + "hsn_sc": hsn.get("gst_hsn_code"), + "desc": hsn.get("description"), + "uqc": hsn.get("stock_uom").upper(), + "qty": hsn.get("stock_qty"), + "val": flt(hsn.get("total_amount"), 2), + "txval": flt(hsn.get("taxable_amount", 2)), + "iamt": 0.0, + "camt": 0.0, + "samt": 0.0, + "csamt": 0.0 + + } + + for account in gst_accounts.get('igst_account'): + row['iamt'] += flt(hsn.get(frappe.scrub(cstr(account)), 0.0), 2) + + for account in gst_accounts.get('cgst_account'): + row['camt'] += flt(hsn.get(frappe.scrub(cstr(account)), 0.0), 2) + + for account in gst_accounts.get('sgst_account'): + row['samt'] += flt(hsn.get(frappe.scrub(cstr(account)), 0.0), 2) + + for account in gst_accounts.get('cess_account'): + row['csamt'] += flt(hsn.get(frappe.scrub(cstr(account)), 0.0), 2) + + data.append(row) + count +=1 + + return data + + diff --git a/erpnext/regional/report/irs_1099/irs_1099.py b/erpnext/regional/report/irs_1099/irs_1099.py index 67834d1221..d3509e500f 100644 --- a/erpnext/regional/report/irs_1099/irs_1099.py +++ b/erpnext/regional/report/irs_1099/irs_1099.py @@ -16,9 +16,15 @@ from frappe.utils.jinja import render_template def execute(filters=None): filters = filters if isinstance(filters, _dict) else _dict(filters) + if not filters: filters.setdefault('fiscal_year', get_fiscal_year(nowdate())[0]) filters.setdefault('company', frappe.db.get_default("company")) + + region = frappe.db.get_value("Company", fieldname = ["country"], filters = { "name": filters.company }) + if region != 'United States': + return [],[] + data = [] columns = get_columns() data = frappe.db.sql(""" diff --git a/erpnext/regional/united_states/test_united_states.py b/erpnext/regional/united_states/test_united_states.py index 688f14576c..ad95010a9a 100644 --- a/erpnext/regional/united_states/test_united_states.py +++ b/erpnext/regional/united_states/test_united_states.py @@ -24,7 +24,7 @@ class TestUnitedStates(unittest.TestCase): def test_irs_1099_report(self): make_payment_entry_to_irs_1099_supplier() - filters = frappe._dict({"fiscal_year": "_Test Fiscal Year 2016", "company": "_Test Company"}) + filters = frappe._dict({"fiscal_year": "_Test Fiscal Year 2016", "company": "_Test Company 1"}) columns, data = execute_1099_report(filters) print(columns, data) expected_row = {'supplier': '_US 1099 Test Supplier', @@ -42,10 +42,10 @@ def make_payment_entry_to_irs_1099_supplier(): pe = frappe.new_doc("Payment Entry") pe.payment_type = "Pay" - pe.company = "_Test Company" + pe.company = "_Test Company 1" pe.posting_date = "2016-01-10" - pe.paid_from = "_Test Bank USD - _TC" - pe.paid_to = "_Test Payable USD - _TC" + pe.paid_from = "_Test Bank USD - _TC1" + pe.paid_to = "_Test Payable USD - _TC1" pe.paid_amount = 100 pe.received_amount = 100 pe.reference_no = "For IRS 1099 testing" diff --git a/erpnext/selling/desk_page/retail/retail.json b/erpnext/selling/desk_page/retail/retail.json index 7b30af20cc..c4ddf26a90 100644 --- a/erpnext/selling/desk_page/retail/retail.json +++ b/erpnext/selling/desk_page/retail/retail.json @@ -2,8 +2,18 @@ "cards": [ { "hidden": 0, - "label": "Retail Operations", - "links": "[\n {\n \"description\": \"Setup default values for POS Invoices\",\n \"label\": \"Point-of-Sale Profile\",\n \"name\": \"POS Profile\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"POS Profile\"\n ],\n \"description\": \"Point of Sale\",\n \"label\": \"POS\",\n \"name\": \"pos\",\n \"onboard\": 1,\n \"type\": \"page\"\n },\n {\n \"description\": \"Cashier Closing\",\n \"label\": \"Cashier Closing\",\n \"name\": \"Cashier Closing\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Setup mode of POS (Online / Offline)\",\n \"label\": \"POS Settings\",\n \"name\": \"POS Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"To make Customer based incentive schemes.\",\n \"label\": \"Loyalty Program\",\n \"name\": \"Loyalty Program\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"To view logs of Loyalty Points assigned to a Customer.\",\n \"label\": \"Loyalty Point Entry\",\n \"name\": \"Loyalty Point Entry\",\n \"type\": \"doctype\"\n }\n]" + "label": "Settings & Configurations", + "links": "[\n {\n \"description\": \"Setup default values for POS Invoices\",\n \"label\": \"Point-of-Sale Profile\",\n \"name\": \"POS Profile\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"POS Settings\",\n \"name\": \"POS Settings\",\n \"type\": \"doctype\"\n }\n]" + }, + { + "hidden": 0, + "label": "Loyalty Program", + "links": "[\n {\n \"description\": \"To make Customer based incentive schemes.\",\n \"label\": \"Loyalty Program\",\n \"name\": \"Loyalty Program\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"To view logs of Loyalty Points assigned to a Customer.\",\n \"label\": \"Loyalty Point Entry\",\n \"name\": \"Loyalty Point Entry\",\n \"type\": \"doctype\"\n }\n]" + }, + { + "hidden": 0, + "label": "Opening & Closing", + "links": "[\n {\n \"label\": \"POS Opening Entry\",\n \"name\": \"POS Opening Entry\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"POS Closing Entry\",\n \"name\": \"POS Closing Entry\",\n \"type\": \"doctype\"\n }\n]" } ], "category": "Domains", @@ -14,10 +24,11 @@ "docstatus": 0, "doctype": "Desk Page", "extends_another_page": 0, + "hide_custom": 0, "idx": 0, "is_standard": 1, "label": "Retail", - "modified": "2020-04-26 22:42:39.346750", + "modified": "2020-09-09 11:46:28.297435", "modified_by": "Administrator", "module": "Selling", "name": "Retail", @@ -25,5 +36,12 @@ "pin_to_bottom": 0, "pin_to_top": 0, "restrict_to_domain": "Retail", - "shortcuts": [] + "shortcuts": [ + { + "doc_view": "", + "label": "Point Of Sale", + "link_to": "point-of-sale", + "type": "Page" + } + ] } \ No newline at end of file diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index 93d4832173..1f955fcd52 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -184,10 +184,10 @@ class Customer(TransactionBase): def validate_credit_limit_on_change(self): if self.get("__islocal") or not self.credit_limits: return - + past_credit_limits = [d.credit_limit for d in frappe.db.get_all("Customer Credit Limit", filters={'parent': self.name}, fields=["credit_limit"], order_by="company")] - + current_credit_limits = [d.credit_limit for d in sorted(self.credit_limits, key=lambda k: k.company)] if past_credit_limits == current_credit_limits: @@ -396,13 +396,12 @@ def check_credit_limit(customer, company, ignore_outstanding_sales_order=False, credit_controller_users = get_users_with_role(credit_controller_role or "Sales Master Manager") # form a list of emails and names to show to the user - credit_controller_users = [get_formatted_email(user).replace("<", "(").replace(">", ")") for user in credit_controller_users] - - if not credit_controller_users: + credit_controller_users_formatted = [get_formatted_email(user).replace("<", "(").replace(">", ")") for user in credit_controller_users] + if not credit_controller_users_formatted: frappe.throw(_("Please contact your administrator to extend the credit limits for {0}.".format(customer))) message = """Please contact any of the following users to extend the credit limits for {0}: -

    """.format(customer, '
  • '.join(credit_controller_users)) +

    """.format(customer, '
  • '.join(credit_controller_users_formatted)) # if the current user does not have permissions to override credit limit, # prompt them to send out an email to the controller users @@ -427,7 +426,7 @@ def send_emails(args): subject = (_("Credit limit reached for customer {0}").format(args.get('customer'))) message = (_("Credit limit has been crossed for customer {0} ({1}/{2})") .format(args.get('customer'), args.get('customer_outstanding'), args.get('credit_limit'))) - frappe.sendmail(recipients=[args.get('credit_controller_users_list')], subject=subject, message=message) + frappe.sendmail(recipients=args.get('credit_controller_users_list'), subject=subject, message=message) def get_customer_outstanding(customer, company, ignore_outstanding_sales_order=False, cost_center=None): # Outstanding based on GL Entries diff --git a/erpnext/selling/doctype/customer/customer_dashboard.py b/erpnext/selling/doctype/customer/customer_dashboard.py index 09e474dc2e..532c11b86e 100644 --- a/erpnext/selling/doctype/customer/customer_dashboard.py +++ b/erpnext/selling/doctype/customer/customer_dashboard.py @@ -33,7 +33,7 @@ def get_data(): }, { 'label': _('Support'), - 'items': ['Issue'] + 'items': ['Issue', 'Maintenance Visit', 'Installation Note', 'Warranty Claim'] }, { 'label': _('Projects'), diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index ab095ebfe0..20ae19f5db 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -285,9 +285,17 @@ def _make_customer(source_name, ignore_permissions=False): return customer else: raise - except frappe.MandatoryError: + except frappe.MandatoryError as e: + mandatory_fields = e.args[0].split(':')[1].split(',') + mandatory_fields = [customer.meta.get_label(field.strip()) for field in mandatory_fields] + frappe.local.message_log = [] - frappe.throw(_("Please create Customer from Lead {0}").format(lead_name)) + lead_link = frappe.utils.get_link_to_form("Lead", lead_name) + message = _("Could not auto create Customer due to the following missing mandatory field(s):") + "
    " + message += "
    " + message += _("Please create Customer from Lead {0}.").format(lead_link) + + frappe.throw(message, title=_("Mandatory Missing")) else: return customer_name else: diff --git a/erpnext/selling/doctype/sales_order/sales_order_dashboard.py b/erpnext/selling/doctype/sales_order/sales_order_dashboard.py index 4126bc6a70..05a760de27 100644 --- a/erpnext/selling/doctype/sales_order/sales_order_dashboard.py +++ b/erpnext/selling/doctype/sales_order/sales_order_dashboard.py @@ -10,6 +10,7 @@ def get_data(): 'Payment Entry': 'reference_name', 'Payment Request': 'reference_name', 'Auto Repeat': 'reference_document', + 'Maintenance Visit': 'prevdoc_docname' }, 'internal_links': { 'Quotation': ['items', 'prevdoc_docname'] @@ -17,7 +18,7 @@ def get_data(): 'transactions': [ { 'label': _('Fulfillment'), - 'items': ['Sales Invoice', 'Pick List', 'Delivery Note'] + 'items': ['Sales Invoice', 'Pick List', 'Delivery Note', 'Maintenance Visit'] }, { 'label': _('Purchasing'), diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index accaa9c3b3..34aaf08395 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -417,7 +417,42 @@ class TestSalesOrder(unittest.TestCase): # add new item trans_item = json.dumps([{'item_code' : '_Test Item', 'rate' : 100, 'qty' : 2}]) self.assertRaises(frappe.ValidationError, update_child_qty_rate,'Sales Order', trans_item, so.name) + test_user.remove_roles("Accounts User") frappe.set_user("Administrator") + + def test_update_child_qty_rate_with_workflow(self): + from frappe.model.workflow import apply_workflow + + workflow = make_sales_order_workflow() + so = make_sales_order(item_code= "_Test Item", qty=1, rate=150, do_not_submit=1) + apply_workflow(so, 'Approve') + + frappe.set_user("Administrator") + user = 'test@example.com' + test_user = frappe.get_doc('User', user) + test_user.add_roles("Sales User", "Test Junior Approver") + frappe.set_user(user) + + # user shouldn't be able to edit since grand_total will become > 200 if qty is doubled + trans_item = json.dumps([{'item_code' : '_Test Item', 'rate' : 150, 'qty' : 2, 'docname': so.items[0].name}]) + self.assertRaises(frappe.ValidationError, update_child_qty_rate, 'Sales Order', trans_item, so.name) + + frappe.set_user("Administrator") + user2 = 'test2@example.com' + test_user2 = frappe.get_doc('User', user2) + test_user2.add_roles("Sales User", "Test Approver") + frappe.set_user(user2) + + # Test Approver is allowed to edit with grand_total > 200 + update_child_qty_rate("Sales Order", trans_item, so.name) + so.reload() + self.assertEqual(so.items[0].qty, 2) + + frappe.set_user("Administrator") + test_user.remove_roles("Sales User", "Test Junior Approver", "Test Approver") + test_user2.remove_roles("Sales User", "Test Junior Approver", "Test Approver") + workflow.is_active = 0 + workflow.save() def test_update_child_qty_rate_product_bundle(self): # test Update Items with product bundle @@ -973,3 +1008,37 @@ def get_reserved_qty(item_code="_Test Item", warehouse="_Test Warehouse - _TC"): "reserved_qty")) test_dependencies = ["Currency Exchange"] + +def make_sales_order_workflow(): + if frappe.db.exists('Workflow', 'SO Test Workflow'): + doc = frappe.get_doc("Workflow", "SO Test Workflow") + doc.set("is_active", 1) + doc.save() + return doc + + frappe.get_doc(dict(doctype='Role', role_name='Test Junior Approver')).insert(ignore_if_duplicate=True) + frappe.get_doc(dict(doctype='Role', role_name='Test Approver')).insert(ignore_if_duplicate=True) + frappe.db.commit() + frappe.cache().hdel('roles', frappe.session.user) + + workflow = frappe.get_doc({ + "doctype": "Workflow", + "workflow_name": "SO Test Workflow", + "document_type": "Sales Order", + "workflow_state_field": "workflow_state", + "is_active": 1, + "send_email_alert": 0, + }) + workflow.append('states', dict( state = 'Pending', allow_edit = 'All' )) + workflow.append('states', dict( state = 'Approved', allow_edit = 'Test Approver', doc_status = 1 )) + workflow.append('transitions', dict( + state = 'Pending', action = 'Approve', next_state = 'Approved', allowed = 'Test Junior Approver', allow_self_approval = 1, + condition = 'doc.grand_total < 200' + )) + workflow.append('transitions', dict( + state = 'Pending', action = 'Approve', next_state = 'Approved', allowed = 'Test Approver', allow_self_approval = 1, + condition = 'doc.grand_total > 200' + )) + workflow.insert(ignore_permissions=True) + + return workflow \ No newline at end of file diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.js b/erpnext/selling/page/point_of_sale/point_of_sale.js index 2ce0b270f9..8d4ac78422 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.js +++ b/erpnext/selling/page/point_of_sale/point_of_sale.js @@ -9,7 +9,7 @@ frappe.pages['point-of-sale'].on_page_load = function(wrapper) { title: __('Point of Sale'), single_column: true }); - // online + wrapper.pos = new erpnext.PointOfSale.Controller(wrapper); window.cur_pos = wrapper.pos; }; \ No newline at end of file diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js index ae5471b900..5018254b0a 100644 --- a/erpnext/selling/page/point_of_sale/pos_controller.js +++ b/erpnext/selling/page/point_of_sale/pos_controller.js @@ -8,7 +8,7 @@ {% include "erpnext/selling/page/point_of_sale/pos_past_order_summary.js" %} erpnext.PointOfSale.Controller = class { - constructor(wrapper) { + constructor(wrapper) { this.wrapper = $(wrapper).find('.layout-main-section'); this.page = wrapper.page; @@ -36,7 +36,7 @@ erpnext.PointOfSale.Controller = class { const table_fields = [ { fieldname: "mode_of_payment", fieldtype: "Link", in_list_view: 1, label: "Mode of Payment", options: "Mode of Payment", reqd: 1 }, { fieldname: "opening_amount", fieldtype: "Currency", default: 0, in_list_view: 1, label: "Opening Amount", - options: "company:company_currency", reqd: 1 } + options: "company:company_currency" } ]; const dialog = new frappe.ui.Dialog({ @@ -51,29 +51,16 @@ erpnext.PointOfSale.Controller = class { options: 'POS Profile', fieldname: 'pos_profile', reqd: 1, onchange: () => { const pos_profile = dialog.fields_dict.pos_profile.get_value(); - const company = dialog.fields_dict.company.get_value(); - const user = frappe.session.user - if (!pos_profile || !company || !user) return; + if (!pos_profile) return; - // auto fetch last closing entry's balance details - frappe.db.get_list("POS Closing Entry", { - filters: { company, pos_profile, user }, - limit: 1, - order_by: 'period_end_date desc' - }).then((res) => { - if (!res.length) return; - const pos_closing_entry = res[0]; - frappe.db.get_doc("POS Closing Entry", pos_closing_entry.name).then(({ payment_reconciliation }) => { - dialog.fields_dict.balance_details.df.data = []; - payment_reconciliation.forEach(pay => { - const { mode_of_payment } = pay; - dialog.fields_dict.balance_details.df.data.push({ - mode_of_payment: mode_of_payment - }); - }); - dialog.fields_dict.balance_details.grid.refresh(); + frappe.db.get_doc("POS Profile", pos_profile).then(doc => { + dialog.fields_dict.balance_details.df.data = []; + doc.payments.forEach(pay => { + const { mode_of_payment } = pay; + dialog.fields_dict.balance_details.df.data.push({ mode_of_payment }); }); + dialog.fields_dict.balance_details.grid.refresh(); }); } }, diff --git a/erpnext/selling/page/point_of_sale/pos_item_cart.js b/erpnext/selling/page/point_of_sale/pos_item_cart.js index c23a6ad58f..724b60b973 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_cart.js +++ b/erpnext/selling/page/point_of_sale/pos_item_cart.js @@ -1,36 +1,36 @@ erpnext.PointOfSale.ItemCart = class { - constructor({ wrapper, events }) { + constructor({ wrapper, events }) { this.wrapper = wrapper; this.events = events; - this.customer_info = undefined; - - this.init_component(); - } - - init_component() { - this.prepare_dom(); - this.init_child_components(); + this.customer_info = undefined; + + this.init_component(); + } + + init_component() { + this.prepare_dom(); + this.init_child_components(); this.bind_events(); this.attach_shortcuts(); - } + } - prepare_dom() { + prepare_dom() { this.wrapper.append( - `
    ` - ) + `
    ` + ) - this.$component = this.wrapper.find('.item-cart'); - } + this.$component = this.wrapper.find('.item-cart'); + } - init_child_components() { - this.init_customer_selector(); - this.init_cart_components(); - } + init_child_components() { + this.init_customer_selector(); + this.init_cart_components(); + } - init_customer_selector() { + init_customer_selector() { this.$component.append( - `
    ` - ) + `
    ` + ) this.$customer_section = this.$component.find('.customer-section'); } @@ -41,9 +41,9 @@ erpnext.PointOfSale.ItemCart = class { this.make_customer_selector(); this.customer_field.set_focus(); } - - init_cart_components() { - this.$component.append( + + init_cart_components() { + this.$component.append( `
    @@ -55,23 +55,23 @@ erpnext.PointOfSale.ItemCart = class {
    -
    ` - ); +
    ` + ); this.$cart_container = this.$component.find('.cart-container'); this.make_cart_totals_section(); this.make_cart_items_section(); - this.make_cart_numpad(); - } + this.make_cart_numpad(); + } - make_cart_items_section() { - this.$cart_header = this.$component.find('.cart-header'); - this.$cart_items_wrapper = this.$component.find('.cart-items-section'); + make_cart_items_section() { + this.$cart_header = this.$component.find('.cart-header'); + this.$cart_items_wrapper = this.$component.find('.cart-items-section'); this.make_no_items_placeholder(); - } - - make_no_items_placeholder() { + } + + make_no_items_placeholder() { this.$cart_header.addClass('d-none'); this.$cart_items_wrapper.html( `
    @@ -81,8 +81,8 @@ erpnext.PointOfSale.ItemCart = class { this.$cart_items_wrapper.addClass('mt-4 border-grey border-dashed'); } - make_cart_totals_section() { - this.$totals_section = this.$component.find('.cart-totals-section'); + make_cart_totals_section() { + this.$totals_section = this.$component.find('.cart-totals-section'); this.$totals_section.append( `
    @@ -116,9 +116,9 @@ erpnext.PointOfSale.ItemCart = class { ) this.$add_discount_elem = this.$component.find(".add-discount"); - } - - make_cart_numpad() { + } + + make_cart_numpad() { this.$numpad_section = this.$component.find('.numpad-section'); this.number_pad = new erpnext.PointOfSale.NumberPad({ @@ -155,9 +155,9 @@ erpnext.PointOfSale.ItemCart = class { Checkout
    ` ) - } - - bind_events() { + } + + bind_events() { const me = this; this.$customer_section.on('click', '.add-remove-customer', function (e) { const customer_info_is_visible = me.$cart_container.hasClass('d-none'); @@ -356,7 +356,7 @@ erpnext.PointOfSale.ItemCart = class { onchange: function() { if (this.value || this.value == 0) { const frm = me.events.get_frm(); - frappe.model.set_value(frm.doc.doctype, frm.doc.name, 'additional_discount_percentage', this.value); + frappe.model.set_value(frm.doc.doctype, frm.doc.name, 'additional_discount_percentage', flt(this.value)); me.hide_discount_control(this.value); } }, @@ -381,8 +381,8 @@ erpnext.PointOfSale.ItemCart = class { ` ); } - - update_customer_section() { + + update_customer_section() { const { customer, email_id='', mobile_no='', image } = this.customer_info || {}; if (customer) { @@ -403,7 +403,7 @@ erpnext.PointOfSale.ItemCart = class {
    ` ); } else { - // reset customer selector + // reset customer selector this.reset_customer_selector(); } @@ -430,9 +430,9 @@ erpnext.PointOfSale.ItemCart = class { ` } } - } - - update_totals_section(frm) { + } + + update_totals_section(frm) { if (!frm) frm = this.events.get_frm(); this.render_net_total(frm.doc.base_net_total); @@ -440,9 +440,9 @@ erpnext.PointOfSale.ItemCart = class { const taxes = frm.doc.taxes.map(t => { return { description: t.description, rate: t.rate }}) this.render_taxes(frm.doc.base_total_taxes_and_charges, taxes); - } - - render_net_total(value) { + } + + render_net_total(value) { const currency = this.events.get_frm().doc.currency; this.$totals_section.find('.net-total').html( `
    @@ -454,9 +454,9 @@ erpnext.PointOfSale.ItemCart = class { ) this.$numpad_section.find('.numpad-net-total').html(`Net Total: ${format_currency(value, currency)}`) - } - - render_grand_total(value) { + } + + render_grand_total(value) { const currency = this.events.get_frm().doc.currency; this.$totals_section.find('.grand-total').html( `
    @@ -495,20 +495,20 @@ erpnext.PointOfSale.ItemCart = class { } else { this.$totals_section.find('.taxes').html('') } - } + } - get_cart_item({ item_code, batch_no, uom }) { + get_cart_item({ item_code, batch_no, uom }) { const batch_attr = `[data-batch-no="${escape(batch_no)}"]`; const item_code_attr = `[data-item-code="${escape(item_code)}"]`; const uom_attr = `[data-uom=${escape(uom)}]`; - const item_selector = batch_no ? - `.cart-item-wrapper${batch_attr}${uom_attr}` : `.cart-item-wrapper${item_code_attr}${uom_attr}`; - - return this.$cart_items_wrapper.find(item_selector); - } - - update_item_html(item, remove_item) { + const item_selector = batch_no ? + `.cart-item-wrapper${batch_attr}${uom_attr}` : `.cart-item-wrapper${item_code_attr}${uom_attr}`; + + return this.$cart_items_wrapper.find(item_selector); + } + + update_item_html(item, remove_item) { const $item = this.get_cart_item(item); if (remove_item) { @@ -524,33 +524,33 @@ erpnext.PointOfSale.ItemCart = class { const no_of_cart_items = this.$cart_items_wrapper.children().length; no_of_cart_items > 0 && this.highlight_checkout_btn(no_of_cart_items > 0); - + this.update_empty_cart_section(no_of_cart_items); } - - render_cart_item(item_data, $item_to_update) { + + render_cart_item(item_data, $item_to_update) { const currency = this.events.get_frm().doc.currency; const me = this; - if (!$item_to_update.length) { - this.$cart_items_wrapper.append( - `
    -
    ` - ) - $item_to_update = this.get_cart_item(item_data); - } +
    ` + ) + $item_to_update = this.get_cart_item(item_data); + } $item_to_update.html( `
    -
    - ${item_data.item_name} -
    - ${get_description_html()} -
    - ${get_rate_discount_html()} -
    ` +
    + ${item_data.item_name} +
    + ${get_description_html()} + + ${get_rate_discount_html()} + ` ) set_dynamic_rate_header_width(); @@ -572,7 +572,7 @@ erpnext.PointOfSale.ItemCart = class { me.$cart_header.find(".rate-list-header").css("width", max_width); me.$cart_items_wrapper.find(".rate-col").css("width", max_width); } - + function get_rate_discount_html() { if (item_data.rate && item_data.amount && item_data.rate !== item_data.amount) { return ` @@ -625,7 +625,7 @@ erpnext.PointOfSale.ItemCart = class { $item_to_update.attr(`data-${selector}`, value); } - toggle_checkout_btn(show_checkout) { + toggle_checkout_btn(show_checkout) { if (show_checkout) { this.$totals_section.find('.checkout-btn').removeClass('d-none'); this.$totals_section.find('.edit-cart-btn').addClass('d-none'); @@ -635,7 +635,7 @@ erpnext.PointOfSale.ItemCart = class { } } - highlight_checkout_btn(toggle) { + highlight_checkout_btn(toggle) { const has_primary_class = this.$totals_section.find('.checkout-btn').hasClass('bg-primary'); if (toggle && !has_primary_class) { this.$totals_section.find('.checkout-btn').addClass('bg-primary text-white text-lg'); @@ -643,8 +643,8 @@ erpnext.PointOfSale.ItemCart = class { this.$totals_section.find('.checkout-btn').removeClass('bg-primary text-white text-lg'); } } - - update_empty_cart_section(no_of_cart_items) { + + update_empty_cart_section(no_of_cart_items) { const $no_item_element = this.$cart_items_wrapper.find('.no-item-wrapper'); // if cart has items and no item is present @@ -652,27 +652,27 @@ erpnext.PointOfSale.ItemCart = class { && this.$cart_items_wrapper.removeClass('mt-4 border-grey border-dashed') && this.$cart_header.removeClass('d-none'); no_of_cart_items === 0 && !$no_item_element.length && this.make_no_items_placeholder(); - } - - on_numpad_event($btn) { + } + + on_numpad_event($btn) { const current_action = $btn.attr('data-button-value'); const action_is_field_edit = ['qty', 'discount_percentage', 'rate'].includes(current_action); this.highlight_numpad_btn($btn, current_action); - const action_is_pressed_twice = this.prev_action === current_action; - const first_click_event = !this.prev_action; - const field_to_edit_changed = this.prev_action && this.prev_action != current_action; + const action_is_pressed_twice = this.prev_action === current_action; + const first_click_event = !this.prev_action; + const field_to_edit_changed = this.prev_action && this.prev_action != current_action; if (action_is_field_edit) { if (first_click_event || field_to_edit_changed) { - this.prev_action = current_action; + this.prev_action = current_action; } else if (action_is_pressed_twice) { this.prev_action = undefined; } - this.numpad_value = ''; - + this.numpad_value = ''; + } else if (current_action === 'checkout') { this.prev_action = undefined; this.toggle_item_highlight(); @@ -688,7 +688,7 @@ erpnext.PointOfSale.ItemCart = class { this.numpad_value = this.numpad_value || 0; } - const first_click_event_is_not_field_edit = !action_is_field_edit && first_click_event; + const first_click_event_is_not_field_edit = !action_is_field_edit && first_click_event; if (first_click_event_is_not_field_edit) { frappe.show_alert({ @@ -708,34 +708,34 @@ erpnext.PointOfSale.ItemCart = class { this.numpad_value = current_action; } - this.events.numpad_event(this.numpad_value, this.prev_action); - } - - highlight_numpad_btn($btn, curr_action) { - const curr_action_is_highlighted = $btn.hasClass('shadow-inner'); - const curr_action_is_action = ['qty', 'discount_percentage', 'rate', 'done'].includes(curr_action); + this.events.numpad_event(this.numpad_value, this.prev_action); + } + + highlight_numpad_btn($btn, curr_action) { + const curr_action_is_highlighted = $btn.hasClass('shadow-inner'); + const curr_action_is_action = ['qty', 'discount_percentage', 'rate', 'done'].includes(curr_action); - if (!curr_action_is_highlighted) { - $btn.addClass('shadow-inner bg-selected'); - } - if (this.prev_action === curr_action && curr_action_is_highlighted) { - // if Qty is pressed twice - $btn.removeClass('shadow-inner bg-selected'); - } - if (this.prev_action && this.prev_action !== curr_action && curr_action_is_action) { - // Order: Qty -> Rate then remove Qty highlight - const prev_btn = $(`[data-button-value='${this.prev_action}']`); - prev_btn.removeClass('shadow-inner bg-selected'); - } - if (!curr_action_is_action || curr_action === 'done') { - // if numbers are clicked - setTimeout(() => { - $btn.removeClass('shadow-inner bg-selected'); - }, 100); - } - } + if (!curr_action_is_highlighted) { + $btn.addClass('shadow-inner bg-selected'); + } + if (this.prev_action === curr_action && curr_action_is_highlighted) { + // if Qty is pressed twice + $btn.removeClass('shadow-inner bg-selected'); + } + if (this.prev_action && this.prev_action !== curr_action && curr_action_is_action) { + // Order: Qty -> Rate then remove Qty highlight + const prev_btn = $(`[data-button-value='${this.prev_action}']`); + prev_btn.removeClass('shadow-inner bg-selected'); + } + if (!curr_action_is_action || curr_action === 'done') { + // if numbers are clicked + setTimeout(() => { + $btn.removeClass('shadow-inner bg-selected'); + }, 100); + } + } - toggle_numpad(show) { + toggle_numpad(show) { if (show) { this.$totals_section.addClass('d-none'); this.$numpad_section.removeClass('d-none'); @@ -946,6 +946,6 @@ erpnext.PointOfSale.ItemCart = class { toggle_component(show) { show ? this.$component.removeClass('d-none') : this.$component.addClass('d-none'); - } - -} \ No newline at end of file + } + +} diff --git a/erpnext/selling/page/point_of_sale/pos_item_details.js b/erpnext/selling/page/point_of_sale/pos_item_details.js index 86a1be9faf..3a5f89ba93 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_details.js +++ b/erpnext/selling/page/point_of_sale/pos_item_details.js @@ -1,28 +1,28 @@ erpnext.PointOfSale.ItemDetails = class { - constructor({ wrapper, events }) { + constructor({ wrapper, events }) { this.wrapper = wrapper; - this.events = events; - this.current_item = {}; + this.events = events; + this.current_item = {}; - this.init_component(); - } + this.init_component(); + } - init_component() { - this.prepare_dom(); - this.init_child_components(); + init_component() { + this.prepare_dom(); + this.init_child_components(); this.bind_events(); this.attach_shortcuts(); - } + } - prepare_dom() { - this.wrapper.append( - `
    ` - ) + prepare_dom() { + this.wrapper.append( + `
    ` + ) - this.$component = this.wrapper.find('.item-details'); - } + this.$component = this.wrapper.find('.item-details'); + } - init_child_components() { + init_child_components() { this.$component.html( `
    @@ -49,28 +49,28 @@ erpnext.PointOfSale.ItemDetails = class { this.$item_image = this.$component.find('.item-image'); this.$form_container = this.$component.find('.form-container'); this.$dicount_section = this.$component.find('.discount-section'); - } + } - toggle_item_details_section(item) { + toggle_item_details_section(item) { const { item_code, batch_no, uom } = this.current_item; const item_code_is_same = item && item_code === item.item_code; const batch_is_same = item && batch_no == item.batch_no; const uom_is_same = item && uom === item.uom; - this.item_has_changed = !item ? false : item_code_is_same && batch_is_same && uom_is_same ? false : true; + this.item_has_changed = !item ? false : item_code_is_same && batch_is_same && uom_is_same ? false : true; - this.events.toggle_item_selector(this.item_has_changed); + this.events.toggle_item_selector(this.item_has_changed); this.toggle_component(this.item_has_changed); - + if (this.item_has_changed) { - this.doctype = item.doctype; + this.doctype = item.doctype; this.item_meta = frappe.get_meta(this.doctype); this.name = item.name; this.item_row = item; - this.currency = this.events.get_frm().doc.currency; - - this.current_item = { item_code: item.item_code, batch_no: item.batch_no, uom: item.uom }; - + this.currency = this.events.get_frm().doc.currency; + + this.current_item = { item_code: item.item_code, batch_no: item.batch_no, uom: item.uom }; + this.render_dom(item); this.render_discount_dom(item); this.render_form(item); @@ -102,9 +102,9 @@ erpnext.PointOfSale.ItemDetails = class { this.events.remove_item_from_cart(); } } - - render_dom(item) { - let { item_code ,item_name, description, image, price_list_rate } = item; + + render_dom(item) { + let { item_code ,item_name, description, image, price_list_rate } = item; function get_description_html() { if (description) { @@ -112,8 +112,8 @@ erpnext.PointOfSale.ItemDetails = class { return description; } return ``; - } - + } + this.$item_name.html(item_name); this.$item_description.html(get_description_html()); this.$item_price.html(format_currency(price_list_rate, this.currency)); @@ -125,9 +125,9 @@ erpnext.PointOfSale.ItemDetails = class { this.$item_image.html(frappe.get_abbr(item_code)); } - } - - render_discount_dom(item) { + } + + render_discount_dom(item) { if (item.discount_percentage) { this.$dicount_section.html( `
    @@ -141,9 +141,9 @@ erpnext.PointOfSale.ItemDetails = class { } else { this.$dicount_section.html(``) } - } + } - render_form(item) { + render_form(item) { const fields_to_display = this.get_form_fields(item); this.$form_container.html(''); @@ -157,7 +157,7 @@ erpnext.PointOfSale.ItemDetails = class { const field_meta = this.item_meta.fields.find(df => df.fieldname === fieldname); fieldname === 'discount_percentage' ? (field_meta.label = __('Discount (%)')) : ''; const me = this; - + this[`${fieldname}_control`] = frappe.ui.form.make_control({ df: { ...field_meta, @@ -174,16 +174,16 @@ erpnext.PointOfSale.ItemDetails = class { this.make_auto_serial_selection_btn(item); this.bind_custom_control_change_event(); - } + } - get_form_fields(item) { + get_form_fields(item) { const fields = ['qty', 'uom', 'rate', 'price_list_rate', 'discount_percentage', 'warehouse', 'actual_qty']; if (item.has_serial_no) fields.push('serial_no'); if (item.has_batch_no) fields.push('batch_no'); return fields; } - make_auto_serial_selection_btn(item) { + make_auto_serial_selection_btn(item) { if (item.has_serial_no) { this.$form_container.append( `
    ` @@ -203,8 +203,8 @@ erpnext.PointOfSale.ItemDetails = class { this.$form_container.find('.serial_no-control').parent().addClass('row-span-2'); } } - - bind_custom_control_change_event() { + + bind_custom_control_change_event() { const me = this; if (this.rate_control) { this.rate_control.df.onchange = function() { @@ -276,8 +276,8 @@ erpnext.PointOfSale.ItemDetails = class { }; this.batch_no_control.df.onchange = function() { me.events.set_value_in_current_cart_item('batch-no', this.value); - me.events.form_updated(me.doctype, me.name, 'batch_no', this.value); - me.current_item.batch_no = this.value; + me.events.form_updated(me.doctype, me.name, 'batch_no', this.value); + me.current_item.batch_no = this.value; } this.batch_no_control.refresh(); } @@ -289,9 +289,9 @@ erpnext.PointOfSale.ItemDetails = class { me.current_item.uom = this.value; } } - } - - async auto_update_batch_no() { + } + + async auto_update_batch_no() { if (this.serial_no_control && this.batch_no_control) { const selected_serial_nos = this.serial_no_control.get_value().split(`\n`).filter(s => s); if (!selected_serial_nos.length) return; @@ -310,9 +310,9 @@ erpnext.PointOfSale.ItemDetails = class { const batch_no = Object.keys(batch_serial_map)[0]; const batch_serial_nos = batch_serial_map[batch_no].join(`\n`); // eg. 10 selected serial no. -> 5 belongs to first batch other 5 belongs to second batch - const serial_nos_belongs_to_other_batch = selected_serial_nos.length !== batch_serial_map[batch_no].length; - - const current_batch_no = this.batch_no_control.get_value(); + const serial_nos_belongs_to_other_batch = selected_serial_nos.length !== batch_serial_map[batch_no].length; + + const current_batch_no = this.batch_no_control.get_value(); current_batch_no != batch_no && await this.batch_no_control.set_value(batch_no); if (serial_nos_belongs_to_other_batch) { @@ -326,8 +326,8 @@ erpnext.PointOfSale.ItemDetails = class { this.events.clone_new_batch_item_in_frm(batch_serial_map, this.current_item); } } - - bind_events() { + + bind_events() { this.bind_auto_serial_fetch_event(); this.bind_fields_to_numpad_fields(); @@ -345,7 +345,7 @@ erpnext.PointOfSale.ItemDetails = class { }); } - bind_fields_to_numpad_fields() { + bind_fields_to_numpad_fields() { const me = this; this.$form_container.on('click', '.input-with-feedback', function() { const fieldname = $(this).attr('data-fieldname'); @@ -355,8 +355,8 @@ erpnext.PointOfSale.ItemDetails = class { } }); } - - bind_auto_serial_fetch_event() { + + bind_auto_serial_fetch_event() { this.$form_container.on('click', '.auto-fetch-btn', () => { this.batch_no_control.set_value(''); let qty = this.qty_control.get_value(); @@ -382,7 +382,7 @@ erpnext.PointOfSale.ItemDetails = class { frappe.msgprint(`Fetched only ${records_length} available serial numbers.`); this.qty_control.set_value(records_length); } - numbers = auto_fetched_serial_numbers.join(`\n`); + numbers = auto_fetched_serial_numbers.join(`\n`); this.serial_no_control.set_value(numbers); }); }) @@ -390,5 +390,5 @@ erpnext.PointOfSale.ItemDetails = class { toggle_component(show) { show ? this.$component.removeClass('d-none') : this.$component.addClass('d-none'); - } + } } \ No newline at end of file diff --git a/erpnext/selling/page/point_of_sale/pos_item_selector.js b/erpnext/selling/page/point_of_sale/pos_item_selector.js index ee0c06d45d..c87b845a41 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_selector.js +++ b/erpnext/selling/page/point_of_sale/pos_item_selector.js @@ -1,115 +1,115 @@ erpnext.PointOfSale.ItemSelector = class { - constructor({ frm, wrapper, events, pos_profile }) { + constructor({ frm, wrapper, events, pos_profile }) { this.wrapper = wrapper; this.events = events; - this.pos_profile = pos_profile; - - this.inti_component(); - } - - inti_component() { - this.prepare_dom(); - this.make_search_bar(); - this.load_items_data(); - this.bind_events(); - this.attach_shortcuts(); - } + this.pos_profile = pos_profile; + + this.inti_component(); + } + + inti_component() { + this.prepare_dom(); + this.make_search_bar(); + this.load_items_data(); + this.bind_events(); + this.attach_shortcuts(); + } - prepare_dom() { + prepare_dom() { this.wrapper.append( - `
    -
    -
    -
    -
    -
    -
    -
    ALL ITEMS
    -
    -
    -
    -
    -
    ` - ); - - this.$component = this.wrapper.find('.items-selector'); - } + `
    +
    +
    +
    +
    +
    +
    +
    ALL ITEMS
    +
    +
    +
    +
    +
    ` + ); + + this.$component = this.wrapper.find('.items-selector'); + } - async load_items_data() { - if (!this.item_group) { - const res = await frappe.db.get_value("Item Group", {lft: 1, is_group: 1}, "name"); - this.parent_item_group = res.message.name; - }; - if (!this.price_list) { - const res = await frappe.db.get_value("POS Profile", this.pos_profile, "selling_price_list"); - this.price_list = res.message.selling_price_list; - } + async load_items_data() { + if (!this.item_group) { + const res = await frappe.db.get_value("Item Group", {lft: 1, is_group: 1}, "name"); + this.parent_item_group = res.message.name; + }; + if (!this.price_list) { + const res = await frappe.db.get_value("POS Profile", this.pos_profile, "selling_price_list"); + this.price_list = res.message.selling_price_list; + } - this.get_items({}).then(({message}) => { - this.render_item_list(message.items); - }); - } + this.get_items({}).then(({message}) => { + this.render_item_list(message.items); + }); + } - get_items({start = 0, page_length = 40, search_value=''}) { - const price_list = this.events.get_frm().doc?.selling_price_list || this.price_list; - let { item_group, pos_profile } = this; + get_items({start = 0, page_length = 40, search_value=''}) { + const price_list = this.events.get_frm().doc?.selling_price_list || this.price_list; + let { item_group, pos_profile } = this; - !item_group && (item_group = this.parent_item_group); - + !item_group && (item_group = this.parent_item_group); + return frappe.call({ method: "erpnext.selling.page.point_of_sale.point_of_sale.get_items", freeze: true, - args: { start, page_length, price_list, item_group, search_value, pos_profile }, - }); + args: { start, page_length, price_list, item_group, search_value, pos_profile }, + }); } render_item_list(items) { - this.$items_container = this.$component.find('.items-container'); - this.$items_container.html(''); + this.$items_container = this.$component.find('.items-container'); + this.$items_container.html(''); - items.forEach(item => { - const item_html = this.get_item_html(item); - this.$items_container.append(item_html); - }) - } + items.forEach(item => { + const item_html = this.get_item_html(item); + this.$items_container.append(item_html); + }) + } - get_item_html(item) { - const { item_image, serial_no, batch_no, barcode, actual_qty, stock_uom } = item; - const indicator_color = actual_qty > 10 ? "green" : actual_qty !== 0 ? "orange" : "red"; + get_item_html(item) { + const { item_image, serial_no, batch_no, barcode, actual_qty, stock_uom } = item; + const indicator_color = actual_qty > 10 ? "green" : actual_qty !== 0 ? "orange" : "red"; - function get_item_image_html() { - if (item_image) { - return `
    - ${item_image} -
    ` - } else { - return `
    - ${frappe.get_abbr(item.item_name)} -
    ` - } - } + function get_item_image_html() { + if (item_image) { + return `
    + ${item_image} +
    ` + } else { + return `
    + ${frappe.get_abbr(item.item_name)} +
    ` + } + } return ( - `
    - ${get_item_image_html()} -
    -
    - - ${frappe.ellipsis(item.item_name, 18)} -
    -
    ${format_currency(item.price_list_rate, item.currency, 0) || 0}
    -
    -
    ` - ) - } + `
    + ${get_item_image_html()} +
    +
    + + ${frappe.ellipsis(item.item_name, 18)} +
    +
    ${format_currency(item.price_list_rate, item.currency, 0) || 0}
    +
    +
    ` + ) + } - make_search_bar() { - const me = this; - this.$component.find('.search-field').html(''); - this.$component.find('.item-group-field').html(''); + make_search_bar() { + const me = this; + this.$component.find('.search-field').html(''); + this.$component.find('.item-group-field').html(''); this.search_field = frappe.ui.form.make_control({ df: { @@ -119,104 +119,104 @@ erpnext.PointOfSale.ItemSelector = class { }, parent: this.$component.find('.search-field'), render_input: true, - }); + }); this.item_group_field = frappe.ui.form.make_control({ df: { label: __('Item Group'), fieldtype: 'Link', options: 'Item Group', - placeholder: __('Select item group'), - onchange: function() { - me.item_group = this.value; - !me.item_group && (me.item_group = me.parent_item_group); - me.filter_items(); - }, - get_query: function () { - return { - query: 'erpnext.selling.page.point_of_sale.point_of_sale.item_group_query', - filters: { - pos_profile: me.events.get_frm().doc?.pos_profile - } - } - }, + placeholder: __('Select item group'), + onchange: function() { + me.item_group = this.value; + !me.item_group && (me.item_group = me.parent_item_group); + me.filter_items(); + }, + get_query: function () { + return { + query: 'erpnext.selling.page.point_of_sale.point_of_sale.item_group_query', + filters: { + pos_profile: me.events.get_frm().doc?.pos_profile + } + } + }, }, - parent: this.$component.find('.item-group-field'), + parent: this.$component.find('.item-group-field'), render_input: true, - }); - this.search_field.toggle_label(false); + }); + this.search_field.toggle_label(false); this.item_group_field.toggle_label(false); } - bind_events() { - const me = this; - onScan.attachTo(document, { - onScan: (sScancode) => { - if (this.search_field && this.$component.is(':visible')) { - this.search_field.set_focus(); - $(this.search_field.$input[0]).val(sScancode).trigger("input"); - this.barcode_scanned = true; - } - } - }); + bind_events() { + const me = this; + onScan.attachTo(document, { + onScan: (sScancode) => { + if (this.search_field && this.$component.is(':visible')) { + this.search_field.set_focus(); + $(this.search_field.$input[0]).val(sScancode).trigger("input"); + this.barcode_scanned = true; + } + } + }); this.$component.on('click', '.item-wrapper', function() { const $item = $(this); const item_code = unescape($item.attr('data-item-code')); - let batch_no = unescape($item.attr('data-batch-no')); - let serial_no = unescape($item.attr('data-serial-no')); - let uom = unescape($item.attr('data-uom')); - - // escape(undefined) returns "undefined" then unescape returns "undefined" - batch_no = batch_no === "undefined" ? undefined : batch_no; - serial_no = serial_no === "undefined" ? undefined : serial_no; - uom = uom === "undefined" ? undefined : uom; + let batch_no = unescape($item.attr('data-batch-no')); + let serial_no = unescape($item.attr('data-serial-no')); + let uom = unescape($item.attr('data-uom')); + + // escape(undefined) returns "undefined" then unescape returns "undefined" + batch_no = batch_no === "undefined" ? undefined : batch_no; + serial_no = serial_no === "undefined" ? undefined : serial_no; + uom = uom === "undefined" ? undefined : uom; - me.events.item_selected({ field: 'qty', value: "+1", item: { item_code, batch_no, serial_no, uom }}); - }) + me.events.item_selected({ field: 'qty', value: "+1", item: { item_code, batch_no, serial_no, uom }}); + }) - this.search_field.$input.on('input', (e) => { + this.search_field.$input.on('input', (e) => { clearTimeout(this.last_search); this.last_search = setTimeout(() => { const search_term = e.target.value; this.filter_items({ search_term }); }, 300); - }); - } + }); + } - attach_shortcuts() { - frappe.ui.keys.on("ctrl+i", () => { - const selector_is_visible = this.$component.is(':visible'); - if (!selector_is_visible) return; - this.search_field.set_focus(); - }); - frappe.ui.keys.on("ctrl+g", () => { - const selector_is_visible = this.$component.is(':visible'); - if (!selector_is_visible) return; - this.item_group_field.set_focus(); - }); - // for selecting the last filtered item on search - frappe.ui.keys.on("enter", () => { - const selector_is_visible = this.$component.is(':visible'); - if (!selector_is_visible || this.search_field.get_value() === "") return; + attach_shortcuts() { + frappe.ui.keys.on("ctrl+i", () => { + const selector_is_visible = this.$component.is(':visible'); + if (!selector_is_visible) return; + this.search_field.set_focus(); + }); + frappe.ui.keys.on("ctrl+g", () => { + const selector_is_visible = this.$component.is(':visible'); + if (!selector_is_visible) return; + this.item_group_field.set_focus(); + }); + // for selecting the last filtered item on search + frappe.ui.keys.on("enter", () => { + const selector_is_visible = this.$component.is(':visible'); + if (!selector_is_visible || this.search_field.get_value() === "") return; - if (this.items.length == 1) { - this.$items_container.find(".item-wrapper").click(); - frappe.utils.play_sound("submit"); - $(this.search_field.$input[0]).val("").trigger("input"); - } else if (this.items.length == 0 && this.barcode_scanned) { - // only show alert of barcode is scanned and enter is pressed - frappe.show_alert({ - message: __("No items found. Scan barcode again."), - indicator: 'orange' - }); - frappe.utils.play_sound("error"); - this.barcode_scanned = false; - $(this.search_field.$input[0]).val("").trigger("input"); - } - }); - } - - filter_items({ search_term='' }={}) { + if (this.items.length == 1) { + this.$items_container.find(".item-wrapper").click(); + frappe.utils.play_sound("submit"); + $(this.search_field.$input[0]).val("").trigger("input"); + } else if (this.items.length == 0 && this.barcode_scanned) { + // only show alert of barcode is scanned and enter is pressed + frappe.show_alert({ + message: __("No items found. Scan barcode again."), + indicator: 'orange' + }); + frappe.utils.play_sound("error"); + this.barcode_scanned = false; + $(this.search_field.$input[0]).val("").trigger("input"); + } + }); + } + + filter_items({ search_term='' }={}) { if (search_term) { search_term = search_term.toLowerCase(); @@ -227,39 +227,39 @@ erpnext.PointOfSale.ItemSelector = class { this.items = items; this.render_item_list(items); return; - } + } } this.get_items({ search_value: search_term }) - .then(({ message }) => { - const { items, serial_no, batch_no, barcode } = message; + .then(({ message }) => { + const { items, serial_no, batch_no, barcode } = message; if (search_term && !barcode) { this.search_index[search_term] = items; } this.items = items; - this.render_item_list(items); - }); + this.render_item_list(items); + }); } - - resize_selector(minimize) { - minimize ? - this.$component.find('.search-field').removeClass('mr-8') : - this.$component.find('.search-field').addClass('mr-8'); + + resize_selector(minimize) { + minimize ? + this.$component.find('.search-field').removeClass('mr-8') : + this.$component.find('.search-field').addClass('mr-8'); - minimize ? - this.$component.find('.filter-section').addClass('flex-col') : - this.$component.find('.filter-section').removeClass('flex-col'); + minimize ? + this.$component.find('.filter-section').addClass('flex-col') : + this.$component.find('.filter-section').removeClass('flex-col'); - minimize ? - this.$component.removeClass('col-span-6').addClass('col-span-2') : - this.$component.removeClass('col-span-2').addClass('col-span-6') + minimize ? + this.$component.removeClass('col-span-6').addClass('col-span-2') : + this.$component.removeClass('col-span-2').addClass('col-span-6') - minimize ? - this.$items_container.removeClass('grid-cols-4').addClass('grid-cols-1') : - this.$items_container.removeClass('grid-cols-1').addClass('grid-cols-4') - } + minimize ? + this.$items_container.removeClass('grid-cols-4').addClass('grid-cols-1') : + this.$items_container.removeClass('grid-cols-1').addClass('grid-cols-4') + } - toggle_component(show) { + toggle_component(show) { show ? this.$component.removeClass('d-none') : this.$component.addClass('d-none'); - } + } } \ No newline at end of file diff --git a/erpnext/selling/page/point_of_sale/pos_number_pad.js b/erpnext/selling/page/point_of_sale/pos_number_pad.js index 2ffc2c0229..4b8e841805 100644 --- a/erpnext/selling/page/point_of_sale/pos_number_pad.js +++ b/erpnext/selling/page/point_of_sale/pos_number_pad.js @@ -1,49 +1,48 @@ erpnext.PointOfSale.NumberPad = class { - constructor({ wrapper, events, cols, keys, css_classes, fieldnames_map }) { - this.wrapper = wrapper; - this.events = events; - this.cols = cols; - this.keys = keys; - this.css_classes = css_classes || []; - this.fieldnames = fieldnames_map || {}; + constructor({ wrapper, events, cols, keys, css_classes, fieldnames_map }) { + this.wrapper = wrapper; + this.events = events; + this.cols = cols; + this.keys = keys; + this.css_classes = css_classes || []; + this.fieldnames = fieldnames_map || {}; - this.init_component(); - } + this.init_component(); + } - init_component() { - this.prepare_dom(); - this.bind_events(); - } + init_component() { + this.prepare_dom(); + this.bind_events(); + } - prepare_dom() { - const { cols, keys, css_classes, fieldnames } = this; + prepare_dom() { + const { cols, keys, css_classes, fieldnames } = this; - function get_keys() { - return keys.reduce((a, row, i) => { - return a + row.reduce((a2, number, j) => { - const class_to_append = css_classes && css_classes[i] ? css_classes[i][j] : ''; - const fieldname = fieldnames && fieldnames[number] ? - fieldnames[number] : - typeof number === 'string' ? frappe.scrub(number) : number; - - return a2 + `
    ${number}
    ` - }, '') - }, ''); - } + function get_keys() { + return keys.reduce((a, row, i) => { + return a + row.reduce((a2, number, j) => { + const class_to_append = css_classes && css_classes[i] ? css_classes[i][j] : ''; + const fieldname = fieldnames && fieldnames[number] ? + fieldnames[number] : typeof number === 'string' ? frappe.scrub(number) : number; - this.wrapper.html( - `
    - ${get_keys()} -
    ` - ) - } + return a2 + `
    ${number}
    ` + }, '') + }, ''); + } - bind_events() { - const me = this; - this.wrapper.on('click', '.numpad-btn', function() { - const $btn = $(this); - me.events.numpad_event($btn); - }) - } + this.wrapper.html( + `
    + ${get_keys()} +
    ` + ) + } + + bind_events() { + const me = this; + this.wrapper.on('click', '.numpad-btn', function() { + const $btn = $(this); + me.events.numpad_event($btn); + }); + } } \ No newline at end of file diff --git a/erpnext/selling/page/point_of_sale/pos_past_order_summary.js b/erpnext/selling/page/point_of_sale/pos_past_order_summary.js index 24326b2256..30e0918ba6 100644 --- a/erpnext/selling/page/point_of_sale/pos_past_order_summary.js +++ b/erpnext/selling/page/point_of_sale/pos_past_order_summary.js @@ -86,7 +86,7 @@ erpnext.PointOfSale.PastOrderSummary = class { this.$summary_container.append( `
    ` ) - + this.$summary_btns = this.$summary_container.find('.summary-btns'); } @@ -110,7 +110,10 @@ erpnext.PointOfSale.PastOrderSummary = class { {fieldname:'print', fieldtype:'Data', label:'Print Preview'} ], primary_action: () => { - this.events.get_frm().print_preview.printit(true); + const frm = this.events.get_frm(); + frm.doc = this.doc; + frm.print_preview.lang_code = frm.doc.language; + frm.print_preview.printit(true); }, primary_action_label: __('Print'), }); @@ -174,7 +177,7 @@ erpnext.PointOfSale.PastOrderSummary = class {
    Tax Charges
    - ${ + ${ doc.taxes.map((t, i) => { let margin_left = ''; if (i !== 0) margin_left = 'ml-2'; @@ -271,6 +274,7 @@ erpnext.PointOfSale.PastOrderSummary = class { // this.print_dialog.show(); const frm = this.events.get_frm(); frm.doc = this.doc; + frm.print_preview.lang_code = frm.doc.language; frm.print_preview.printit(true); }); } @@ -284,9 +288,9 @@ erpnext.PointOfSale.PastOrderSummary = class { this.$summary_container.find('.print-btn').click(); }); } - + toggle_component(show) { - show ? + show ? this.$component.removeClass('d-none') : this.$component.addClass('d-none'); } @@ -372,9 +376,9 @@ erpnext.PointOfSale.PastOrderSummary = class { } get_condition_btn_map(after_submission) { - if (after_submission) + if (after_submission) return [{ condition: true, visible_btns: ['Print Receipt', 'Email Receipt', 'New Order'] }]; - + return [ { condition: this.doc.docstatus === 0, visible_btns: ['Edit Order'] }, { condition: !this.doc.is_return && this.doc.docstatus === 1, visible_btns: ['Print Receipt', 'Email Receipt', 'Return']}, @@ -384,7 +388,7 @@ erpnext.PointOfSale.PastOrderSummary = class { load_summary_of(doc, after_submission=false) { this.$summary_wrapper.removeClass("d-none"); - + after_submission ? this.switch_to_post_submit_summary() : this.switch_to_recent_invoice_summary(); diff --git a/erpnext/selling/page/sales_funnel/sales_funnel.py b/erpnext/selling/page/sales_funnel/sales_funnel.py index dba24ef5b0..b613718c7e 100644 --- a/erpnext/selling/page/sales_funnel/sales_funnel.py +++ b/erpnext/selling/page/sales_funnel/sales_funnel.py @@ -20,29 +20,28 @@ def get_funnel_data(from_date, to_date, company): validate_filters(from_date, to_date, company) active_leads = frappe.db.sql("""select count(*) from `tabLead` - where (date(`modified`) between %s and %s) - and status != "Do Not Contact" and company=%s""", (from_date, to_date, company))[0][0] - - active_leads += frappe.db.sql("""select count(distinct contact.name) from `tabContact` contact - left join `tabDynamic Link` dl on (dl.parent=contact.name) where dl.link_doctype='Customer' - and (date(contact.modified) between %s and %s) and status != "Passive" """, (from_date, to_date))[0][0] + where (date(`creation`) between %s and %s) + and company=%s""", (from_date, to_date, company))[0][0] opportunities = frappe.db.sql("""select count(*) from `tabOpportunity` where (date(`creation`) between %s and %s) - and status != "Lost" and company=%s""", (from_date, to_date, company))[0][0] + and opportunity_from='Lead' and company=%s""", (from_date, to_date, company))[0][0] quotations = frappe.db.sql("""select count(*) from `tabQuotation` where docstatus = 1 and (date(`creation`) between %s and %s) - and status != "Lost" and company=%s""", (from_date, to_date, company))[0][0] + and (opportunity!="" or quotation_to="Lead") and company=%s""", (from_date, to_date, company))[0][0] + + converted = frappe.db.sql("""select count(*) from `tabCustomer` + JOIN `tabLead` ON `tabLead`.name = `tabCustomer`.lead_name + WHERE (date(`tabCustomer`.creation) between %s and %s) + and `tabLead`.company=%s""", (from_date, to_date, company))[0][0] - sales_orders = frappe.db.sql("""select count(*) from `tabSales Order` - where docstatus = 1 and (date(`creation`) between %s and %s) and company=%s""", (from_date, to_date, company))[0][0] return [ - { "title": _("Active Leads / Customers"), "value": active_leads, "color": "#B03B46" }, + { "title": _("Active Leads"), "value": active_leads, "color": "#B03B46" }, { "title": _("Opportunities"), "value": opportunities, "color": "#F09C00" }, { "title": _("Quotations"), "value": quotations, "color": "#006685" }, - { "title": _("Sales Orders"), "value": sales_orders, "color": "#00AD65" } + { "title": _("Converted"), "value": converted, "color": "#00AD65" } ] @frappe.whitelist() diff --git a/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/item_group_wise_sales_target_variance.py b/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/item_group_wise_sales_target_variance.py index 857b9823e0..ae216ca5d6 100644 --- a/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/item_group_wise_sales_target_variance.py +++ b/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/item_group_wise_sales_target_variance.py @@ -63,13 +63,13 @@ def get_columns(filters, period_list, partner_doctype): "label": _(partner_doctype), "fieldtype": "Link", "options": partner_doctype, - "width": 100 + "width": 150 }, { "fieldname": "item_group", "label": _("Item Group"), "fieldtype": "Link", "options": "Item Group", - "width": 100 + "width": 150 }] for period in period_list: @@ -81,19 +81,19 @@ def get_columns(filters, period_list, partner_doctype): "label": _("Target ({})").format(period.label), "fieldtype": fieldtype, "options": options, - "width": 100 + "width": 150 }, { "fieldname": period.key, "label": _("Achieved ({})").format(period.label), "fieldtype": fieldtype, "options": options, - "width": 100 + "width": 150 }, { "fieldname": variance_key, "label": _("Variance ({})").format(period.label), "fieldtype": fieldtype, "options": options, - "width": 100 + "width": 150 }]) columns.extend([{ @@ -101,19 +101,19 @@ def get_columns(filters, period_list, partner_doctype): "label": _("Total Target"), "fieldtype": fieldtype, "options": options, - "width": 100 + "width": 150 }, { "fieldname": "total_achieved", "label": _("Total Achieved"), "fieldtype": fieldtype, "options": options, - "width": 100 + "width": 150 }, { "fieldname": "total_variance", "label": _("Total Variance"), "fieldtype": fieldtype, "options": options, - "width": 100 + "width": 150 }]) return columns @@ -154,10 +154,10 @@ def prepare_data(filters, sales_users_data, actual_data, date_field, period_list if (r.get(sales_field) == d.parent and r.item_group == d.item_group and period.from_date <= r.get(date_field) and r.get(date_field) <= period.to_date): details[p_key] += r.get(qty_or_amount_field, 0) - details[variance_key] = details.get(target_key) - details.get(p_key) + details[variance_key] = details.get(p_key) - details.get(target_key) details["total_achieved"] += details.get(p_key) - details["total_variance"] = details.get("total_target") - details.get("total_achieved") + details["total_variance"] = details.get("total_achieved") - details.get("total_target") return rows diff --git a/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/sales_partner_target_variance_based_on_item_group.js b/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/sales_partner_target_variance_based_on_item_group.js index f99f68c524..38bb127e23 100644 --- a/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/sales_partner_target_variance_based_on_item_group.js +++ b/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/sales_partner_target_variance_based_on_item_group.js @@ -44,5 +44,20 @@ frappe.query_reports["Sales Partner Target Variance based on Item Group"] = { options: "Quantity\nAmount", default: "Quantity" }, - ] + ], + "formatter": function (value, row, column, data, default_formatter) { + value = default_formatter(value, row, column, data); + + if (column.fieldname.includes('variance')) { + + if (data[column.fieldname] < 0) { + value = "" + value + ""; + } + else if (data[column.fieldname] > 0) { + value = "" + value + ""; + } + } + + return value; + } } diff --git a/erpnext/selling/report/sales_person_target_variance_based_on_item_group/sales_person_target_variance_based_on_item_group.js b/erpnext/selling/report/sales_person_target_variance_based_on_item_group/sales_person_target_variance_based_on_item_group.js index 9f6bfc41df..a8e2fad373 100644 --- a/erpnext/selling/report/sales_person_target_variance_based_on_item_group/sales_person_target_variance_based_on_item_group.js +++ b/erpnext/selling/report/sales_person_target_variance_based_on_item_group/sales_person_target_variance_based_on_item_group.js @@ -44,5 +44,20 @@ frappe.query_reports["Sales Person Target Variance Based On Item Group"] = { options: "Quantity\nAmount", default: "Quantity" }, - ] + ], + "formatter": function (value, row, column, data, default_formatter) { + value = default_formatter(value, row, column, data); + + if (column.fieldname.includes('variance')) { + + if (data[column.fieldname] < 0) { + value = "" + value + ""; + } + else if (data[column.fieldname] > 0) { + value = "" + value + ""; + } + } + + return value; + } } diff --git a/erpnext/selling/report/territory_target_variance_based_on_item_group/territory_target_variance_based_on_item_group.js b/erpnext/selling/report/territory_target_variance_based_on_item_group/territory_target_variance_based_on_item_group.js index dd9607ffbd..263391a7f7 100644 --- a/erpnext/selling/report/territory_target_variance_based_on_item_group/territory_target_variance_based_on_item_group.js +++ b/erpnext/selling/report/territory_target_variance_based_on_item_group/territory_target_variance_based_on_item_group.js @@ -44,5 +44,20 @@ frappe.query_reports["Territory Target Variance Based On Item Group"] = { options: "Quantity\nAmount", default: "Quantity" }, - ] + ], + "formatter": function (value, row, column, data, default_formatter) { + value = default_formatter(value, row, column, data); + + if (column.fieldname.includes('variance')) { + + if (data[column.fieldname] < 0) { + value = "" + value + ""; + } + else if (data[column.fieldname] > 0) { + value = "" + value + ""; + } + } + + return value; + } } diff --git a/erpnext/setup/doctype/company/delete_company_transactions.py b/erpnext/setup/doctype/company/delete_company_transactions.py index 8ecc13b2fb..c94831ef93 100644 --- a/erpnext/setup/doctype/company/delete_company_transactions.py +++ b/erpnext/setup/doctype/company/delete_company_transactions.py @@ -26,7 +26,8 @@ def delete_company_transactions(company_name): tabDocField where fieldtype='Link' and options='Company'"""): if doctype not in ("Account", "Cost Center", "Warehouse", "Budget", "Party Account", "Employee", "Sales Taxes and Charges Template", - "Purchase Taxes and Charges Template", "POS Profile", 'BOM'): + "Purchase Taxes and Charges Template", "POS Profile", "BOM", + "Company", "Bank Account"): delete_for_doctype(doctype, company_name) # reset company values diff --git a/erpnext/setup/install.py b/erpnext/setup/install.py index aa9fbc0a92..2225fe169f 100644 --- a/erpnext/setup/install.py +++ b/erpnext/setup/install.py @@ -7,6 +7,7 @@ import frappe from erpnext.accounts.doctype.cash_flow_mapper.default_cash_flow_mapper import DEFAULT_MAPPERS from .default_success_action import get_default_success_action from frappe import _ +from frappe.utils import cint from frappe.desk.page.setup_wizard.setup_wizard import add_all_roles_to from frappe.custom.doctype.custom_field.custom_field import create_custom_field from erpnext.setup.default_energy_point_rules import get_default_energy_point_rules @@ -19,18 +20,20 @@ def after_install(): frappe.get_doc({'doctype': "Role", "role_name": "Analytics"}).insert() set_single_defaults() create_compact_item_print_custom_field() + create_print_uom_after_qty_custom_field() create_print_zero_amount_taxes_custom_field() add_all_roles_to("Administrator") create_default_cash_flow_mapper_templates() create_default_success_action() create_default_energy_point_rules() add_company_to_session_defaults() + add_standard_navbar_items() frappe.db.commit() def check_setup_wizard_not_completed(): - if frappe.db.get_default('desktop:home_page') != 'setup-wizard': - message = """ERPNext can only be installed on a fresh site where the setup wizard is not completed. + if cint(frappe.db.get_single_value('System Settings', 'setup_complete') or 0): + message = """ERPNext can only be installed on a fresh site where the setup wizard is not completed. You can reinstall this site (after saving your data) using: bench --site [sitename] reinstall""" frappe.throw(message) @@ -64,6 +67,16 @@ def create_compact_item_print_custom_field(): }) +def create_print_uom_after_qty_custom_field(): + create_custom_field('Print Settings', { + 'label': _('Print UOM after Quantity'), + 'fieldname': 'print_uom_after_quantity', + 'fieldtype': 'Check', + 'default': 0, + 'insert_after': 'compact_item_print' + }) + + def create_print_zero_amount_taxes_custom_field(): create_custom_field('Print Settings', { 'label': _('Print taxes with zero amount'), @@ -103,3 +116,45 @@ def add_company_to_session_defaults(): "ref_doctype": "Company" }) settings.save() + +def add_standard_navbar_items(): + navbar_settings = frappe.get_single("Navbar Settings") + + erpnext_navbar_items = [ + { + 'item_label': 'Documentation', + 'item_type': 'Route', + 'route': 'https://erpnext.com/docs/user/manual', + 'is_standard': 1 + }, + { + 'item_label': 'User Forum', + 'item_type': 'Route', + 'route': 'https://discuss.erpnext.com', + 'is_standard': 1 + }, + { + 'item_label': 'Report an Issue', + 'item_type': 'Route', + 'route': 'https://github.com/frappe/erpnext/issues', + 'is_standard': 1 + } + ] + + current_nabvar_items = navbar_settings.help_dropdown + navbar_settings.set('help_dropdown', []) + + for item in erpnext_navbar_items: + navbar_settings.append('help_dropdown', item) + + for item in current_nabvar_items: + navbar_settings.append('help_dropdown', { + 'item_label': item.item_label, + 'item_type': item.item_type, + 'route': item.route, + 'action': item.action, + 'is_standard': item.is_standard, + 'hidden': item.hidden + }) + + navbar_settings.save() diff --git a/erpnext/startup/leaderboard.py b/erpnext/startup/leaderboard.py index 5545f13e8c..ef238f1165 100644 --- a/erpnext/startup/leaderboard.py +++ b/erpnext/startup/leaderboard.py @@ -123,7 +123,8 @@ def get_all_suppliers(date_range, company, field, limit = None): if field == "outstanding_amount": filters = [['docstatus', '=', '1'], ['company', '=', company]] if date_range: - filters.append(['posting_date', 'between' [date_range[0], date_range[1]]]) + date_range = frappe.parse_json(date_range) + filters.append(['posting_date', 'between', [date_range[0], date_range[1]]]) return frappe.db.get_all('Purchase Invoice', fields = ['supplier as name', 'sum(outstanding_amount) as value'], filters = filters, diff --git a/erpnext/stock/desk_page/stock/stock.json b/erpnext/stock/desk_page/stock/stock.json index 1bf81f7f0e..2fba5fa804 100644 --- a/erpnext/stock/desk_page/stock/stock.json +++ b/erpnext/stock/desk_page/stock/stock.json @@ -33,7 +33,7 @@ { "hidden": 0, "label": "Key Reports", - "links": "[\n {\n \"dependencies\": [\n \"Item Price\"\n ],\n \"doctype\": \"Item Price\",\n \"is_query_report\": false,\n \"label\": \"Item-wise Price List Rate\",\n \"name\": \"Item-wise Price List Rate\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Stock Entry\"\n ],\n \"doctype\": \"Stock Entry\",\n \"is_query_report\": true,\n \"label\": \"Stock Analytics\",\n \"name\": \"Stock Analytics\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Delivery Note\"\n ],\n \"doctype\": \"Delivery Note\",\n \"is_query_report\": true,\n \"label\": \"Delivery Note Trends\",\n \"name\": \"Delivery Note Trends\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Purchase Receipt\"\n ],\n \"doctype\": \"Purchase Receipt\",\n \"is_query_report\": true,\n \"label\": \"Purchase Receipt Trends\",\n \"name\": \"Purchase Receipt Trends\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Order\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Sales Order Analysis\",\n \"name\": \"Sales Order Analysis\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Purchase Order\"\n ],\n \"doctype\": \"Purchase Order\",\n \"is_query_report\": true,\n \"label\": \"Purchase Order Analysis\",\n \"name\": \"Purchase Order Analysis\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Bin\"\n ],\n \"doctype\": \"Bin\",\n \"is_query_report\": true,\n \"label\": \"Item Shortage Report\",\n \"name\": \"Item Shortage Report\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Batch\"\n ],\n \"doctype\": \"Batch\",\n \"is_query_report\": true,\n \"label\": \"Batch-Wise Balance History\",\n \"name\": \"Batch-Wise Balance History\",\n \"type\": \"report\"\n }\n]" + "links": "[\n {\n \"dependencies\": [\n \"Item Price\"\n ],\n \"doctype\": \"Item Price\",\n \"is_query_report\": false,\n \"label\": \"Item-wise Price List Rate\",\n \"name\": \"Item-wise Price List Rate\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Stock Entry\"\n ],\n \"doctype\": \"Stock Entry\",\n \"is_query_report\": true,\n \"label\": \"Stock Analytics\",\n \"name\": \"Stock Analytics\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"doctype\": \"Item\",\n \"is_query_report\": true,\n \"label\": \"Stock Qty vs Serial No Count\",\n \"name\": \"Stock Qty vs Serial No Count\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Delivery Note\"\n ],\n \"doctype\": \"Delivery Note\",\n \"is_query_report\": true,\n \"label\": \"Delivery Note Trends\",\n \"name\": \"Delivery Note Trends\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Purchase Receipt\"\n ],\n \"doctype\": \"Purchase Receipt\",\n \"is_query_report\": true,\n \"label\": \"Purchase Receipt Trends\",\n \"name\": \"Purchase Receipt Trends\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Order\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Sales Order Analysis\",\n \"name\": \"Sales Order Analysis\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Purchase Order\"\n ],\n \"doctype\": \"Purchase Order\",\n \"is_query_report\": true,\n \"label\": \"Purchase Order Analysis\",\n \"name\": \"Purchase Order Analysis\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Bin\"\n ],\n \"doctype\": \"Bin\",\n \"is_query_report\": true,\n \"label\": \"Item Shortage Report\",\n \"name\": \"Item Shortage Report\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Batch\"\n ],\n \"doctype\": \"Batch\",\n \"is_query_report\": true,\n \"label\": \"Batch-Wise Balance History\",\n \"name\": \"Batch-Wise Balance History\",\n \"type\": \"report\"\n }\n]" }, { "hidden": 0, @@ -58,7 +58,7 @@ "idx": 0, "is_standard": 1, "label": "Stock", - "modified": "2020-05-30 17:32:11.062681", + "modified": "2020-08-11 17:29:32.626067", "modified_by": "Administrator", "module": "Stock", "name": "Stock", diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.js b/erpnext/stock/doctype/delivery_note/delivery_note.js index 60f6a68f49..19d0bec26c 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.js +++ b/erpnext/stock/doctype/delivery_note/delivery_note.js @@ -130,12 +130,18 @@ erpnext.stock.DeliveryNoteController = erpnext.selling.SellingController.extend( if (this.frm.doc.docstatus===0) { this.frm.add_custom_button(__('Sales Order'), function() { + if (!me.frm.doc.customer) { + frappe.throw({ + title: __("Mandatory"), + message: __("Please Select a Customer") + }); + } erpnext.utils.map_current_doc({ method: "erpnext.selling.doctype.sales_order.sales_order.make_delivery_note", source_doctype: "Sales Order", target: me.frm, setters: { - customer: me.frm.doc.customer || undefined, + customer: me.frm.doc.customer, }, get_query_filters: { docstatus: 1, diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index d209f48353..d22fda85f4 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -111,6 +111,7 @@ class Item(WebsiteGenerator): self.synced_with_hub = 0 self.validate_has_variants() + self.validate_attributes_in_variants() self.validate_stock_exists_for_template_item() self.validate_attributes() self.validate_variant_attributes() @@ -806,6 +807,77 @@ class Item(WebsiteGenerator): if frappe.db.exists("Item", {"variant_of": self.name}): frappe.throw(_("Item has variants.")) + def validate_attributes_in_variants(self): + if not self.has_variants or self.get("__islocal"): + return + + old_doc = self.get_doc_before_save() + old_doc_attributes = set([attr.attribute for attr in old_doc.attributes]) + own_attributes = [attr.attribute for attr in self.attributes] + + # Check if old attributes were removed from the list + # Is old_attrs is a subset of new ones + # that means we need not check any changes + if old_doc_attributes.issubset(set(own_attributes)): + return + + from collections import defaultdict + + # get all item variants + items = [item["name"] for item in frappe.get_all("Item", {"variant_of": self.name})] + + # get all deleted attributes + deleted_attribute = list(old_doc_attributes.difference(set(own_attributes))) + + # fetch all attributes of these items + item_attributes = frappe.get_all( + "Item Variant Attribute", + filters={ + "parent": ["in", items], + "attribute": ["in", deleted_attribute] + }, + fields=["attribute", "parent"] + ) + not_included = defaultdict(list) + + for attr in item_attributes: + if attr["attribute"] not in own_attributes: + not_included[attr["parent"]].append(attr["attribute"]) + + if not len(not_included): + return + + def body(docnames): + docnames.sort() + return "
    ".join(docnames) + + def table_row(title, body): + return """ + {0} + {1} + """.format(title, body) + + rows = '' + for docname, attr_list in not_included.items(): + link = "{0}".format(frappe.bold(_(docname))) + rows += table_row(link, body(attr_list)) + + error_description = _('The following deleted attributes exist in Variants but not in the Template. You can either delete the Variants or keep the attribute(s) in template.') + + message = """ +
    {0}

    + + + + + + {3} +
    {1}{2}
    + """.format(error_description, _('Variant Items'), _('Attributes'), rows) + + frappe.throw(message, title=_("Variant Attribute Error"), is_minimizable=True, wide=True) + + def validate_stock_exists_for_template_item(self): if self.stock_ledger_created() and self._doc_before_save: if (cint(self._doc_before_save.has_variants) != cint(self.has_variants) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js index 50c18f6282..c504e23677 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js @@ -116,12 +116,18 @@ erpnext.stock.PurchaseReceiptController = erpnext.buying.BuyingController.extend if (this.frm.doc.docstatus == 0) { this.frm.add_custom_button(__('Purchase Order'), function () { + if (!me.frm.doc.supplier) { + frappe.throw({ + title: __("Mandatory"), + message: __("Please Select a Supplier") + }); + } erpnext.utils.map_current_doc({ method: "erpnext.buying.doctype.purchase_order.purchase_order.make_purchase_receipt", source_doctype: "Purchase Order", target: me.frm, setters: { - supplier: me.frm.doc.supplier || undefined, + supplier: me.frm.doc.supplier, }, get_query_filters: { docstatus: 1, diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index d0ba001d7e..4e173fff4d 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -227,6 +227,14 @@ class PurchaseReceipt(BuyingController): if not stock_value_diff: continue + # If PR is sub-contracted and fg item rate is zero + # in that case if account for shource and target warehouse are same, + # then GL entries should not be posted + if flt(stock_value_diff) == flt(d.rm_supp_cost) \ + and warehouse_account.get(self.supplier_warehouse) \ + and warehouse_account[d.warehouse]["account"] == warehouse_account[self.supplier_warehouse]["account"]: + continue + gl_entries.append(self.get_gl_dict({ "account": warehouse_account[d.warehouse]["account"], "against": stock_rbnb, @@ -242,16 +250,16 @@ class PurchaseReceipt(BuyingController): credit_amount = flt(d.base_net_amount, d.precision("base_net_amount")) \ if credit_currency == self.company_currency else flt(d.net_amount, d.precision("net_amount")) - - gl_entries.append(self.get_gl_dict({ - "account": warehouse_account[d.from_warehouse]['account'] \ - if d.from_warehouse else stock_rbnb, - "against": warehouse_account[d.warehouse]["account"], - "cost_center": d.cost_center, - "remarks": self.get("remarks") or _("Accounting Entry for Stock"), - "debit": -1 * flt(d.base_net_amount, d.precision("base_net_amount")), - "debit_in_account_currency": -1 * credit_amount - }, credit_currency, item=d)) + if credit_amount: + gl_entries.append(self.get_gl_dict({ + "account": warehouse_account[d.from_warehouse]['account'] \ + if d.from_warehouse else stock_rbnb, + "against": warehouse_account[d.warehouse]["account"], + "cost_center": d.cost_center, + "remarks": self.get("remarks") or _("Accounting Entry for Stock"), + "debit": -1 * flt(d.base_net_amount, d.precision("base_net_amount")), + "debit_in_account_currency": -1 * credit_amount + }, credit_currency, item=d)) negative_expense_to_be_booked += flt(d.item_tax_amount) diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index d97b9e82c3..67161aa6dd 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals import unittest +import json import frappe, erpnext import frappe.defaults from frappe.utils import cint, flt, cstr, today, random_string @@ -18,6 +19,28 @@ class TestPurchaseReceipt(unittest.TestCase): set_perpetual_inventory(0) frappe.db.set_value("Buying Settings", None, "allow_multiple_items", 1) + def test_reverse_purchase_receipt_sle(self): + + frappe.db.set_value('UOM', '_Test UOM', 'must_be_whole_number', 0) + + pr = make_purchase_receipt(qty=0.5) + + sl_entry = frappe.db.get_all("Stock Ledger Entry", {"voucher_type": "Purchase Receipt", + "voucher_no": pr.name}, ['actual_qty']) + + self.assertEqual(len(sl_entry), 1) + self.assertEqual(sl_entry[0].actual_qty, 0.5) + + pr.cancel() + + sl_entry_cancelled = frappe.db.get_all("Stock Ledger Entry", {"voucher_type": "Purchase Receipt", + "voucher_no": pr.name}, ['actual_qty'], order_by='creation') + + self.assertEqual(len(sl_entry_cancelled), 2) + self.assertEqual(sl_entry_cancelled[1].actual_qty, -0.5) + + frappe.db.set_value('UOM', '_Test UOM', 'must_be_whole_number', 1) + def test_make_purchase_invoice(self): pr = make_purchase_receipt(do_not_save=True) self.assertRaises(frappe.ValidationError, make_purchase_invoice, pr.name) @@ -121,6 +144,87 @@ class TestPurchaseReceipt(unittest.TestCase): rm_supp_cost = sum([d.amount for d in pr.get("supplied_items")]) self.assertEqual(pr.get("items")[0].rm_supp_cost, flt(rm_supp_cost, 2)) + def test_subcontracting_gle_fg_item_rate_zero(self): + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + set_perpetual_inventory() + frappe.db.set_value("Buying Settings", None, "backflush_raw_materials_of_subcontract_based_on", "BOM") + make_stock_entry(item_code="_Test Item", target="Work In Progress - TCP1", qty=100, basic_rate=100, company="_Test Company with perpetual inventory") + make_stock_entry(item_code="_Test Item Home Desktop 100", target="Work In Progress - TCP1", + qty=100, basic_rate=100, company="_Test Company with perpetual inventory") + pr = make_purchase_receipt(item_code="_Test FG Item", qty=10, rate=0, is_subcontracted="Yes", + company="_Test Company with perpetual inventory", warehouse='Stores - TCP1', supplier_warehouse='Work In Progress - TCP1') + + gl_entries = get_gl_entries("Purchase Receipt", pr.name) + + self.assertFalse(gl_entries) + + set_perpetual_inventory(0) + + def test_subcontracting_over_receipt(self): + """ + Behaviour: Raise multiple PRs against one PO that in total + receive more than the required qty in the PO. + Expected Result: Error Raised for Over Receipt against PO. + """ + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + from erpnext.buying.doctype.purchase_order.test_purchase_order import (update_backflush_based_on, + make_subcontracted_item, create_purchase_order) + from erpnext.buying.doctype.purchase_order.purchase_order import (make_purchase_receipt, + make_rm_stock_entry as make_subcontract_transfer_entry) + + update_backflush_based_on("Material Transferred for Subcontract") + item_code = "_Test Subcontracted FG Item 1" + make_subcontracted_item(item_code) + + po = create_purchase_order(item_code=item_code, qty=1, + is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC") + + #stock raw materials in a warehouse before transfer + make_stock_entry(target="_Test Warehouse - _TC", + item_code="_Test Item Home Desktop 100", qty=1, basic_rate=100) + make_stock_entry(target="_Test Warehouse - _TC", + item_code = "Test Extra Item 1", qty=1, basic_rate=100) + make_stock_entry(target="_Test Warehouse - _TC", + item_code = "_Test Item", qty=1, basic_rate=100) + + rm_items = [ + { + "item_code": item_code, + "rm_item_code": po.supplied_items[0].rm_item_code, + "item_name": "_Test Item", + "qty": po.supplied_items[0].required_qty, + "warehouse": "_Test Warehouse - _TC", + "stock_uom": "Nos" + }, + { + "item_code": item_code, + "rm_item_code": po.supplied_items[1].rm_item_code, + "item_name": "Test Extra Item 1", + "qty": po.supplied_items[1].required_qty, + "warehouse": "_Test Warehouse - _TC", + "stock_uom": "Nos" + }, + { + "item_code": item_code, + "rm_item_code": po.supplied_items[2].rm_item_code, + "item_name": "_Test Item Home Desktop 100", + "qty": po.supplied_items[2].required_qty, + "warehouse": "_Test Warehouse - _TC", + "stock_uom": "Nos" + } + ] + rm_item_string = json.dumps(rm_items) + se = frappe.get_doc(make_subcontract_transfer_entry(po.name, rm_item_string)) + se.to_warehouse = "_Test Warehouse 1 - _TC" + se.save() + se.submit() + + pr1 = make_purchase_receipt(po.name) + pr2 = make_purchase_receipt(po.name) + + pr1.submit() + self.assertRaises(frappe.ValidationError, pr2.submit) + def test_serial_no_supplier(self): pr = make_purchase_receipt(item_code="_Test Serialized Item With Series", qty=1) self.assertEqual(frappe.db.get_value("Serial No", pr.get("items")[0].serial_no, "supplier"), @@ -688,7 +792,7 @@ def make_purchase_receipt(**args): "received_qty": received_qty, "rejected_qty": rejected_qty, "rejected_warehouse": args.rejected_warehouse or "_Test Rejected Warehouse - _TC" if rejected_qty != 0 else "", - "rate": args.rate or 50, + "rate": args.rate if args.rate != None else 50, "conversion_factor": args.conversion_factor or 1.0, "serial_no": args.serial_no, "stock_uom": args.stock_uom or "_Test UOM", diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.json b/erpnext/stock/doctype/quality_inspection/quality_inspection.json index c951066aa8..3643174fb4 100644 --- a/erpnext/stock/doctype/quality_inspection/quality_inspection.json +++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.json @@ -73,7 +73,8 @@ "fieldname": "reference_type", "fieldtype": "Select", "label": "Reference Type", - "options": "\nPurchase Receipt\nPurchase Invoice\nDelivery Note\nSales Invoice\nStock Entry" + "options": "\nPurchase Receipt\nPurchase Invoice\nDelivery Note\nSales Invoice\nStock Entry", + "reqd": 1 }, { "fieldname": "reference_name", @@ -84,7 +85,8 @@ "label": "Reference Name", "oldfieldname": "purchase_receipt_no", "oldfieldtype": "Link", - "options": "reference_type" + "options": "reference_type", + "reqd": 1 }, { "fieldname": "section_break_7", @@ -231,9 +233,10 @@ ], "icon": "fa fa-search", "idx": 1, + "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-04-26 17:50:25.068222", + "modified": "2020-09-12 16:11:31.910508", "modified_by": "Administrator", "module": "Stock", "name": "Quality Inspection", diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 30bcccdda6..a92d04ff8c 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -513,7 +513,7 @@ class StockEntry(StockController): d.basic_amount = flt((raw_material_cost - scrap_material_cost), d.precision("basic_amount")) elif self.purpose == "Repack" and total_fg_qty and not d.set_basic_rate_manually: d.basic_rate = flt(raw_material_cost) / flt(total_fg_qty) - d.basic_amount = d.basic_rate * d.qty + d.basic_amount = d.basic_rate * flt(d.qty) def distribute_additional_costs(self): if self.purpose == "Material Issue": diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 43fbc00466..b81f8a086d 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -258,6 +258,7 @@ class StockReconciliation(StockController): sl_entries.append(args) + qty_after_transaction = 0 for serial_no in serial_nos: args = self.get_sle_for_items(row, [serial_no]) @@ -271,11 +272,19 @@ class StockReconciliation(StockController): if previous_sle and row.warehouse != previous_sle.get("warehouse"): # If serial no exists in different warehouse + warehouse = previous_sle.get("warehouse", '') or row.warehouse + + if not qty_after_transaction: + qty_after_transaction = get_stock_balance(row.item_code, + warehouse, self.posting_date, self.posting_time) + + qty_after_transaction -= 1 + new_args = args.copy() new_args.update({ 'actual_qty': -1, - 'qty_after_transaction': cint(previous_sle.get('qty_after_transaction')) - 1, - 'warehouse': previous_sle.get("warehouse", '') or row.warehouse, + 'qty_after_transaction': qty_after_transaction, + 'warehouse': warehouse, 'valuation_rate': previous_sle.get("valuation_rate") }) diff --git a/erpnext/stock/report/stock_qty_vs_serial_no_count/__init__.py b/erpnext/stock/report/stock_qty_vs_serial_no_count/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.js b/erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.js new file mode 100644 index 0000000000..2a0fd4025c --- /dev/null +++ b/erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.js @@ -0,0 +1,42 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Stock Qty vs Serial No Count"] = { + "filters": [ + { + "fieldname":"company", + "label": __("Company"), + "fieldtype": "Link", + "options": "Company", + "default": frappe.defaults.get_user_default("Company"), + "reqd": 1 + }, + { + "fieldname":"warehouse", + "label": __("Warehouse"), + "fieldtype": "Link", + "options": "Warehouse", + "get_query": function() { + const company = frappe.query_report.get_filter_value('company'); + return { + filters: { 'company': company } + } + }, + "reqd": 1 + }, + ], + + "formatter": function (value, row, column, data, default_formatter) { + value = default_formatter(value, row, column, data); + if (column.fieldname == "difference" && data) { + if (data.difference > 0) { + value = "" + value + ""; + } + else if (data.difference < 0) { + value = "" + value + ""; + } + } + return value; + } +}; diff --git a/erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.json b/erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.json new file mode 100644 index 0000000000..c7108b513e --- /dev/null +++ b/erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.json @@ -0,0 +1,27 @@ +{ + "add_total_row": 0, + "creation": "2020-07-23 19:31:32.395011", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 0, + "is_standard": "Yes", + "modified": "2020-07-23 19:32:02.168185", + "modified_by": "Administrator", + "module": "Stock", + "name": "Stock Qty vs Serial No Count", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Item", + "report_name": "Stock Qty vs Serial No Count", + "report_type": "Script Report", + "roles": [ + { + "role": "Stock Manager" + }, + { + "role": "Stock User" + } + ] +} \ No newline at end of file diff --git a/erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py b/erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py new file mode 100644 index 0000000000..55f041c95c --- /dev/null +++ b/erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py @@ -0,0 +1,80 @@ +# 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 _ + +def execute(filters=None): + columns = get_columns() + data = get_data(filters.warehouse) + return columns, data + +def get_columns(): + columns = [ + { + "label": _("Item Code"), + "fieldname": "item_code", + "fieldtype": "Link", + "options": "Item", + "width": 200 + }, + { + "label": _("Item Name"), + "fieldname": "item_name", + "fieldtype": "Data", + "width": 200 + }, + { + "label": _("Serial No Count"), + "fieldname": "total", + "fieldtype": "Float", + "width": 150 + }, + { + "label": _("Stock Qty"), + "fieldname": "stock_qty", + "fieldtype": "Float", + "width": 150 + }, + { + "label": _("Difference"), + "fieldname": "difference", + "fieldtype": "Float", + "width": 150 + }, + ] + + return columns + +def get_data(warehouse): + serial_item_list = frappe.get_all("Item", filters={ + 'has_serial_no': True, + }, fields=['item_code', 'item_name']) + + status_list = ['Active', 'Expired'] + data = [] + for item in serial_item_list: + total_serial_no = frappe.db.count("Serial No", + filters={"item_code": item.item_code, "status": ("in", status_list), "warehouse": warehouse}) + + actual_qty = frappe.db.get_value('Bin', fieldname=['actual_qty'], + filters={"warehouse": warehouse, "item_code": item.item_code}) + + # frappe.db.get_value returns null if no record exist. + if not actual_qty: + actual_qty = 0 + + difference = total_serial_no - actual_qty + + row = { + "item_code": item.item_code, + "item_name": item.item_name, + "total": total_serial_no, + "stock_qty": actual_qty, + "difference": difference, + } + + data.append(row) + + return data \ No newline at end of file diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index e1b3730f2f..f4490f1b01 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -31,7 +31,7 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc sle['posting_time'] = now_datetime().strftime('%H:%M:%S.%f') if cancel: - sle['actual_qty'] = -flt(sle.get('actual_qty'), 0) + sle['actual_qty'] = -flt(sle.get('actual_qty')) if sle['actual_qty'] < 0 and not sle.get('outgoing_rate'): sle['outgoing_rate'] = get_incoming_outgoing_rate_for_cancel(sle.item_code, diff --git a/erpnext/support/doctype/issue/issue.js b/erpnext/support/doctype/issue/issue.js index 9e15757ce0..858564a527 100644 --- a/erpnext/support/doctype/issue/issue.js +++ b/erpnext/support/doctype/issue/issue.js @@ -209,11 +209,11 @@ function set_time_to_resolve_and_response(frm) { frm.dashboard.set_headline_alert( '
    ' + - '
    ' + - ' ' + + '
    ' + + 'Time to Respond: '+ time_to_respond.diff_display +' ' + '
    ' + - '
    ' + - ' ' + + '
    ' + + 'Time to Resolve: '+ time_to_resolve.diff_display +' ' + '
    ' + '
    ' ); diff --git a/erpnext/templates/generators/item/item_add_to_cart.html b/erpnext/templates/generators/item/item_add_to_cart.html index 2a70d8dbe9..40bc0c749b 100644 --- a/erpnext/templates/generators/item/item_add_to_cart.html +++ b/erpnext/templates/generators/item/item_add_to_cart.html @@ -27,22 +27,25 @@ {% endif %}
    {% endif %} - {% if product_info.price and (cart_settings.allow_items_not_in_stock or product_info.in_stock) %}
    - - {{ _("View in Cart") }} - - + {% if product_info.price and (cart_settings.allow_items_not_in_stock or product_info.in_stock) %} + + {{ _("View in Cart") }} + + + {% endif %} + {% if cart_settings.show_contact_us_button %} + {% include "templates/generators/item/item_inquiry.html" %} + {% endif %}
    - {% endif %}
    diff --git a/erpnext/templates/generators/item/item_configure.html b/erpnext/templates/generators/item/item_configure.html index b8b0d98bdc..73f9ec99b3 100644 --- a/erpnext/templates/generators/item/item_configure.html +++ b/erpnext/templates/generators/item/item_configure.html @@ -10,14 +10,11 @@ {{ _('Configure') }} {% endif %} - {% if cart_settings.show_contact_us_button | int %} - + {% if cart_settings.show_contact_us_button %} + {% include "templates/generators/item/item_inquiry.html" %} {% endif %}
    {% endif %} diff --git a/erpnext/templates/generators/item/item_inquiry.html b/erpnext/templates/generators/item/item_inquiry.html new file mode 100644 index 0000000000..83653b6821 --- /dev/null +++ b/erpnext/templates/generators/item/item_inquiry.html @@ -0,0 +1,11 @@ +{% if shopping_cart and shopping_cart.cart_settings.enabled %} +{% set cart_settings = shopping_cart.cart_settings %} + {% if cart_settings.show_contact_us_button | int %} + + {% endif %} + +{% endif %} diff --git a/erpnext/templates/generators/item/item_inquiry.js b/erpnext/templates/generators/item/item_inquiry.js index 52ddae2624..e7db3a368d 100644 --- a/erpnext/templates/generators/item/item_inquiry.js +++ b/erpnext/templates/generators/item/item_inquiry.js @@ -20,6 +20,13 @@ frappe.ready(() => { options: 'Email', reqd: 1 }, + { + fieldtype: 'Data', + label: __('Phone Number'), + fieldname: 'phone', + options: 'Phone', + reqd: 1 + }, { fieldtype: 'Data', label: __('Subject'), diff --git a/erpnext/templates/print_formats/includes/item_table_qty.html b/erpnext/templates/print_formats/includes/item_table_qty.html index 239859eea1..ecaaef42b5 100644 --- a/erpnext/templates/print_formats/includes/item_table_qty.html +++ b/erpnext/templates/print_formats/includes/item_table_qty.html @@ -1,6 +1,15 @@ -{% if (doc.uom and not doc.is_print_hide("uom")) %} - {{ _(doc.uom) }} -{% elif (doc.stock_uom and not doc.is_print_hide("stock_uom")) %} - {{ _(doc.stock_uom) }} +{% set qty_first=frappe.db.get_single_value("Print Settings", "print_uom_after_quantity") %} +{% if qty_first %} + {{ doc.get_formatted("qty", doc) }} + {% if (doc.uom and not doc.is_print_hide("uom")) %} {{ _(doc.uom) }} + {% elif (doc.stock_uom and not doc.is_print_hide("stock_uom")) %} {{ _(doc.stock_uom) }} + {%- endif %} +{% else %} + {% if (doc.uom and not doc.is_print_hide("uom")) %} + {{ _(doc.uom) }} + {% elif (doc.stock_uom and not doc.is_print_hide("stock_uom")) %} + {{ _(doc.stock_uom) }} + {%- endif %} + {{ doc.get_formatted("qty", doc) }} {%- endif %} -{{ doc.get_formatted("qty", doc) }} + diff --git a/erpnext/utilities/product.py b/erpnext/utilities/product.py index c23c1f7096..66d6cd3888 100644 --- a/erpnext/utilities/product.py +++ b/erpnext/utilities/product.py @@ -82,6 +82,7 @@ def get_price(item_code, price_list, customer_group, company, qty=1): pricing_rule = get_pricing_rule_for_item(frappe._dict({ "item_code": item_code, "qty": qty, + "stock_qty": qty, "transaction_type": "selling", "price_list": price_list, "customer_group": customer_group, diff --git a/yarn.lock b/yarn.lock index b19f566fd0..97a063597d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -282,9 +282,9 @@ balanced-match@^1.0.0: integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= bl@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/bl/-/bl-3.0.0.tgz#3611ec00579fd18561754360b21e9f784500ff88" - integrity sha512-EUAyP5UHU5hxF8BPT0LKW8gjYLhq1DQIcneOX/pL/m2Alo+OYDQAJlHq+yseMP50Os2nHXOSic6Ss3vSQeyf4A== + version "3.0.1" + resolved "https://registry.yarnpkg.com/bl/-/bl-3.0.1.tgz#1cbb439299609e419b5a74d7fce2f8b37d8e5c6f" + integrity sha512-jrCW5ZhfQ/Vt07WX1Ngs+yn9BDqPL/gw28S7s9H6QK/gupnizNzJAss5akW20ISgOrbLTlXOOCTJeNUQqruAWQ== dependencies: readable-stream "^3.0.1" @@ -866,12 +866,12 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@^2.0.3, inherits@~2.0.3: +inherits@2, inherits@~2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= -inherits@2.0.4, inherits@~2.0.1: +inherits@2.0.4, inherits@^2.0.3, inherits@~2.0.1: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -1447,9 +1447,9 @@ readable-stream@2, readable-stream@~2.3.6: util-deprecate "~1.0.1" readable-stream@^3.0.1, readable-stream@^3.1.1: - version "3.5.0" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.5.0.tgz#465d70e6d1087f6162d079cd0b5db7fbebfd1606" - integrity sha512-gSz026xs2LfxBPudDuI41V1lka8cxg64E66SGe78zJlsUofOg/yqwezdIcdfwik6B4h8LFmWPA9ef9X3FiNFLA== + version "3.6.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" + integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== dependencies: inherits "^2.0.3" string_decoder "^1.1.1" @@ -1505,9 +1505,9 @@ safe-buffer@^5.0.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1: integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== safe-buffer@~5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519" - integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg== + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== "safer-buffer@>= 2.1.2 < 3": version "2.1.2"