diff --git a/erpnext/accounts/desk_page/accounting/accounting.json b/erpnext/accounts/desk_page/accounting/accounting.json index a2497838ee..2c5231491c 100644 --- a/erpnext/accounts/desk_page/accounting/accounting.json +++ b/erpnext/accounts/desk_page/accounting/accounting.json @@ -43,7 +43,7 @@ { "hidden": 0, "label": "Bank Statement", - "links": "[\n {\n \"label\": \"Bank\",\n \"name\": \"Bank\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Account\",\n \"name\": \"Bank Account\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Statement Transaction Entry\",\n \"name\": \"Bank Statement Transaction Entry\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Statement Settings\",\n \"name\": \"Bank Statement Settings\",\n \"type\": \"doctype\"\n }\n]" + "links": "[\n {\n \"label\": \"Bank\",\n \"name\": \"Bank\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Account\",\n \"name\": \"Bank Account\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Reconciliation\",\n \"name\": \"bank-reconciliation\",\n \"type\": \"page\"\n },\n {\n \"label\": \"Bank Clearance\",\n \"name\": \"Bank Clearance\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Statement Transaction Entry\",\n \"name\": \"Bank Statement Transaction Entry\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Statement Settings\",\n \"name\": \"Bank Statement Settings\",\n \"type\": \"doctype\"\n }\n]" }, { "hidden": 0, @@ -98,7 +98,7 @@ "idx": 0, "is_standard": 1, "label": "Accounting", - "modified": "2020-06-19 12:42:44.054598", + "modified": "2020-09-03 10:37:07.865801", "modified_by": "Administrator", "module": "Accounts", "name": "Accounting", @@ -158,4 +158,4 @@ "type": "Dashboard" } ] -} +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/account/account.py b/erpnext/accounts/doctype/account/account.py index c6de6410eb..164f120067 100644 --- a/erpnext/accounts/doctype/account/account.py +++ b/erpnext/accounts/doctype/account/account.py @@ -244,6 +244,8 @@ class Account(NestedSet): super(Account, self).on_trash(True) +@frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def get_parent_account(doctype, txt, searchfield, start, page_len, filters): return frappe.db.sql("""select name from tabAccount where is_group = 1 and docstatus != 2 and company = %s diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/chart_of_accounts.py b/erpnext/accounts/doctype/account/chart_of_accounts/chart_of_accounts.py index 1bf9196a4f..0e3b24cda3 100644 --- a/erpnext/accounts/doctype/account/chart_of_accounts/chart_of_accounts.py +++ b/erpnext/accounts/doctype/account/chart_of_accounts/chart_of_accounts.py @@ -225,7 +225,7 @@ def build_tree_from_json(chart_template, chart_data=None): account['parent_account'] = parent account['expandable'] = True if identify_is_group(child) else False - account['value'] = (child.get('account_number') + ' - ' + account_name) \ + account['value'] = (cstr(child.get('account_number')).strip() + ' - ' + account_name) \ if child.get('account_number') else account_name accounts.append(account) _import_accounts(child, account['value']) diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json index 8ca8b71ef8..b2e8b090c7 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json @@ -225,7 +225,7 @@ "idx": 1, "issingle": 1, "links": [], - "modified": "2020-06-22 20:13:26.043092", + "modified": "2020-08-03 20:13:26.043092", "modified_by": "Administrator", "module": "Accounts", "name": "Accounts Settings", diff --git a/erpnext/accounts/doctype/bank_clearance/bank_clearance.py b/erpnext/accounts/doctype/bank_clearance/bank_clearance.py index 6fec3ab368..76d82e7339 100644 --- a/erpnext/accounts/doctype/bank_clearance/bank_clearance.py +++ b/erpnext/accounts/doctype/bank_clearance/bank_clearance.py @@ -60,12 +60,12 @@ class BankClearance(Document): """.format(condition=condition), {"account": self.account, "from":self.from_date, "to": self.to_date, "bank_account": self.bank_account}, as_dict=1) - pos_entries = [] + pos_sales_invoices, pos_purchase_invoices = [], [] if self.include_pos_transactions: - pos_entries = frappe.db.sql(""" + pos_sales_invoices = frappe.db.sql(""" select "Sales Invoice Payment" as payment_document, sip.name as payment_entry, sip.amount as debit, - si.posting_date, si.debit_to as against_account, sip.clearance_date, + si.posting_date, si.customer as against_account, sip.clearance_date, account.account_currency, 0 as credit from `tabSales Invoice Payment` sip, `tabSales Invoice` si, `tabAccount` account where @@ -75,7 +75,20 @@ class BankClearance(Document): si.posting_date ASC, si.name DESC """, {"account":self.account, "from":self.from_date, "to":self.to_date}, as_dict=1) - entries = sorted(list(payment_entries)+list(journal_entries+list(pos_entries)), + pos_purchase_invoices = frappe.db.sql(""" + select + "Purchase Invoice" as payment_document, pi.name as payment_entry, pi.paid_amount as credit, + pi.posting_date, pi.supplier as against_account, pi.clearance_date, + account.account_currency, 0 as debit + from `tabPurchase Invoice` pi, `tabAccount` account + where + pi.cash_bank_account=%(account)s and pi.docstatus=1 and account.name = pi.cash_bank_account + and pi.posting_date >= %(from)s and pi.posting_date <= %(to)s + order by + pi.posting_date ASC, pi.name DESC + """, {"account": self.account, "from": self.from_date, "to": self.to_date}, as_dict=1) + + entries = sorted(list(payment_entries) + list(journal_entries + list(pos_sales_invoices) + list(pos_purchase_invoices)), key=lambda k: k['posting_date'] or getdate(nowdate())) self.set('payment_entries', []) diff --git a/erpnext/accounts/doctype/bank_guarantee/bank_guarantee.py b/erpnext/accounts/doctype/bank_guarantee/bank_guarantee.py index f28a07431f..88e1055beb 100644 --- a/erpnext/accounts/doctype/bank_guarantee/bank_guarantee.py +++ b/erpnext/accounts/doctype/bank_guarantee/bank_guarantee.py @@ -27,4 +27,4 @@ def get_vouchar_detials(column_list, doctype, docname): for col in column_list: sanitize_searchfield(col) return frappe.db.sql(''' select {columns} from `tab{doctype}` where name=%s''' - .format(columns=", ".join(json.loads(column_list)), doctype=doctype), docname, as_dict=1)[0] + .format(columns=", ".join(column_list), doctype=doctype), docname, as_dict=1)[0] 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/chart_of_accounts_importer/chart_of_accounts_importer.js b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js index 0b7cff3d63..2235298201 100644 --- a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js +++ b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js @@ -135,7 +135,7 @@ var create_import_button = function(frm) { callback: function(r) { if(!r.exc) { clearInterval(frm.page["interval"]); - frm.page.set_indicator(__('Import Successfull'), 'blue'); + frm.page.set_indicator(__('Import Successful'), 'blue'); create_reset_button(frm); } } 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/dunning/dunning.js b/erpnext/accounts/doctype/dunning/dunning.js index c563368894..9909c6c2ab 100644 --- a/erpnext/accounts/doctype/dunning/dunning.js +++ b/erpnext/accounts/doctype/dunning/dunning.js @@ -44,6 +44,19 @@ frappe.ui.form.on("Dunning", { ); frm.page.set_inner_btn_group_as_primary(__("Create")); } + + if(frm.doc.docstatus > 0) { + frm.add_custom_button(__('Ledger'), function() { + frappe.route_options = { + "voucher_no": frm.doc.name, + "from_date": frm.doc.posting_date, + "to_date": frm.doc.posting_date, + "company": frm.doc.company, + "show_cancelled_entries": frm.doc.docstatus === 2 + }; + frappe.set_route("query-report", "General Ledger"); + }, __('View')); + } }, overdue_days: function (frm) { frappe.db.get_value( @@ -125,9 +138,9 @@ frappe.ui.form.on("Dunning", { }, calculate_interest_and_amount: function (frm) { const interest_per_year = frm.doc.outstanding_amount * frm.doc.rate_of_interest / 100; - const interest_amount = interest_per_year / 365 * frm.doc.overdue_days || 0; - const dunning_amount = interest_amount + frm.doc.dunning_fee; - const grand_total = frm.doc.outstanding_amount + dunning_amount; + const interest_amount = flt((interest_per_year * cint(frm.doc.overdue_days)) / 365 || 0, precision('interest_amount')); + const dunning_amount = flt(interest_amount + frm.doc.dunning_fee, precision('dunning_amount')); + const grand_total = flt(frm.doc.outstanding_amount + dunning_amount, precision('grand_total')); frm.set_value("interest_amount", interest_amount); frm.set_value("dunning_amount", dunning_amount); frm.set_value("grand_total", grand_total); diff --git a/erpnext/accounts/doctype/dunning/dunning.json b/erpnext/accounts/doctype/dunning/dunning.json index b3eddf5f22..d55bfd1ac4 100644 --- a/erpnext/accounts/doctype/dunning/dunning.json +++ b/erpnext/accounts/doctype/dunning/dunning.json @@ -29,10 +29,10 @@ "company_address_display", "section_break_6", "dunning_type", - "interest_amount", + "dunning_fee", "column_break_8", "rate_of_interest", - "dunning_fee", + "interest_amount", "section_break_12", "dunning_amount", "grand_total", @@ -215,7 +215,7 @@ }, { "default": "0", - "fetch_from": "dunning_type.interest_rate", + "fetch_from": "dunning_type.rate_of_interest", "fetch_if_empty": 1, "fieldname": "rate_of_interest", "fieldtype": "Float", @@ -315,7 +315,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2020-07-21 18:20:23.512151", + "modified": "2020-08-03 18:55:43.683053", "modified_by": "Administrator", "module": "Accounts", "name": "Dunning", diff --git a/erpnext/accounts/doctype/dunning/dunning.py b/erpnext/accounts/doctype/dunning/dunning.py index 0be6a480c9..1a6dbedf56 100644 --- a/erpnext/accounts/doctype/dunning/dunning.py +++ b/erpnext/accounts/doctype/dunning/dunning.py @@ -6,7 +6,7 @@ from __future__ import unicode_literals import frappe import json from six import string_types -from frappe.utils import getdate, get_datetime, rounded, flt +from frappe.utils import getdate, get_datetime, rounded, flt, cint from erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual import days_in_year from erpnext.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions @@ -27,11 +27,11 @@ class Dunning(AccountsController): amounts = calculate_interest_and_amount( self.posting_date, self.outstanding_amount, self.rate_of_interest, self.dunning_fee, self.overdue_days) if self.interest_amount != amounts.get('interest_amount'): - self.interest_amount = amounts.get('interest_amount') + self.interest_amount = flt(amounts.get('interest_amount'), self.precision('interest_amount')) if self.dunning_amount != amounts.get('dunning_amount'): - self.dunning_amount = amounts.get('dunning_amount') + self.dunning_amount = flt(amounts.get('dunning_amount'), self.precision('dunning_amount')) if self.grand_total != amounts.get('grand_total'): - self.grand_total = amounts.get('grand_total') + self.grand_total = flt(amounts.get('grand_total'), self.precision('grand_total')) def on_submit(self): self.make_gl_entries() @@ -47,10 +47,13 @@ class Dunning(AccountsController): gl_entries = [] invoice_fields = ["project", "cost_center", "debit_to", "party_account_currency", "conversion_rate", "cost_center"] inv = frappe.db.get_value("Sales Invoice", self.sales_invoice, invoice_fields, as_dict=1) + accounting_dimensions = get_accounting_dimensions() invoice_fields.extend(accounting_dimensions) + dunning_in_company_currency = flt(self.dunning_amount * inv.conversion_rate) default_cost_center = frappe.get_cached_value('Company', self.company, 'cost_center') + gl_entries.append( self.get_gl_dict({ "account": inv.debit_to, @@ -90,10 +93,10 @@ def resolve_dunning(doc, state): def calculate_interest_and_amount(posting_date, outstanding_amount, rate_of_interest, dunning_fee, overdue_days): interest_amount = 0 + grand_total = 0 if rate_of_interest: - interest_per_year = rounded(flt(outstanding_amount) * flt(rate_of_interest))/100 - interest_amount = ( - interest_per_year / days_in_year(get_datetime(posting_date).year)) * int(overdue_days) + interest_per_year = flt(outstanding_amount) * flt(rate_of_interest) / 100 + interest_amount = (interest_per_year * cint(overdue_days)) / 365 grand_total = flt(outstanding_amount) + flt(interest_amount) + flt(dunning_fee) dunning_amount = flt(interest_amount) + flt(dunning_fee) return { diff --git a/erpnext/accounts/doctype/dunning/dunning_dashboard.py b/erpnext/accounts/doctype/dunning/dunning_dashboard.py new file mode 100644 index 0000000000..19a73ddfa4 --- /dev/null +++ b/erpnext/accounts/doctype/dunning/dunning_dashboard.py @@ -0,0 +1,17 @@ +from __future__ import unicode_literals +from frappe import _ + +def get_data(): + return { + 'fieldname': 'dunning', + 'non_standard_fieldnames': { + 'Journal Entry': 'reference_name', + 'Payment Entry': 'reference_name' + }, + 'transactions': [ + { + 'label': _('Payment'), + 'items': ['Payment Entry', 'Journal Entry'] + } + ] + } \ No newline at end of file diff --git a/erpnext/accounts/doctype/fiscal_year/fiscal_year_dashboard.py b/erpnext/accounts/doctype/fiscal_year/fiscal_year_dashboard.py index c7604ec7cc..58480df119 100644 --- a/erpnext/accounts/doctype/fiscal_year/fiscal_year_dashboard.py +++ b/erpnext/accounts/doctype/fiscal_year/fiscal_year_dashboard.py @@ -13,7 +13,7 @@ def get_data(): }, { 'label': _('References'), - 'items': ['Period Closing Voucher', 'Request for Quotation', 'Tax Withholding Category'] + 'items': ['Period Closing Voucher', 'Tax Withholding Category'] }, { 'label': _('Target Details'), 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 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_profile/pos_profile.js b/erpnext/accounts/doctype/pos_profile/pos_profile.js index ef431d7d41..8ec6a53626 100755 --- a/erpnext/accounts/doctype/pos_profile/pos_profile.js +++ b/erpnext/accounts/doctype/pos_profile/pos_profile.js @@ -31,8 +31,7 @@ frappe.ui.form.on('POS Profile', { frm.set_query("print_format", function() { return { filters: [ - ['Print Format', 'doc_type', '=', 'Sales Invoice'], - ['Print Format', 'print_format_type', '=', 'Jinja'], + ['Print Format', 'doc_type', '=', 'POS Invoice'] ] }; }); @@ -45,10 +44,6 @@ frappe.ui.form.on('POS Profile', { }; }); - frm.set_query("print_format", function() { - return { filters: { doc_type: "Sales Invoice", print_format_type: "JS"} }; - }); - frm.set_query('company_address', function(doc) { if(!doc.company) { frappe.throw(__('Please set Company')); diff --git a/erpnext/accounts/doctype/pos_profile/pos_profile.json b/erpnext/accounts/doctype/pos_profile/pos_profile.json index 454c598d63..d4c1791789 100644 --- a/erpnext/accounts/doctype/pos_profile/pos_profile.json +++ b/erpnext/accounts/doctype/pos_profile/pos_profile.json @@ -302,10 +302,10 @@ "fieldname": "warehouse", "fieldtype": "Link", "label": "Warehouse", + "mandatory_depends_on": "update_stock", "oldfieldname": "warehouse", "oldfieldtype": "Link", - "options": "Warehouse", - "reqd": 1 + "options": "Warehouse" }, { "default": "0", @@ -350,4 +350,4 @@ ], "sort_field": "modified", "sort_order": "DESC" -} \ No newline at end of file +} diff --git a/erpnext/accounts/doctype/pos_profile/pos_profile.py b/erpnext/accounts/doctype/pos_profile/pos_profile.py index 8655b4bf3a..789b4c3bd9 100644 --- a/erpnext/accounts/doctype/pos_profile/pos_profile.py +++ b/erpnext/accounts/doctype/pos_profile/pos_profile.py @@ -105,6 +105,7 @@ def get_series(): return frappe.get_meta("POS Invoice").get_field("naming_series").options or "s" @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def pos_profile_query(doctype, txt, searchfield, start, page_len, filters): user = frappe.session['user'] company = filters.get('company') or frappe.defaults.get_user_default('company') 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 d90ae28e5a..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) @@ -433,14 +433,14 @@ def make_pricing_rule(doctype, docname): return doc @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def get_item_uoms(doctype, txt, searchfield, start, page_len, filters): items = [filters.get('value')] if filters.get('apply_on') != 'Item Code': field = frappe.scrub(filters.get('apply_on')) + items = [d.name for d in frappe.db.get_all("Item", filters={field: filters.get('value')})] - items = frappe.db.sql_list("""select name - from `tabItem` where {0} = %s""".format(field), filters.get('value')) - - return frappe.get_all('UOM Conversion Detail', - filters = {'parent': ('in', items), 'uom': ("like", "{0}%".format(txt))}, - fields = ["distinct uom"], as_list=1) + return frappe.get_all('UOM Conversion Detail', filters={ + 'parent': ('in', items), + 'uom': ("like", "{0}%".format(txt)) + }, fields = ["distinct uom"], as_list=1) 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/process_statement_of_accounts/__init__.py b/erpnext/accounts/doctype/process_statement_of_accounts/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html new file mode 100644 index 0000000000..e1ddeff61f --- /dev/null +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html @@ -0,0 +1,89 @@ +

{{ filters.party[0] }}

+

{{ _("Statement of Accounts") }}

+ +
+ {{ frappe.format(filters.from_date, 'Date')}} + {{ _("to") }} + {{ frappe.format(filters.to_date, 'Date')}} +
+ + + + + + + + + + + + + + {% for row in data %} + + {% if(row.posting_date) %} + + + + + + {% else %} + + + + + + {% endif %} + + + {% endfor %} + +
{{ _("Date") }}{{ _("Ref") }}{{ _("Party") }}{{ _("Debit") }}{{ _("Credit") }}{{ _("Balance (Dr - Cr)") }}
{{ frappe.format(row.posting_date, 'Date') }}{{ row.voucher_type }} +
{{ row.voucher_no }}
+ {% if not (filters.party or filters.account) %} + {{ row.party or row.account }} +
+ {% endif %} + + {{ _("Against") }}: {{ row.against }} +
{{ _("Remarks") }}: {{ row.remarks }} + {% if row.bill_no %} +
{{ _("Supplier Invoice No") }}: {{ row.bill_no }} + {% endif %} +
+ {{ frappe.utils.fmt_money(row.debit, filters.presentation_currency) }} + {{ frappe.utils.fmt_money(row.credit, filters.presentation_currency) }}{{ frappe.format(row.account, {fieldtype: "Link"}) or " " }} + {{ row.account and frappe.utils.fmt_money(row.debit, filters.presentation_currency) }} + + {{ row.account and frappe.utils.fmt_money(row.credit, filters.presentation_currency) }} + + {{ frappe.utils.fmt_money(row.balance, filters.presentation_currency) }} +
+

+{% if aging %} +

{{ _("Ageing Report Based On ") }} {{ aging.ageing_based_on }}

+
+ {{ _("Up to " ) }} {{ frappe.format(filters.to_date, 'Date')}} +
+
+ + + + + + + + + + + + + + + + + + +
30 Days60 Days90 Days120 Days
{{ aging.range1 }}{{ aging.range2 }}{{ aging.range3 }}{{ aging.range4 }}
+{% endif %} +

Printed On {{ frappe.format(frappe.utils.get_datetime(), 'Datetime') }}

\ No newline at end of file diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js new file mode 100644 index 0000000000..7425132c46 --- /dev/null +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js @@ -0,0 +1,132 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Process Statement Of Accounts', { + view_properties: function(frm) { + frappe.route_options = {doc_type: 'Customer'}; + frappe.set_route("Form", "Customize Form"); + }, + refresh: function(frm){ + if(!frm.doc.__islocal) { + frm.add_custom_button('Send Emails',function(){ + frappe.call({ + method: "erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts.send_emails", + args: { + "document_name": frm.doc.name, + }, + callback: function(r) { + if(r && r.message) { + frappe.show_alert({message: __('Emails Queued'), indicator: 'blue'}); + } + else{ + frappe.msgprint('No Records for these settings.') + } + } + }); + }); + frm.add_custom_button('Download',function(){ + var url = frappe.urllib.get_full_url( + '/api/method/erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts.download_statements?' + + 'document_name='+encodeURIComponent(frm.doc.name)) + $.ajax({ + url: url, + type: 'GET', + success: function(result) { + if(jQuery.isEmptyObject(result)){ + frappe.msgprint('No Records for these settings.'); + } + else{ + window.location = url; + } + } + }); + }); + } + }, + onload: function(frm) { + frm.set_query('currency', function(){ + return { + filters: { + 'enabled': 1 + } + } + }); + if(frm.doc.__islocal){ + frm.set_value('from_date', frappe.datetime.add_months(frappe.datetime.get_today(), -1)); + frm.set_value('to_date', frappe.datetime.get_today()); + } + }, + customer_collection: function(frm){ + frm.set_value('collection_name', ''); + if(frm.doc.customer_collection){ + frm.get_field('collection_name').set_label(frm.doc.customer_collection); + } + }, + frequency: function(frm){ + if(frm.doc.frequency != ''){ + frm.set_value('start_date', frappe.datetime.get_today()); + } + else{ + frm.set_value('start_date', ''); + } + }, + fetch_customers: function(frm){ + if(frm.doc.collection_name){ + frappe.call({ + method: "erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts.fetch_customers", + args: { + 'customer_collection': frm.doc.customer_collection, + 'collection_name': frm.doc.collection_name, + 'primary_mandatory': frm.doc.primary_mandatory + }, + callback: function(r) { + if(!r.exc) { + if(r.message.length){ + frm.clear_table('customers'); + for (const customer of r.message){ + var row = frm.add_child('customers'); + row.customer = customer.name; + row.primary_email = customer.primary_email; + row.billing_email = customer.billing_email; + } + frm.refresh_field('customers'); + } + else{ + frappe.msgprint('No Customers found with selected options.'); + } + } + } + }); + } + else { + frappe.throw('Enter ' + frm.doc.customer_collection + ' name.'); + } + } +}); + +frappe.ui.form.on('Process Statement Of Accounts Customer', { + customer: function(frm, cdt, cdn){ + var row = locals[cdt][cdn]; + if (!row.customer){ + return; + } + frappe.call({ + method: 'erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts.get_customer_emails', + args: { + 'customer_name': row.customer, + 'primary_mandatory': frm.doc.primary_mandatory + }, + callback: function(r){ + if(!r.exe){ + if(r.message.length){ + frappe.model.set_value(cdt, cdn, "primary_email", r.message[0]) + frappe.model.set_value(cdt, cdn, "billing_email", r.message[1]) + } + else { + return + } + } + } + }) + } +}); \ No newline at end of file diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json new file mode 100644 index 0000000000..4be0e2ec06 --- /dev/null +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json @@ -0,0 +1,310 @@ +{ + "actions": [], + "allow_workflow": 1, + "autoname": "Prompt", + "creation": "2020-05-22 16:46:18.712954", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "section_break_11", + "from_date", + "company", + "account", + "group_by", + "cost_center", + "column_break_14", + "to_date", + "finance_book", + "currency", + "project", + "section_break_3", + "customer_collection", + "collection_name", + "fetch_customers", + "column_break_6", + "primary_mandatory", + "column_break_17", + "customers", + "preferences", + "orientation", + "section_break_14", + "include_ageing", + "ageing_based_on", + "section_break_1", + "enable_auto_email", + "section_break_18", + "frequency", + "filter_duration", + "column_break_21", + "start_date", + "section_break_33", + "subject", + "column_break_28", + "cc_to", + "section_break_30", + "body", + "help_text" + ], + "fields": [ + { + "fieldname": "frequency", + "fieldtype": "Select", + "label": "Frequency", + "options": "Weekly\nMonthly\nQuarterly" + }, + { + "fieldname": "company", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Company", + "options": "Company", + "reqd": 1 + }, + { + "depends_on": "eval:doc.enable_auto_email == 0;", + "fieldname": "from_date", + "fieldtype": "Date", + "label": "From Date", + "mandatory_depends_on": "eval:doc.frequency == '';" + }, + { + "depends_on": "eval:doc.enable_auto_email == 0;", + "fieldname": "to_date", + "fieldtype": "Date", + "label": "To Date", + "mandatory_depends_on": "eval:doc.frequency == '';" + }, + { + "fieldname": "cost_center", + "fieldtype": "Table MultiSelect", + "label": "Cost Center", + "options": "PSOA Cost Center" + }, + { + "fieldname": "project", + "fieldtype": "Table MultiSelect", + "label": "Project", + "options": "PSOA Project" + }, + { + "fieldname": "section_break_3", + "fieldtype": "Section Break", + "label": "Customers" + }, + { + "fieldname": "column_break_6", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_11", + "fieldtype": "Section Break", + "label": "General Ledger Filters" + }, + { + "fieldname": "column_break_14", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_17", + "fieldtype": "Section Break", + "hide_border": 1 + }, + { + "fieldname": "customer_collection", + "fieldtype": "Select", + "label": "Select Customers By", + "options": "\nCustomer Group\nTerritory\nSales Partner\nSales Person" + }, + { + "depends_on": "eval: doc.customer_collection !== ''", + "fieldname": "collection_name", + "fieldtype": "Dynamic Link", + "label": "Recipient", + "options": "customer_collection" + }, + { + "fieldname": "section_break_1", + "fieldtype": "Section Break", + "label": "Email Settings" + }, + { + "fieldname": "account", + "fieldtype": "Link", + "label": "Account", + "options": "Account" + }, + { + "fieldname": "finance_book", + "fieldtype": "Link", + "label": "Finance Book", + "options": "Finance Book" + }, + { + "fieldname": "preferences", + "fieldtype": "Section Break", + "label": "Print Preferences" + }, + { + "fieldname": "orientation", + "fieldtype": "Select", + "label": "Orientation", + "options": "Landscape\nPortrait" + }, + { + "default": "Today", + "fieldname": "start_date", + "fieldtype": "Date", + "label": "Start Date" + }, + { + "default": "Group by Voucher (Consolidated)", + "fieldname": "group_by", + "fieldtype": "Select", + "label": "Group By", + "options": "\nGroup by Voucher\nGroup by Voucher (Consolidated)" + }, + { + "fieldname": "currency", + "fieldtype": "Link", + "label": "Currency", + "options": "Currency" + }, + { + "default": "0", + "fieldname": "include_ageing", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Include Ageing Summary" + }, + { + "default": "Due Date", + "depends_on": "eval:doc.include_ageing === 1", + "fieldname": "ageing_based_on", + "fieldtype": "Select", + "label": "Ageing Based On", + "options": "Due Date\nPosting Date" + }, + { + "default": "0", + "fieldname": "enable_auto_email", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Enable Auto Email" + }, + { + "fieldname": "section_break_14", + "fieldtype": "Column Break", + "hide_border": 1 + }, + { + "depends_on": "eval: doc.enable_auto_email ==1", + "fieldname": "section_break_18", + "fieldtype": "Section Break", + "hide_border": 1 + }, + { + "fieldname": "column_break_21", + "fieldtype": "Column Break" + }, + { + "depends_on": "eval: doc.customer_collection !== ''", + "fieldname": "fetch_customers", + "fieldtype": "Button", + "label": "Fetch Customers", + "options": "fetch_customers", + "print_hide": 1, + "report_hide": 1 + }, + { + "default": "1", + "fieldname": "primary_mandatory", + "fieldtype": "Check", + "label": "Send To Primary Contact" + }, + { + "fieldname": "cc_to", + "fieldtype": "Link", + "label": "CC To", + "options": "User" + }, + { + "default": "1", + "fieldname": "filter_duration", + "fieldtype": "Int", + "label": "Filter Duration (Months)" + }, + { + "fieldname": "customers", + "fieldtype": "Table", + "label": "Customers", + "options": "Process Statement Of Accounts Customer", + "reqd": 1 + }, + { + "fieldname": "column_break_28", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_30", + "fieldtype": "Section Break", + "hide_border": 1 + }, + { + "fieldname": "section_break_33", + "fieldtype": "Section Break", + "hide_border": 1 + }, + { + "fieldname": "help_text", + "fieldtype": "HTML", + "label": "Help Text", + "options": "
\n

Note

\n\n

Examples

\n\n\n" + }, + { + "fieldname": "subject", + "fieldtype": "Data", + "label": "Subject" + }, + { + "fieldname": "body", + "fieldtype": "Text Editor", + "label": "Body" + } + ], + "links": [], + "modified": "2020-08-08 08:47:09.185728", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Process Statement Of Accounts", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts User", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py new file mode 100644 index 0000000000..d50e4a8af9 --- /dev/null +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py @@ -0,0 +1,271 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe.model.document import Document +from erpnext.accounts.report.general_ledger.general_ledger import execute as get_soa +from erpnext.accounts.report.accounts_receivable_summary.accounts_receivable_summary import execute as get_ageing +from frappe.core.doctype.communication.email import make + +from frappe.utils.print_format import report_to_pdf +from frappe.utils.pdf import get_pdf +from frappe.utils import today, add_days, add_months, getdate, format_date +from frappe.utils.jinja import validate_template + +import copy +from datetime import timedelta +from frappe.www.printview import get_print_style + +class ProcessStatementOfAccounts(Document): + def validate(self): + if not self.subject: + self.subject = 'Statement Of Accounts for {{ customer.name }}' + if not self.body: + self.body = 'Hello {{ customer.name }},
PFA your Statement Of Accounts from {{ doc.from_date }} to {{ doc.to_date }}.' + + validate_template(self.subject) + validate_template(self.body) + + if not self.customers: + frappe.throw(frappe._('Customers not selected.')) + + if self.enable_auto_email: + self.to_date = self.start_date + self.from_date = add_months(self.to_date, -1 * self.filter_duration) + + +def get_report_pdf(doc, consolidated=True): + statement_dict = {} + aging = '' + base_template_path = "frappe/www/printview.html" + template_path = "erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html" + + for entry in doc.customers: + if doc.include_ageing: + ageing_filters = frappe._dict({ + 'company': doc.company, + 'report_date': doc.to_date, + 'ageing_based_on': doc.ageing_based_on, + 'range1': 30, + 'range2': 60, + 'range3': 90, + 'range4': 120, + 'customer': entry.customer + }) + col1, aging = get_ageing(ageing_filters) + aging[0]['ageing_based_on'] = doc.ageing_based_on + + tax_id = frappe.get_doc('Customer', entry.customer).tax_id + + filters= frappe._dict({ + 'from_date': doc.from_date, + 'to_date': doc.to_date, + 'company': doc.company, + 'finance_book': doc.finance_book if doc.finance_book else None, + "account": doc.account if doc.account else None, + 'party_type': 'Customer', + 'party': [entry.customer], + 'group_by': doc.group_by, + 'currency': doc.currency, + 'cost_center': [cc.cost_center_name for cc in doc.cost_center], + 'project': [p.project_name for p in doc.project], + 'show_opening_entries': 0, + 'include_default_book_entries': 0, + 'show_cancelled_entries': 1, + 'tax_id': tax_id if tax_id else None + }) + col, res = get_soa(filters) + + for x in [0, -2, -1]: + res[x]['account'] = res[x]['account'].replace("'","") + + if len(res) == 3: + continue + html = frappe.render_template(template_path, \ + {"filters": filters, "data": res, "aging": aging[0] if doc.include_ageing else None}) + html = frappe.render_template(base_template_path, {"body": html, \ + "css": get_print_style(), "title": "Statement For " + entry.customer}) + statement_dict[entry.customer] = html + if not bool(statement_dict): + return False + elif consolidated: + result = ''.join(list(statement_dict.values())) + return get_pdf(result, {'orientation': doc.orientation}) + else: + for customer, statement_html in statement_dict.items(): + statement_dict[customer]=get_pdf(statement_html, {'orientation': doc.orientation}) + return statement_dict + +def get_customers_based_on_territory_or_customer_group(customer_collection, collection_name): + fields_dict = { + 'Customer Group': 'customer_group', + 'Territory': 'territory', + } + collection = frappe.get_doc(customer_collection, collection_name) + selected = [customer.name for customer in frappe.get_list(customer_collection, filters=[ + ['lft', '>=', collection.lft], + ['rgt', '<=', collection.rgt] + ], + fields=['name'], + order_by='lft asc, rgt desc' + )] + return frappe.get_list('Customer', fields=['name', 'email_id'], \ + filters=[[fields_dict[customer_collection], 'IN', selected]]) + +def get_customers_based_on_sales_person(sales_person): + lft, rgt = frappe.db.get_value("Sales Person", + sales_person, ["lft", "rgt"]) + records = frappe.db.sql(""" + select distinct parent, parenttype + from `tabSales Team` steam + where parenttype = 'Customer' + and exists(select name from `tabSales Person` where lft >= %s and rgt <= %s and name = steam.sales_person) + """, (lft, rgt), as_dict=1) + sales_person_records = frappe._dict() + for d in records: + sales_person_records.setdefault(d.parenttype, set()).add(d.parent) + customers = frappe.get_list('Customer', fields=['name', 'email_id'], \ + filters=[['name', 'in', list(sales_person_records['Customer'])]]) + return customers + +def get_recipients_and_cc(customer, doc): + recipients = [] + for clist in doc.customers: + if clist.customer == customer: + recipients.append(clist.billing_email) + if doc.primary_mandatory and clist.primary_email: + recipients.append(clist.primary_email) + cc = [] + if doc.cc_to != '': + try: + cc=[frappe.get_value('User', doc.cc_to, 'email')] + except: + pass + + return recipients, cc + +def get_context(customer, doc): + template_doc = copy.deepcopy(doc) + del template_doc.customers + template_doc.from_date = format_date(template_doc.from_date) + template_doc.to_date = format_date(template_doc.to_date) + return { + 'doc': template_doc, + 'customer': frappe.get_doc('Customer', customer), + 'frappe': frappe.utils + } + +@frappe.whitelist() +def fetch_customers(customer_collection, collection_name, primary_mandatory): + customer_list = [] + customers = [] + + if customer_collection == 'Sales Person': + customers = get_customers_based_on_sales_person(collection_name) + if not bool(customers): + frappe.throw('No Customers found with selected options.') + else: + if customer_collection == 'Sales Partner': + customers = frappe.get_list('Customer', fields=['name', 'email_id'], \ + filters=[['default_sales_partner', '=', collection_name]]) + else: + customers = get_customers_based_on_territory_or_customer_group(customer_collection, collection_name) + + for customer in customers: + primary_email = customer.get('email_id') or '' + billing_email = get_customer_emails(customer.name, 1, billing_and_primary=False) + + if billing_email == '' or (primary_email == '' and int(primary_mandatory)): + continue + + customer_list.append({ + 'name': customer.name, + 'primary_email': primary_email, + 'billing_email': billing_email + }) + return customer_list + +@frappe.whitelist() +def get_customer_emails(customer_name, primary_mandatory, billing_and_primary=True): + billing_email = frappe.db.sql(""" + SELECT c.email_id FROM `tabContact` AS c JOIN `tabDynamic Link` AS l ON c.name=l.parent \ + WHERE l.link_doctype='Customer' and l.link_name='""" + customer_name + """' and \ + c.is_billing_contact=1 \ + order by c.creation desc""") + + if len(billing_email) == 0 or (billing_email[0][0] is None): + if billing_and_primary: + frappe.throw('No billing email found for customer: '+ customer_name) + else: + return '' + + if billing_and_primary: + primary_email = frappe.get_value('Customer', customer_name, 'email_id') + if primary_email is None and int(primary_mandatory): + frappe.throw('No primary email found for customer: '+ customer_name) + return [primary_email or '', billing_email[0][0]] + else: + return billing_email[0][0] or '' + +@frappe.whitelist() +def download_statements(document_name): + doc = frappe.get_doc('Process Statement Of Accounts', document_name) + report = get_report_pdf(doc) + if report: + frappe.local.response.filename = doc.name + '.pdf' + frappe.local.response.filecontent = report + frappe.local.response.type = "download" + +@frappe.whitelist() +def send_emails(document_name, from_scheduler=False): + doc = frappe.get_doc('Process Statement Of Accounts', document_name) + report = get_report_pdf(doc, consolidated=False) + + if report: + for customer, report_pdf in report.items(): + attachments = [{ + 'fname': customer + '.pdf', + 'fcontent': report_pdf + }] + + recipients, cc = get_recipients_and_cc(customer, doc) + context = get_context(customer, doc) + subject = frappe.render_template(doc.subject, context) + message = frappe.render_template(doc.body, context) + + frappe.enqueue( + queue='short', + method=frappe.sendmail, + recipients=recipients, + sender=frappe.session.user, + cc=cc, + subject=subject, + message=message, + now=True, + reference_doctype='Process Statement Of Accounts', + reference_name=document_name, + attachments=attachments + ) + + if doc.enable_auto_email and from_scheduler: + new_to_date = getdate(today()) + if doc.frequency == 'Weekly': + new_to_date = add_days(new_to_date, 7) + else: + new_to_date = add_months(new_to_date, 1 if doc.frequency == 'Monthly' else 3) + new_from_date = add_months(new_to_date, -1 * doc.filter_duration) + doc.add_comment('Comment', 'Emails sent on: ' + frappe.utils.format_datetime(frappe.utils.now())) + doc.db_set('to_date', new_to_date, commit=True) + doc.db_set('from_date', new_from_date, commit=True) + return True + else: + return False + +@frappe.whitelist() +def send_auto_email(): + selected = frappe.get_list('Process Statement Of Accounts', filters={'to_date': format_date(today()), 'enable_auto_email': 1}) + for entry in selected: + send_emails(entry.name, from_scheduler=True) + return True \ No newline at end of file diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/test_process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/test_process_statement_of_accounts.py new file mode 100644 index 0000000000..30efbb3683 --- /dev/null +++ b/erpnext/accounts/doctype/process_statement_of_accounts/test_process_statement_of_accounts.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestProcessStatementOfAccounts(unittest.TestCase): + pass diff --git a/erpnext/accounts/doctype/process_statement_of_accounts_customer/__init__.py b/erpnext/accounts/doctype/process_statement_of_accounts_customer/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/accounts/doctype/process_statement_of_accounts_customer/process_statement_of_accounts_customer.json b/erpnext/accounts/doctype/process_statement_of_accounts_customer/process_statement_of_accounts_customer.json new file mode 100644 index 0000000000..dd04dc1b3c --- /dev/null +++ b/erpnext/accounts/doctype/process_statement_of_accounts_customer/process_statement_of_accounts_customer.json @@ -0,0 +1,47 @@ +{ + "actions": [], + "allow_workflow": 1, + "creation": "2020-08-03 16:35:21.852178", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "customer", + "billing_email", + "primary_email" + ], + "fields": [ + { + "fieldname": "customer", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Customer", + "options": "Customer", + "reqd": 1 + }, + { + "fieldname": "primary_email", + "fieldtype": "Read Only", + "in_list_view": 1, + "label": "Primary Contact Email" + }, + { + "fieldname": "billing_email", + "fieldtype": "Read Only", + "in_list_view": 1, + "label": "Billing Email" + } + ], + "istable": 1, + "links": [], + "modified": "2020-08-03 22:55:38.875601", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Process Statement Of Accounts Customer", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/process_statement_of_accounts_customer/process_statement_of_accounts_customer.py b/erpnext/accounts/doctype/process_statement_of_accounts_customer/process_statement_of_accounts_customer.py new file mode 100644 index 0000000000..1a760101db --- /dev/null +++ b/erpnext/accounts/doctype/process_statement_of_accounts_customer/process_statement_of_accounts_customer.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class ProcessStatementOfAccountsCustomer(Document): + pass diff --git a/erpnext/accounts/doctype/psoa_cost_center/__init__.py b/erpnext/accounts/doctype/psoa_cost_center/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/accounts/doctype/psoa_cost_center/psoa_cost_center.json b/erpnext/accounts/doctype/psoa_cost_center/psoa_cost_center.json new file mode 100644 index 0000000000..e292b60d68 --- /dev/null +++ b/erpnext/accounts/doctype/psoa_cost_center/psoa_cost_center.json @@ -0,0 +1,30 @@ +{ + "actions": [], + "creation": "2020-08-03 16:56:45.744905", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "cost_center_name" + ], + "fields": [ + { + "fieldname": "cost_center_name", + "fieldtype": "Link", + "label": "Cost Center", + "options": "Cost Center" + } + ], + "istable": 1, + "links": [], + "modified": "2020-08-03 16:56:45.744905", + "modified_by": "Administrator", + "module": "Accounts", + "name": "PSOA Cost Center", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/psoa_cost_center/psoa_cost_center.py b/erpnext/accounts/doctype/psoa_cost_center/psoa_cost_center.py new file mode 100644 index 0000000000..0aeef3ed3a --- /dev/null +++ b/erpnext/accounts/doctype/psoa_cost_center/psoa_cost_center.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class PSOACostCenter(Document): + pass diff --git a/erpnext/accounts/doctype/psoa_project/__init__.py b/erpnext/accounts/doctype/psoa_project/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/accounts/doctype/psoa_project/psoa_project.json b/erpnext/accounts/doctype/psoa_project/psoa_project.json new file mode 100644 index 0000000000..20a03eed96 --- /dev/null +++ b/erpnext/accounts/doctype/psoa_project/psoa_project.json @@ -0,0 +1,30 @@ +{ + "actions": [], + "creation": "2020-08-03 16:52:14.731978", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "project_name" + ], + "fields": [ + { + "fieldname": "project_name", + "fieldtype": "Link", + "label": "Project", + "options": "Project" + } + ], + "istable": 1, + "links": [], + "modified": "2020-08-03 16:53:39.219736", + "modified_by": "Administrator", + "module": "Accounts", + "name": "PSOA Project", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/psoa_project/psoa_project.py b/erpnext/accounts/doctype/psoa_project/psoa_project.py new file mode 100644 index 0000000000..f4a5dee975 --- /dev/null +++ b/erpnext/accounts/doctype/psoa_project/psoa_project.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class PSOAProject(Document): + pass diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index df77dc8417..d62e73b6ac 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -180,7 +180,7 @@ "no_copy": 1, "oldfieldname": "naming_series", "oldfieldtype": "Select", - "options": "ACC-PINV-.YYYY.-", + "options": "ACC-PINV-.YYYY.-\nACC-PINV-RET-.YYYY.-", "print_hide": 1, "reqd": 1, "set_only_once": 1 @@ -969,8 +969,10 @@ { "fieldname": "clearance_date", "fieldtype": "Date", - "hidden": 1, - "label": "Clearance Date" + "label": "Clearance Date", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 }, { "fieldname": "col_br_payments", @@ -1332,7 +1334,7 @@ "idx": 204, "is_submittable": 1, "links": [], - "modified": "2020-07-24 09:46:40.405463", + "modified": "2020-08-03 23:20:04.466153", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice", @@ -1394,4 +1396,4 @@ "timeline_field": "supplier", "title_field": "title", "track_changes": 1 -} \ No newline at end of file +} 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 4dc81e9087..2397b7d0cb 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -1,7 +1,6 @@ { "actions": [], "allow_import": 1, - "allow_workflow": 1, "autoname": "naming_series:", "creation": "2013-05-24 19:29:05", "doctype": "DocType", @@ -217,7 +216,7 @@ "no_copy": 1, "oldfieldname": "naming_series", "oldfieldtype": "Select", - "options": "ACC-SINV-.YYYY.-", + "options": "ACC-SINV-.YYYY.-\nACC-SINV-RET-.YYYY.-", "print_hide": 1, "reqd": 1, "set_only_once": 1 @@ -448,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", @@ -1947,7 +1946,7 @@ "idx": 181, "is_submittable": 1, "links": [], - "modified": "2020-07-18 05:07:16.725974", + "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.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 3dab054014..71f2e120cc 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -1619,22 +1619,23 @@ def update_multi_mode_option(doc, pos_profile): for pos_payment_method in pos_profile.get('payments'): pos_payment_method = pos_payment_method.as_dict() - + payment_mode = get_mode_of_payment_info(pos_payment_method.mode_of_payment, doc.company) - payment_mode[0].default = pos_payment_method.default - append_payment(payment_mode[0]) + if payment_mode: + payment_mode[0].default = pos_payment_method.default + append_payment(payment_mode[0]) def get_all_mode_of_payments(doc): return frappe.db.sql(""" - select mpa.default_account, mpa.parent, mp.type as type - from `tabMode of Payment Account` mpa,`tabMode of Payment` mp + select mpa.default_account, mpa.parent, mp.type as type + from `tabMode of Payment Account` mpa,`tabMode of Payment` mp where mpa.parent = mp.name and mpa.company = %(company)s and mp.enabled = 1""", {'company': doc.company}, as_dict=1) def get_mode_of_payment_info(mode_of_payment, company): return frappe.db.sql(""" - select mpa.default_account, mpa.parent, mp.type as type - from `tabMode of Payment Account` mpa,`tabMode of Payment` mp + select mpa.default_account, mpa.parent, mp.type as type + from `tabMode of Payment Account` mpa,`tabMode of Payment` mp where mpa.parent = mp.name and mpa.company = %s and mp.enabled = 1 and mp.name = %s""", (company, mode_of_payment), as_dict=1) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py index 4a8fcc03fd..2980213f3b 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py @@ -13,12 +13,13 @@ 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': [ { 'label': _('Payment'), - 'items': ['Payment Entry', 'Payment Request', 'Journal Entry', 'Invoice Discounting'] + 'items': ['Payment Entry', 'Payment Request', 'Journal Entry', 'Invoice Discounting', 'Dunning'] }, { 'label': _('Reference'), diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 964566a17e..9660c9570e 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -206,10 +206,19 @@ class TestSalesInvoice(unittest.TestCase): "rate": 14, 'included_in_print_rate': 1 }) + si.append("taxes", { + "charge_type": "On Item Quantity", + "account_head": "_Test Account Education Cess - _TC", + "cost_center": "_Test Cost Center - _TC", + "description": "CESS", + "rate": 5, + 'included_in_print_rate': 1 + }) si.insert() # with inclusive tax - self.assertEqual(si.net_total, 4385.96) + self.assertEqual(si.items[0].net_amount, 3947.368421052631) + self.assertEqual(si.net_total, 3947.37) self.assertEqual(si.grand_total, 5000) si.reload() @@ -222,8 +231,8 @@ class TestSalesInvoice(unittest.TestCase): si.save() # with inclusive tax and additional discount - self.assertEqual(si.net_total, 4285.96) - self.assertEqual(si.grand_total, 4885.99) + self.assertEqual(si.net_total, 3847.37) + self.assertEqual(si.grand_total, 4886) si.reload() @@ -235,7 +244,7 @@ class TestSalesInvoice(unittest.TestCase): si.save() # with inclusive tax and additional discount - self.assertEqual(si.net_total, 4298.25) + self.assertEqual(si.net_total, 3859.65) self.assertEqual(si.grand_total, 4900.00) def test_sales_invoice_discount_amount(self): 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/sales_invoice_payment/sales_invoice_payment.json b/erpnext/accounts/doctype/sales_invoice_payment/sales_invoice_payment.json index 2f9d381c92..5ab46b7fd5 100644 --- a/erpnext/accounts/doctype/sales_invoice_payment/sales_invoice_payment.json +++ b/erpnext/accounts/doctype/sales_invoice_payment/sales_invoice_payment.json @@ -64,6 +64,7 @@ "fieldname": "clearance_date", "fieldtype": "Date", "label": "Clearance Date", + "no_copy": 1, "print_hide": 1, "read_only": 1 }, @@ -78,7 +79,7 @@ ], "istable": 1, "links": [], - "modified": "2020-05-05 16:51:20.091441", + "modified": "2020-08-03 12:45:39.986598", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice Payment", diff --git a/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template_dashboard.py b/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template_dashboard.py index 0e9c808608..d825c6fd32 100644 --- a/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template_dashboard.py +++ b/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template_dashboard.py @@ -8,7 +8,7 @@ def get_data(): 'fieldname': 'taxes_and_charges', 'non_standard_fieldnames': { 'Tax Rule': 'sales_tax_template', - 'Subscription': 'tax_template', + 'Subscription': 'sales_tax_template', 'Restaurant': 'default_tax_template' }, 'transactions': [ 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/subscription/test_subscription.py b/erpnext/accounts/doctype/subscription/test_subscription.py index f41f08a6c4..811fc356cf 100644 --- a/erpnext/accounts/doctype/subscription/test_subscription.py +++ b/erpnext/accounts/doctype/subscription/test_subscription.py @@ -7,8 +7,8 @@ import unittest import frappe from erpnext.accounts.doctype.subscription.subscription import get_prorata_factor -from frappe.utils.data import nowdate, add_days, add_to_date, add_months, date_diff, flt, get_date_str - +from frappe.utils.data import (nowdate, add_days, add_to_date, add_months, date_diff, flt, get_date_str, + get_first_day, get_last_day) def create_plan(): if not frappe.db.exists('Subscription Plan', '_Test Plan Name'): @@ -68,14 +68,14 @@ class TestSubscription(unittest.TestCase): subscription.party_type = 'Customer' subscription.party = '_Test Customer' subscription.trial_period_start = nowdate() - subscription.trial_period_end = add_days(nowdate(), 30) + subscription.trial_period_end = add_months(nowdate(), 1) subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1}) subscription.save() self.assertEqual(subscription.trial_period_start, nowdate()) - self.assertEqual(subscription.trial_period_end, add_days(nowdate(), 30)) + self.assertEqual(subscription.trial_period_end, add_months(nowdate(), 1)) self.assertEqual(add_days(subscription.trial_period_end, 1), get_date_str(subscription.current_invoice_start)) - self.assertEqual(add_days(subscription.current_invoice_start, 30), get_date_str(subscription.current_invoice_end)) + self.assertEqual(add_to_date(subscription.current_invoice_start, months=1, days=-1), get_date_str(subscription.current_invoice_end)) self.assertEqual(subscription.invoices, []) self.assertEqual(subscription.status, 'Trialling') 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/page/bank_reconciliation/bank_reconciliation.py b/erpnext/accounts/page/bank_reconciliation/bank_reconciliation.py index 7df090bf62..ce6baa6846 100644 --- a/erpnext/accounts/page/bank_reconciliation/bank_reconciliation.py +++ b/erpnext/accounts/page/bank_reconciliation/bank_reconciliation.py @@ -290,6 +290,7 @@ def get_matching_transactions_payments(description_matching): return [] @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def payment_entry_query(doctype, txt, searchfield, start, page_len, filters): account = frappe.db.get_value("Bank Account", filters.get("bank_account"), "account") if not account: @@ -319,6 +320,7 @@ def payment_entry_query(doctype, txt, searchfield, start, page_len, filters): ) @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def journal_entry_query(doctype, txt, searchfield, start, page_len, filters): account = frappe.db.get_value("Bank Account", filters.get("bank_account"), "account") @@ -355,6 +357,7 @@ def journal_entry_query(doctype, txt, searchfield, start, page_len, filters): ) @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def sales_invoices_query(doctype, txt, searchfield, start, page_len, filters): return frappe.db.sql(""" SELECT diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index 28a6519650..2f800bb2ab 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -611,7 +611,7 @@ def get_partywise_advanced_payment_amount(party_type, posting_date = None, futur cond = "posting_date <= '{0}'".format(posting_date) if company: - cond += "and company = '{0}'".format(company) + cond += "and company = {0}".format(frappe.db.escape(company)) data = frappe.db.sql(""" SELECT party, sum({0}) as amount FROM `tabGL Entry` diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 66aa18058b..59117c8174 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -643,8 +643,10 @@ class ReceivablePayableReport(object): account_type = "Receivable" if self.party_type == "Customer" else "Payable" accounts = [d.name for d in frappe.get_all("Account", filters={"account_type": account_type, "company": self.filters.company})] - conditions.append("account in (%s)" % ','.join(['%s'] *len(accounts))) - values += accounts + + if accounts: + conditions.append("account in (%s)" % ','.join(['%s'] *len(accounts))) + values += accounts def add_customer_filters(self, conditions, values): if self.filters.get("customer_group"): 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 3785ebf215..1b65a318b6 100644 --- a/erpnext/accounts/report/financial_statements.py +++ b/erpnext/accounts/report/financial_statements.py @@ -14,7 +14,7 @@ import frappe, erpnext from erpnext.accounts.report.utils import get_currency, convert_to_presentation_currency from erpnext.accounts.utils import get_fiscal_year from frappe import _ -from frappe.utils import (flt, getdate, get_first_day, add_months, add_days, formatdate, cstr) +from frappe.utils import (flt, getdate, get_first_day, add_months, add_days, formatdate, cstr, cint) from six import itervalues from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions, get_dimension_with_children @@ -46,7 +46,7 @@ def get_period_list(from_fiscal_year, to_fiscal_year, period_start_date, period_ start_date = year_start_date months = get_months(year_start_date, year_end_date) - for i in range(math.ceil(months / months_to_add)): + for i in range(cint(math.ceil(months / months_to_add))): period = frappe._dict({ "from_date": start_date }) @@ -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_maintenance/asset_maintenance.py b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py index 1869a29c8d..60c528bcc4 100644 --- a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py +++ b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py @@ -106,6 +106,7 @@ def update_maintenance_log(asset_maintenance, item_code, item_name, task): maintenance_log.save() @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def get_team_members(doctype, txt, searchfield, start, page_len, filters): return frappe.db.get_values('Maintenance Team Member', { 'parent': filters.get("maintenance_team") }) diff --git a/erpnext/assets/doctype/asset_maintenance_log/asset_maintenance_log.py b/erpnext/assets/doctype/asset_maintenance_log/asset_maintenance_log.py index f169f01616..34facd8d05 100644 --- a/erpnext/assets/doctype/asset_maintenance_log/asset_maintenance_log.py +++ b/erpnext/assets/doctype/asset_maintenance_log/asset_maintenance_log.py @@ -11,7 +11,7 @@ from erpnext.assets.doctype.asset_maintenance.asset_maintenance import calculate class AssetMaintenanceLog(Document): def validate(self): - if getdate(self.due_date) < getdate(nowdate()): + if getdate(self.due_date) < getdate(nowdate()) and self.maintenance_status not in ["Completed", "Cancelled"]: self.maintenance_status = "Overdue" if self.maintenance_status == "Completed" and not self.completion_date: @@ -41,6 +41,7 @@ class AssetMaintenanceLog(Document): asset_maintenance_doc.save() @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def get_maintenance_tasks(doctype, txt, searchfield, start, page_len, filters): asset_maintenance_tasks = frappe.db.get_values('Asset Maintenance Task', {'parent':filters.get("asset_maintenance")}, 'maintenance_task') return asset_maintenance_tasks diff --git a/erpnext/assets/doctype/asset_maintenance_log/asset_maintenance_log_list.js b/erpnext/assets/doctype/asset_maintenance_log/asset_maintenance_log_list.js index b854413310..23000e60ef 100644 --- a/erpnext/assets/doctype/asset_maintenance_log/asset_maintenance_log_list.js +++ b/erpnext/assets/doctype/asset_maintenance_log/asset_maintenance_log_list.js @@ -1,14 +1,15 @@ frappe.listview_settings['Asset Maintenance Log'] = { add_fields: ["maintenance_status"], + has_indicator_for_draft: 1, get_indicator: function(doc) { - if(doc.maintenance_status=="Pending") { - return [__("Pending"), "orange"]; - } else if(doc.maintenance_status=="Completed") { - return [__("Completed"), "green"]; - } else if(doc.maintenance_status=="Cancelled") { - return [__("Cancelled"), "red"]; - } else if(doc.maintenance_status=="Overdue") { - return [__("Overdue"), "red"]; + if (doc.maintenance_status=="Planned") { + return [__(doc.maintenance_status), "orange", "status,=," + doc.maintenance_status]; + } else if (doc.maintenance_status=="Completed") { + return [__(doc.maintenance_status), "green", "status,=," + doc.maintenance_status]; + } else if (doc.maintenance_status=="Cancelled") { + return [__(doc.maintenance_status), "red", "status,=," + doc.maintenance_status]; + } else if (doc.maintenance_status=="Overdue") { + return [__(doc.maintenance_status), "red", "status,=," + doc.maintenance_status]; } } }; diff --git a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py index 155597e856..fd702c74c7 100644 --- a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py +++ b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py @@ -8,6 +8,7 @@ from frappe import _ from frappe.utils import flt, getdate, cint, date_diff, formatdate from erpnext.assets.doctype.asset.depreciation import get_depreciation_accounts from frappe.model.document import Document +from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_checks_for_pl_and_bs_accounts class AssetValueAdjustment(Document): def validate(self): @@ -53,17 +54,33 @@ class AssetValueAdjustment(Document): je.company = self.company je.remark = "Depreciation Entry against {0} worth {1}".format(self.asset, self.difference_amount) - je.append("accounts", { + credit_entry = { "account": accumulated_depreciation_account, "credit_in_account_currency": self.difference_amount, "cost_center": depreciation_cost_center or self.cost_center - }) + } - je.append("accounts", { + debit_entry = { "account": depreciation_expense_account, "debit_in_account_currency": self.difference_amount, "cost_center": depreciation_cost_center or self.cost_center - }) + } + + accounting_dimensions = get_checks_for_pl_and_bs_accounts() + + for dimension in accounting_dimensions: + if dimension.get('mandatory_for_bs'): + credit_entry.update({ + dimension['fieldname']: self.get(dimension['fieldname']) or dimension.get('default_dimension') + }) + + if dimension.get('mandatory_for_pl'): + debit_entry.update({ + dimension['fieldname']: self.get(dimension['fieldname']) or dimension.get('default_dimension') + }) + + je.append("accounts", credit_entry) + je.append("accounts", debit_entry) je.flags.ignore_permissions = True je.submit() diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index 25065ab155..9f2b9714f7 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -94,7 +94,7 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend( if(this.frm.doc.status !== 'Closed' && flt(this.frm.doc.per_received) < 100 && flt(this.frm.doc.per_billed) < 100) { this.frm.add_custom_button(__('Update Items'), () => { erpnext.utils.update_child_items({ - frm: frm, + frm: this.frm, child_docname: "items", child_doctype: "Purchase Order Detail", cannot_add_row: false, diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py index 4b852300e5..b54a585b97 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py @@ -207,6 +207,7 @@ def get_list_context(context=None): return list_context @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def get_supplier_contacts(doctype, txt, searchfield, start, page_len, filters): return frappe.db.sql("""select `tabContact`.name from `tabContact`, `tabDynamic Link` where `tabDynamic Link`.link_doctype = 'Supplier' and (`tabDynamic Link`.link_name=%(name)s 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 89c38c710b..d61e44b53d 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -325,7 +325,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): @@ -479,7 +479,11 @@ class AccountsController(TransactionBase): if d.against_order: allocated_amount = flt(d.amount) else: - amount = self.rounded_total or self.grand_total + if self.get('party_account_currency') == self.company_currency: + amount = self.get('base_rounded_total') or self.base_grand_total + else: + amount = self.get('rounded_total') or self.grand_total + allocated_amount = min(amount - advance_allocated, d.amount) advance_allocated += flt(allocated_amount) @@ -802,10 +806,22 @@ class AccountsController(TransactionBase): self.payment_terms_template = '' return + party_account_currency = self.get('party_account_currency') + if not party_account_currency: + party_type, party = self.get_party() + + if party_type and party: + party_account_currency = get_party_account_currency(party_type, party, self.company) + posting_date = self.get("bill_date") or self.get("posting_date") or self.get("transaction_date") date = self.get("due_date") due_date = date or posting_date - grand_total = self.get("rounded_total") or self.grand_total + + if party_account_currency == self.company_currency: + grand_total = self.get("base_rounded_total") or self.base_grand_total + else: + grand_total = self.get("rounded_total") or self.grand_total + if self.doctype in ("Sales Invoice", "Purchase Invoice"): grand_total = grand_total - flt(self.write_off_amount) @@ -850,13 +866,25 @@ class AccountsController(TransactionBase): def validate_payment_schedule_amount(self): if self.doctype == 'Sales Invoice' and self.is_pos: return + party_account_currency = self.get('party_account_currency') + if not party_account_currency: + party_type, party = self.get_party() + + if party_type and party: + party_account_currency = get_party_account_currency(party_type, party, self.company) + if self.get("payment_schedule"): total = 0 for d in self.get("payment_schedule"): total += flt(d.payment_amount) - total = flt(total, self.precision("grand_total")) - grand_total = flt(self.get("rounded_total") or self.grand_total, self.precision('grand_total')) + if party_account_currency == self.company_currency: + total = flt(total, self.precision("base_grand_total")) + grand_total = flt(self.get("base_rounded_total") or self.base_grand_total, self.precision('base_grand_total')) + else: + total = flt(total, self.precision("grand_total")) + grand_total = flt(self.get("rounded_total") or self.grand_total, self.precision('grand_total')) + if self.get("total_advance"): grand_total -= self.get("total_advance") @@ -957,7 +985,7 @@ def validate_inclusive_tax(tax, doc): # all rows about the reffered tax should be inclusive _on_previous_row_error("1 - %d" % (tax.row_id,)) elif tax.get("category") == "Valuation": - frappe.throw(_("Valuation type charges can not marked as Inclusive")) + frappe.throw(_("Valuation type charges can not be marked as Inclusive")) def set_balance_in_account_currency(gl_dict, account_currency=None, conversion_rate=None, company_currency=None): 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 31e34987be..c88bf66411 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -12,6 +12,7 @@ from frappe.utils import unique # searches for active employees @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def employee_query(doctype, txt, searchfield, start, page_len, filters): conditions = [] fields = get_fields("Employee", ["name", "employee_name"]) @@ -42,6 +43,7 @@ def employee_query(doctype, txt, searchfield, start, page_len, filters): # searches for leads which are not converted @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def lead_query(doctype, txt, searchfield, start, page_len, filters): fields = get_fields("Lead", ["name", "lead_name", "company_name"]) @@ -72,6 +74,7 @@ def lead_query(doctype, txt, searchfield, start, page_len, filters): # searches for customer @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def customer_query(doctype, txt, searchfield, start, page_len, filters): conditions = [] cust_master_name = frappe.defaults.get_user_default("cust_master_name") @@ -110,8 +113,10 @@ def customer_query(doctype, txt, searchfield, start, page_len, filters): # searches for supplier @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def supplier_query(doctype, txt, searchfield, start, page_len, filters): supp_master_name = frappe.defaults.get_user_default("supp_master_name") + if supp_master_name == "Supplier Name": fields = ["name", "supplier_group"] else: @@ -142,32 +147,49 @@ def supplier_query(doctype, txt, searchfield, start, page_len, filters): @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def tax_account_query(doctype, txt, searchfield, start, page_len, filters): company_currency = erpnext.get_company_currency(filters.get('company')) - tax_accounts = frappe.db.sql("""select name, parent_account from tabAccount - where tabAccount.docstatus!=2 - and account_type in (%s) - and is_group = 0 - and company = %s - and account_currency = %s - and `%s` LIKE %s - order by idx desc, name - limit %s, %s""" % - (", ".join(['%s']*len(filters.get("account_type"))), "%s", "%s", searchfield, "%s", "%s", "%s"), - tuple(filters.get("account_type") + [filters.get("company"), company_currency, "%%%s%%" % txt, - start, page_len])) + def get_accounts(with_account_type_filter): + account_type_condition = '' + if with_account_type_filter: + account_type_condition = "AND account_type in %(account_types)s" + + accounts = frappe.db.sql(""" + SELECT name, parent_account + FROM `tabAccount` + WHERE `tabAccount`.docstatus!=2 + {account_type_condition} + AND is_group = 0 + AND company = %(company)s + AND account_currency = %(currency)s + AND `{searchfield}` LIKE %(txt)s + ORDER BY idx DESC, name + LIMIT %(offset)s, %(limit)s + """.format(account_type_condition=account_type_condition, searchfield=searchfield), + dict( + account_types=filters.get("account_type"), + company=filters.get("company"), + currency=company_currency, + txt="%{}%".format(txt), + offset=start, + limit=page_len + ) + ) + + return accounts + + tax_accounts = get_accounts(True) + if not tax_accounts: - tax_accounts = frappe.db.sql("""select name, parent_account from tabAccount - where tabAccount.docstatus!=2 and is_group = 0 - and company = %s and account_currency = %s and `%s` LIKE %s limit %s, %s""" #nosec - % ("%s", "%s", searchfield, "%s", "%s", "%s"), - (filters.get("company"), company_currency, "%%%s%%" % txt, start, page_len)) + tax_accounts = get_accounts(False) return tax_accounts @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=False): conditions = [] @@ -215,7 +237,6 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals idx desc, name, item_name limit %(start)s, %(page_len)s """.format( - key=searchfield, columns=columns, scond=searchfields, fcond=get_filters_cond(doctype, filters, conditions).replace('%', '%%'), @@ -231,6 +252,7 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def bom(doctype, txt, searchfield, start, page_len, filters): conditions = [] fields = get_fields("BOM", ["name", "item"]) @@ -258,6 +280,7 @@ def bom(doctype, txt, searchfield, start, page_len, filters): @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def get_project_name(doctype, txt, searchfield, start, page_len, filters): cond = '' if filters.get('customer'): @@ -285,6 +308,7 @@ def get_project_name(doctype, txt, searchfield, start, page_len, filters): @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def get_delivery_notes_to_be_billed(doctype, txt, searchfield, start, page_len, filters, as_dict): fields = get_fields("Delivery Note", ["name", "customer", "posting_date"]) @@ -315,6 +339,7 @@ def get_delivery_notes_to_be_billed(doctype, txt, searchfield, start, page_len, @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def get_batch_no(doctype, txt, searchfield, start, page_len, filters): cond = "" if filters.get("posting_date"): @@ -373,6 +398,7 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters): @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def get_account_list(doctype, txt, searchfield, start, page_len, filters): filter_list = [] @@ -395,8 +421,8 @@ def get_account_list(doctype, txt, searchfield, start, page_len, filters): fields = ["name", "parent_account"], limit_start=start, limit_page_length=page_len, as_list=True) - @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def get_blanket_orders(doctype, txt, searchfield, start, page_len, filters): return frappe.db.sql("""select distinct bo.name, bo.blanket_order_type, bo.to_date from `tabBlanket Order` bo, `tabBlanket Order Item` boi @@ -413,6 +439,7 @@ def get_blanket_orders(doctype, txt, searchfield, start, page_len, filters): @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def get_income_account(doctype, txt, searchfield, start, page_len, filters): from erpnext.controllers.queries import get_match_cond @@ -439,6 +466,7 @@ def get_income_account(doctype, txt, searchfield, start, page_len, filters): @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def get_expense_account(doctype, txt, searchfield, start, page_len, filters): from erpnext.controllers.queries import get_match_cond @@ -463,29 +491,24 @@ def get_expense_account(doctype, txt, searchfield, start, page_len, filters): @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def warehouse_query(doctype, txt, searchfield, start, page_len, filters): # Should be used when item code is passed in 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), @@ -506,6 +529,7 @@ def get_doctype_wise_filters(filters): @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def get_batch_numbers(doctype, txt, searchfield, start, page_len, filters): query = """select batch_id from `tabBatch` where disabled = 0 @@ -519,6 +543,7 @@ def get_batch_numbers(doctype, txt, searchfield, start, page_len, filters): @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def item_manufacturer_query(doctype, txt, searchfield, start, page_len, filters): item_filters = [ ['manufacturer', 'like', '%' + txt + '%'], @@ -537,6 +562,7 @@ def item_manufacturer_query(doctype, txt, searchfield, start, page_len, filters) @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def get_purchase_receipts(doctype, txt, searchfield, start, page_len, filters): query = """ select pr.name @@ -551,6 +577,7 @@ def get_purchase_receipts(doctype, txt, searchfield, start, page_len, filters): @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def get_purchase_invoices(doctype, txt, searchfield, start, page_len, filters): query = """ select pi.name @@ -565,6 +592,7 @@ def get_purchase_invoices(doctype, txt, searchfield, start, page_len, filters): @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def get_tax_template(doctype, txt, searchfield, start, page_len, filters): item_doc = frappe.get_cached_doc('Item', filters.get('item_code')) @@ -579,9 +607,12 @@ def get_tax_template(doctype, txt, searchfield, start, page_len, filters): if not taxes: return frappe.db.sql(""" SELECT name FROM `tabItem Tax Template` """) else: + valid_from = filters.get('valid_from') + valid_from = valid_from[1] if isinstance(valid_from, list) else valid_from + args = { 'item_code': filters.get('item_code'), - 'posting_date': filters.get('valid_from'), + 'posting_date': valid_from, 'tax_category': filters.get('tax_category'), 'company': filters.get('company') } diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index 1085486f94..fa2b14f3bf 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -282,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 @@ -297,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/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 572e1ca239..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): @@ -161,8 +162,9 @@ class calculate_taxes_and_totals(object): for item in self.doc.get("items"): item_tax_map = self._load_item_tax_rate(item.item_tax_rate) cumulated_tax_fraction = 0 + total_inclusive_tax_amount_per_qty = 0 for i, tax in enumerate(self.doc.get("taxes")): - tax.tax_fraction_for_current_item = self.get_current_tax_fraction(tax, item_tax_map) + tax.tax_fraction_for_current_item, inclusive_tax_amount_per_qty = self.get_current_tax_fraction(tax, item_tax_map) if i==0: tax.grand_total_fraction_for_current_item = 1 + tax.tax_fraction_for_current_item @@ -172,9 +174,12 @@ class calculate_taxes_and_totals(object): + tax.tax_fraction_for_current_item cumulated_tax_fraction += tax.tax_fraction_for_current_item + total_inclusive_tax_amount_per_qty += inclusive_tax_amount_per_qty * flt(item.qty) - if cumulated_tax_fraction and not self.discount_amount_applied and item.qty: - item.net_amount = flt(item.amount / (1 + cumulated_tax_fraction)) + if not self.discount_amount_applied and item.qty and (cumulated_tax_fraction or total_inclusive_tax_amount_per_qty): + amount = flt(item.amount) - total_inclusive_tax_amount_per_qty + + item.net_amount = flt(amount / (1 + cumulated_tax_fraction)) item.net_rate = flt(item.net_amount / item.qty, item.precision("net_rate")) item.discount_percentage = flt(item.discount_percentage, item.precision("discount_percentage")) @@ -190,6 +195,7 @@ class calculate_taxes_and_totals(object): from tax inclusive amount """ current_tax_fraction = 0 + inclusive_tax_amount_per_qty = 0 if cint(tax.included_in_print_rate): tax_rate = self._get_tax_rate(tax, item_tax_map) @@ -205,9 +211,14 @@ class calculate_taxes_and_totals(object): current_tax_fraction = (tax_rate / 100.0) * \ self.doc.get("taxes")[cint(tax.row_id) - 1].grand_total_fraction_for_current_item - if getattr(tax, "add_deduct_tax", None): - current_tax_fraction *= -1.0 if (tax.add_deduct_tax == "Deduct") else 1.0 - return current_tax_fraction + elif tax.charge_type == "On Item Quantity": + inclusive_tax_amount_per_qty = flt(tax_rate) + + if getattr(tax, "add_deduct_tax", None) and tax.add_deduct_tax == "Deduct": + current_tax_fraction *= -1.0 + inclusive_tax_amount_per_qty *= -1.0 + + return current_tax_fraction, inclusive_tax_amount_per_qty def _get_tax_rate(self, tax, item_tax_map): if tax.account_head in item_tax_map: @@ -321,7 +332,7 @@ class calculate_taxes_and_totals(object): current_tax_amount = (tax_rate / 100.0) * \ self.doc.get("taxes")[cint(tax.row_id) - 1].grand_total_for_current_item elif tax.charge_type == "On Item Quantity": - current_tax_amount = tax_rate * item.stock_qty + current_tax_amount = tax_rate * item.qty self.set_item_wise_tax(item, tax, tax_rate, current_tax_amount) @@ -472,7 +483,7 @@ class calculate_taxes_and_totals(object): actual_taxes_dict = {} for tax in self.doc.get("taxes"): - if tax.charge_type == "Actual": + if tax.charge_type in ["Actual", "On Item Quantity"]: tax_amount = self.get_tax_amount_if_for_valuation_or_deduction(tax.tax_amount, tax) actual_taxes_dict.setdefault(tax.idx, tax_amount) elif tax.row_id in actual_taxes_dict: @@ -597,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.json b/erpnext/crm/doctype/opportunity/opportunity.json index 545e2324ac..b61cad3620 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.json +++ b/erpnext/crm/doctype/opportunity/opportunity.json @@ -16,6 +16,7 @@ "opportunity_from", "party_name", "customer_name", + "source", "column_break0", "title", "opportunity_type", @@ -49,10 +50,9 @@ "contact_email", "contact_mobile", "more_info", - "source", + "company", "campaign", "column_break1", - "company", "transaction_date", "amended_from", "lost_reasons" @@ -344,7 +344,7 @@ "collapsible": 1, "fieldname": "more_info", "fieldtype": "Section Break", - "label": "Source", + "label": "More Information", "oldfieldtype": "Section Break", "options": "fa fa-file-text" }, @@ -411,7 +411,7 @@ "fieldname": "lost_reasons", "fieldtype": "Table MultiSelect", "label": "Lost Reasons", - "options": "Lost Reason Detail", + "options": "Opportunity Lost Reason Detail", "read_only": 1 }, { @@ -424,7 +424,7 @@ "icon": "fa fa-info-sign", "idx": 195, "links": [], - "modified": "2020-07-14 16:49:15.888503", + "modified": "2020-08-11 17:34:35.066961", "modified_by": "Administrator", "module": "CRM", "name": "Opportunity", diff --git a/erpnext/crm/doctype/opportunity/opportunity.py b/erpnext/crm/doctype/opportunity/opportunity.py index 1b071ea1b7..47b05f306b 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.py +++ b/erpnext/crm/doctype/opportunity/opportunity.py @@ -119,11 +119,19 @@ class Opportunity(TransactionBase): and q.status not in ('Lost', 'Closed')""", self.name) def has_ordered_quotation(self): - return frappe.db.sql(""" - select q.name - from `tabQuotation` q, `tabQuotation Item` qi - where q.name = qi.parent and q.docstatus=1 and qi.prevdoc_docname =%s - and q.status = 'Ordered'""", self.name) + if not self.with_items: + return frappe.get_all('Quotation', + { + 'opportunity': self.name, + 'status': 'Ordered', + 'docstatus': 1 + }, 'name') + else: + return frappe.db.sql(""" + select q.name + from `tabQuotation` q, `tabQuotation Item` qi + where q.name = qi.parent and q.docstatus=1 and qi.prevdoc_docname =%s + and q.status = 'Ordered'""", self.name) def has_lost_quotation(self): lost_quotation = frappe.db.sql(""" @@ -259,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" @@ -269,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) @@ -317,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) @@ -329,8 +341,9 @@ def make_opportunity_from_communication(communication, ignore_communication_link opportunity = frappe.get_doc({ "doctype": "Opportunity", + "company": company, "opportunity_from": opportunity_from, - "lead": lead + "party_name": lead }).insert(ignore_permissions=True) link_communication_to_document(doc, "Opportunity", opportunity.name, ignore_communication_links) 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/opportunity_lost_reason_detail/__init__.py b/erpnext/crm/doctype/opportunity_lost_reason_detail/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/crm/doctype/opportunity_lost_reason_detail/opportunity_lost_reason_detail.json b/erpnext/crm/doctype/opportunity_lost_reason_detail/opportunity_lost_reason_detail.json new file mode 100644 index 0000000000..50620e2c34 --- /dev/null +++ b/erpnext/crm/doctype/opportunity_lost_reason_detail/opportunity_lost_reason_detail.json @@ -0,0 +1,31 @@ +{ + "actions": [], + "creation": "2020-07-16 16:11:39.830389", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "lost_reason" + ], + "fields": [ + { + "fieldname": "lost_reason", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Opportunity Lost Reason", + "options": "Opportunity Lost Reason" + } + ], + "istable": 1, + "links": [], + "modified": "2020-07-26 17:58:26.313242", + "modified_by": "Administrator", + "module": "CRM", + "name": "Opportunity Lost Reason Detail", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/crm/doctype/opportunity_lost_reason_detail/opportunity_lost_reason_detail.py b/erpnext/crm/doctype/opportunity_lost_reason_detail/opportunity_lost_reason_detail.py new file mode 100644 index 0000000000..8723f1d045 --- /dev/null +++ b/erpnext/crm/doctype/opportunity_lost_reason_detail/opportunity_lost_reason_detail.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class OpportunityLostReasonDetail(Document): + pass 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/dashboard_chart/course_wise_enrollment/course_wise_enrollment.json b/erpnext/education/dashboard_chart/course_wise_enrollment/course_wise_enrollment.json new file mode 100644 index 0000000000..9c5f784648 --- /dev/null +++ b/erpnext/education/dashboard_chart/course_wise_enrollment/course_wise_enrollment.json @@ -0,0 +1,31 @@ +{ + "based_on": "", + "chart_name": "Course wise Enrollment", + "chart_type": "Group By", + "creation": "2020-07-23 18:24:38.214220", + "docstatus": 0, + "doctype": "Dashboard Chart", + "document_type": "Course Enrollment", + "dynamic_filters_json": "[]", + "filters_json": "[[\"Course Enrollment\",\"enrollment_date\",\"Timespan\",\"this year\",false]]", + "group_by_based_on": "course", + "group_by_type": "Count", + "idx": 0, + "is_public": 1, + "is_standard": 1, + "last_synced_on": "2020-07-27 17:50:32.490587", + "modified": "2020-07-27 17:54:09.829206", + "modified_by": "Administrator", + "module": "Education", + "name": "Course wise Enrollment", + "number_of_groups": 0, + "owner": "Administrator", + "source": "", + "time_interval": "Yearly", + "timeseries": 0, + "timespan": "Last Year", + "type": "Percentage", + "use_report_chart": 0, + "value_based_on": "", + "y_axis": [] +} \ No newline at end of file diff --git a/erpnext/education/dashboard_chart/course_wise_student_count/course_wise_student_count.json b/erpnext/education/dashboard_chart/course_wise_student_count/course_wise_student_count.json new file mode 100644 index 0000000000..5441518def --- /dev/null +++ b/erpnext/education/dashboard_chart/course_wise_student_count/course_wise_student_count.json @@ -0,0 +1,31 @@ +{ + "based_on": "", + "chart_name": "Course wise Student Count", + "chart_type": "Group By", + "creation": "2020-07-27 17:24:39.136163", + "docstatus": 0, + "doctype": "Dashboard Chart", + "document_type": "Course Enrollment", + "dynamic_filters_json": "[]", + "filters_json": "[[\"Course Enrollment\",\"enrollment_date\",\"Timespan\",\"this year\",false]]", + "group_by_based_on": "course", + "group_by_type": "Count", + "idx": 0, + "is_public": 1, + "is_standard": 1, + "last_synced_on": "2020-07-27 17:24:56.184236", + "modified": "2020-07-27 17:25:46.232846", + "modified_by": "Administrator", + "module": "Education", + "name": "Course wise Student Count", + "number_of_groups": 0, + "owner": "Administrator", + "source": "", + "time_interval": "Yearly", + "timeseries": 0, + "timespan": "Last Year", + "type": "Donut", + "use_report_chart": 0, + "value_based_on": "", + "y_axis": [] +} \ No newline at end of file diff --git a/erpnext/education/dashboard_chart/instructor_gender_diversity_ratio/instructor_gender_diversity_ratio.json b/erpnext/education/dashboard_chart/instructor_gender_diversity_ratio/instructor_gender_diversity_ratio.json new file mode 100644 index 0000000000..b7ee509b92 --- /dev/null +++ b/erpnext/education/dashboard_chart/instructor_gender_diversity_ratio/instructor_gender_diversity_ratio.json @@ -0,0 +1,31 @@ +{ + "based_on": "", + "chart_name": "Instructor Gender Diversity Ratio", + "chart_type": "Group By", + "creation": "2020-07-23 18:35:02.544019", + "docstatus": 0, + "doctype": "Dashboard Chart", + "document_type": "Instructor", + "dynamic_filters_json": "[]", + "filters_json": "[[\"Instructor\",\"status\",\"=\",\"Active\",false]]", + "group_by_based_on": "gender", + "group_by_type": "Count", + "idx": 0, + "is_public": 1, + "is_standard": 1, + "last_synced_on": "2020-07-27 17:50:32.783820", + "modified": "2020-07-27 17:55:41.595260", + "modified_by": "Administrator", + "module": "Education", + "name": "Instructor Gender Diversity Ratio", + "number_of_groups": 0, + "owner": "Administrator", + "source": "", + "time_interval": "Yearly", + "timeseries": 0, + "timespan": "Last Year", + "type": "Donut", + "use_report_chart": 0, + "value_based_on": "", + "y_axis": [] +} \ No newline at end of file diff --git a/erpnext/education/dashboard_chart/program_enrollments/program_enrollments.json b/erpnext/education/dashboard_chart/program_enrollments/program_enrollments.json new file mode 100644 index 0000000000..2a4a4a3e0c --- /dev/null +++ b/erpnext/education/dashboard_chart/program_enrollments/program_enrollments.json @@ -0,0 +1,30 @@ +{ + "based_on": "enrollment_date", + "chart_name": "Program Enrollments", + "chart_type": "Count", + "creation": "2020-07-23 18:27:53.641616", + "docstatus": 0, + "doctype": "Dashboard Chart", + "document_type": "Program Enrollment", + "dynamic_filters_json": "[]", + "filters_json": "[[\"Program Enrollment\",\"docstatus\",\"=\",\"1\",false]]", + "group_by_type": "Count", + "idx": 0, + "is_public": 1, + "is_standard": 1, + "last_synced_on": "2020-07-27 17:50:32.203069", + "modified": "2020-07-27 17:51:59.022909", + "modified_by": "Administrator", + "module": "Education", + "name": "Program Enrollments", + "number_of_groups": 0, + "owner": "Administrator", + "source": "", + "time_interval": "Daily", + "timeseries": 1, + "timespan": "Last Month", + "type": "Line", + "use_report_chart": 0, + "value_based_on": "", + "y_axis": [] +} \ No newline at end of file diff --git a/erpnext/education/dashboard_chart/program_wise_enrollment/program_wise_enrollment.json b/erpnext/education/dashboard_chart/program_wise_enrollment/program_wise_enrollment.json new file mode 100644 index 0000000000..2ba138e332 --- /dev/null +++ b/erpnext/education/dashboard_chart/program_wise_enrollment/program_wise_enrollment.json @@ -0,0 +1,31 @@ +{ + "based_on": "", + "chart_name": "Program wise Enrollment", + "chart_type": "Group By", + "creation": "2020-07-23 18:23:45.192748", + "docstatus": 0, + "doctype": "Dashboard Chart", + "document_type": "Program Enrollment", + "dynamic_filters_json": "[]", + "filters_json": "[[\"Program Enrollment\",\"docstatus\",\"=\",\"1\",false],[\"Program Enrollment\",\"enrollment_date\",\"Timespan\",\"this year\",false]]", + "group_by_based_on": "program", + "group_by_type": "Count", + "idx": 0, + "is_public": 1, + "is_standard": 1, + "last_synced_on": "2020-07-27 17:50:32.629321", + "modified": "2020-07-27 17:53:36.269098", + "modified_by": "Administrator", + "module": "Education", + "name": "Program wise Enrollment", + "number_of_groups": 0, + "owner": "Administrator", + "source": "", + "time_interval": "Yearly", + "timeseries": 0, + "timespan": "Last Year", + "type": "Percentage", + "use_report_chart": 0, + "value_based_on": "", + "y_axis": [] +} \ No newline at end of file diff --git a/erpnext/education/dashboard_chart/program_wise_fee_collection/program_wise_fee_collection.json b/erpnext/education/dashboard_chart/program_wise_fee_collection/program_wise_fee_collection.json new file mode 100644 index 0000000000..38c1b6d7f3 --- /dev/null +++ b/erpnext/education/dashboard_chart/program_wise_fee_collection/program_wise_fee_collection.json @@ -0,0 +1,28 @@ +{ + "chart_name": "Program wise Fee Collection", + "chart_type": "Report", + "creation": "2020-08-05 16:19:53.398335", + "custom_options": "", + "docstatus": 0, + "doctype": "Dashboard Chart", + "dynamic_filters_json": "{\"from_date\":\"frappe.datetime.add_months(frappe.datetime.get_today(), -1)\",\"to_date\":\"frappe.datetime.nowdate()\"}", + "filters_json": "{}", + "group_by_type": "Count", + "idx": 0, + "is_public": 1, + "is_standard": 1, + "modified": "2020-08-05 16:20:47.436847", + "modified_by": "Administrator", + "module": "Education", + "name": "Program wise Fee Collection", + "number_of_groups": 0, + "owner": "Administrator", + "report_name": "Program wise Fee Collection", + "time_interval": "Yearly", + "timeseries": 0, + "timespan": "Last Year", + "type": "Bar", + "use_report_chart": 1, + "x_field": "", + "y_axis": [] +} \ No newline at end of file diff --git a/erpnext/education/dashboard_chart/student_category_wise_program_enrollments/student_category_wise_program_enrollments.json b/erpnext/education/dashboard_chart/student_category_wise_program_enrollments/student_category_wise_program_enrollments.json new file mode 100644 index 0000000000..88871457ec --- /dev/null +++ b/erpnext/education/dashboard_chart/student_category_wise_program_enrollments/student_category_wise_program_enrollments.json @@ -0,0 +1,31 @@ +{ + "based_on": "", + "chart_name": "Student Category wise Program Enrollments", + "chart_type": "Group By", + "creation": "2020-07-27 17:37:47.116446", + "docstatus": 0, + "doctype": "Dashboard Chart", + "document_type": "Program Enrollment", + "dynamic_filters_json": "[]", + "filters_json": "[[\"Program Enrollment\",\"enrollment_date\",\"Timespan\",\"this year\",false],[\"Program Enrollment\",\"docstatus\",\"=\",\"1\",false]]", + "group_by_based_on": "student_category", + "group_by_type": "Count", + "idx": 0, + "is_public": 1, + "is_standard": 1, + "last_synced_on": "2020-07-27 17:46:54.901911", + "modified": "2020-07-27 17:47:21.370866", + "modified_by": "Administrator", + "module": "Education", + "name": "Student Category wise Program Enrollments", + "number_of_groups": 0, + "owner": "Administrator", + "source": "", + "time_interval": "Yearly", + "timeseries": 0, + "timespan": "Last Year", + "type": "Donut", + "use_report_chart": 0, + "value_based_on": "", + "y_axis": [] +} \ No newline at end of file diff --git a/erpnext/education/dashboard_chart/student_gender_diversity_ratio/student_gender_diversity_ratio.json b/erpnext/education/dashboard_chart/student_gender_diversity_ratio/student_gender_diversity_ratio.json new file mode 100644 index 0000000000..ce602d29ee --- /dev/null +++ b/erpnext/education/dashboard_chart/student_gender_diversity_ratio/student_gender_diversity_ratio.json @@ -0,0 +1,30 @@ +{ + "based_on": "", + "chart_name": "Student Gender Diversity Ratio", + "chart_type": "Group By", + "creation": "2020-07-23 18:12:15.972123", + "docstatus": 0, + "doctype": "Dashboard Chart", + "document_type": "Student", + "dynamic_filters_json": "[]", + "filters_json": "[[\"Student\",\"enabled\",\"=\",1,false]]", + "group_by_based_on": "gender", + "group_by_type": "Count", + "idx": 0, + "is_public": 1, + "is_standard": 1, + "modified": "2020-07-23 18:12:21.606772", + "modified_by": "Administrator", + "module": "Education", + "name": "Student Gender Diversity Ratio", + "number_of_groups": 0, + "owner": "Administrator", + "source": "", + "time_interval": "Yearly", + "timeseries": 0, + "timespan": "Last Year", + "type": "Donut", + "use_report_chart": 0, + "value_based_on": "", + "y_axis": [] +} \ No newline at end of file diff --git a/erpnext/education/desk_page/education/education.json b/erpnext/education/desk_page/education/education.json index b341ec4b99..77ee8ecaf6 100644 --- a/erpnext/education/desk_page/education/education.json +++ b/erpnext/education/desk_page/education/education.json @@ -2,18 +2,13 @@ "cards": [ { "hidden": 0, - "label": "Tools", - "links": "[\n {\n \"label\": \"Student Attendance Tool\",\n \"name\": \"Student Attendance Tool\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Assessment Result Tool\",\n \"name\": \"Assessment Result Tool\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Student Group Creation Tool\",\n \"name\": \"Student Group Creation Tool\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Program Enrollment Tool\",\n \"name\": \"Program Enrollment Tool\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Course Scheduling Tool\",\n \"name\": \"Course Scheduling Tool\",\n \"type\": \"doctype\"\n }\n]" + "label": "Student and Instructor", + "links": "[\n {\n \"label\": \"Student\",\n \"name\": \"Student\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Instructor\",\n \"name\": \"Instructor\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Guardian\",\n \"name\": \"Guardian\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Student Group\",\n \"name\": \"Student Group\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Student Log\",\n \"name\": \"Student Log\",\n \"type\": \"doctype\"\n }\n]" }, { "hidden": 0, - "label": "Other Reports", - "links": "[\n {\n \"dependencies\": [\n \"Program Enrollment\"\n ],\n \"doctype\": \"Program Enrollment\",\n \"is_query_report\": true,\n \"label\": \"Student and Guardian Contact Details\",\n \"name\": \"Student and Guardian Contact Details\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Student Attendance\"\n ],\n \"doctype\": \"Student Attendance\",\n \"is_query_report\": true,\n \"label\": \"Student Monthly Attendance Sheet\",\n \"name\": \"Student Monthly Attendance Sheet\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Fees\"\n ],\n \"doctype\": \"Fees\",\n \"is_query_report\": true,\n \"label\": \"Student Fee Collection\",\n \"name\": \"Student Fee Collection\",\n \"type\": \"report\"\n }\n]" - }, - { - "hidden": 0, - "label": "Settings", - "links": "[\n {\n \"label\": \"Student Category\",\n \"name\": \"Student Category\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Student Batch Name\",\n \"name\": \"Student Batch Name\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Grading Scale\",\n \"name\": \"Grading Scale\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Academic Term\",\n \"name\": \"Academic Term\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Academic Year\",\n \"name\": \"Academic Year\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Education Settings\",\n \"name\": \"Education Settings\",\n \"type\": \"doctype\"\n }\n]" + "label": "Masters", + "links": "[\n {\n \"label\": \"Program\",\n \"name\": \"Program\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Course\",\n \"name\": \"Course\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Topic\",\n \"name\": \"Topic\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Room\",\n \"name\": \"Room\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n }\n]" }, { "hidden": 0, @@ -22,33 +17,18 @@ }, { "hidden": 0, - "label": "Attendance", - "links": "[\n {\n \"label\": \"Student Attendance\",\n \"name\": \"Student Attendance\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Student Leave Application\",\n \"name\": \"Student Leave Application\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Student Attendance\"\n ],\n \"doctype\": \"Student Attendance\",\n \"is_query_report\": true,\n \"label\": \"Absent Student Report\",\n \"name\": \"Absent Student Report\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Student Attendance\"\n ],\n \"doctype\": \"Student Attendance\",\n \"is_query_report\": true,\n \"label\": \"Student Batch-Wise Attendance\",\n \"name\": \"Student Batch-Wise Attendance\",\n \"type\": \"report\"\n }\n]" + "label": "Settings", + "links": "[\n {\n \"label\": \"Education Settings\",\n \"name\": \"Education Settings\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Student Category\",\n \"name\": \"Student Category\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Student Batch Name\",\n \"name\": \"Student Batch Name\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Grading Scale\",\n \"name\": \"Grading Scale\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Academic Term\",\n \"name\": \"Academic Term\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Academic Year\",\n \"name\": \"Academic Year\",\n \"type\": \"doctype\"\n }\n]" }, { "hidden": 0, "label": "Admission", - "links": "[\n {\n \"label\": \"Student Applicant\",\n \"name\": \"Student Applicant\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Student Admission\",\n \"name\": \"Student Admission\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Program Enrollment\",\n \"name\": \"Program Enrollment\",\n \"type\": \"doctype\"\n }\n]" + "links": "[\n {\n \"label\": \"Student Applicant\",\n \"name\": \"Student Applicant\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Student Admission\",\n \"name\": \"Student Admission\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Program Enrollment\",\n \"name\": \"Program Enrollment\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Course Enrollment\",\n \"name\": \"Course Enrollment\",\n \"type\": \"doctype\"\n }\n]" }, { "hidden": 0, - "label": "Assessment", - "links": "[\n {\n \"label\": \"Assessment Plan\",\n \"name\": \"Assessment Plan\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Assessment Group\",\n \"link\": \"Tree/Assessment Group\",\n \"name\": \"Assessment Group\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Assessment Result\",\n \"name\": \"Assessment Result\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Assessment Criteria\",\n \"name\": \"Assessment Criteria\",\n \"type\": \"doctype\"\n }\n]" - }, - { - "hidden": 0, - "label": "Student", - "links": "[\n {\n \"label\": \"Student\",\n \"name\": \"Student\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Guardian\",\n \"name\": \"Guardian\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Student Log\",\n \"name\": \"Student Log\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Student Group\",\n \"name\": \"Student Group\",\n \"type\": \"doctype\"\n }\n]" - }, - { - "hidden": 0, - "label": "Masters", - "links": "[\n {\n \"label\": \"Program\",\n \"name\": \"Program\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Course\",\n \"name\": \"Course\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Topic\",\n \"name\": \"Topic\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Instructor\",\n \"name\": \"Instructor\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Room\",\n \"name\": \"Room\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n }\n]" - }, - { - "hidden": 0, - "label": "LMS Activity", - "links": "[\n {\n \"label\": \"Course Enrollment\",\n \"name\": \"Course Enrollment\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Course Activity\",\n \"name\": \"Course Activity\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Quiz Activity\",\n \"name\": \"Quiz Activity\",\n \"type\": \"doctype\"\n }\n]" + "label": "Fees", + "links": "[\n {\n \"label\": \"Fee Structure\",\n \"name\": \"Fee Structure\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Fee Category\",\n \"name\": \"Fee Category\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Fee Schedule\",\n \"name\": \"Fee Schedule\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Fees\",\n \"name\": \"Fees\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Fees\"\n ],\n \"doctype\": \"Fees\",\n \"is_query_report\": true,\n \"label\": \"Student Fee Collection Report\",\n \"name\": \"Student Fee Collection\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Fees\"\n ],\n \"doctype\": \"Fees\",\n \"is_query_report\": true,\n \"label\": \"Program wise Fee Collection Report\",\n \"name\": \"Program wise Fee Collection\",\n \"type\": \"report\"\n }\n]" }, { "hidden": 0, @@ -57,8 +37,18 @@ }, { "hidden": 0, - "label": "Fees", - "links": "[\n {\n \"label\": \"Fees\",\n \"name\": \"Fees\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Fee Schedule\",\n \"name\": \"Fee Schedule\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Fee Structure\",\n \"name\": \"Fee Structure\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Fee Category\",\n \"name\": \"Fee Category\",\n \"type\": \"doctype\"\n }\n]" + "label": "Attendance", + "links": "[\n {\n \"label\": \"Student Attendance\",\n \"name\": \"Student Attendance\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Student Leave Application\",\n \"name\": \"Student Leave Application\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Student Attendance\"\n ],\n \"doctype\": \"Student Attendance\",\n \"is_query_report\": true,\n \"label\": \"Student Monthly Attendance Sheet\",\n \"name\": \"Student Monthly Attendance Sheet\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Student Attendance\"\n ],\n \"doctype\": \"Student Attendance\",\n \"is_query_report\": true,\n \"label\": \"Absent Student Report\",\n \"name\": \"Absent Student Report\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Student Attendance\"\n ],\n \"doctype\": \"Student Attendance\",\n \"is_query_report\": true,\n \"label\": \"Student Batch-Wise Attendance\",\n \"name\": \"Student Batch-Wise Attendance\",\n \"type\": \"report\"\n }\n]" + }, + { + "hidden": 0, + "label": "LMS Activity", + "links": "[\n {\n \"label\": \"Course Enrollment\",\n \"name\": \"Course Enrollment\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Course Activity\",\n \"name\": \"Course Activity\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Quiz Activity\",\n \"name\": \"Quiz Activity\",\n \"type\": \"doctype\"\n }\n]" + }, + { + "hidden": 0, + "label": "Assessment", + "links": "[\n {\n \"label\": \"Assessment Plan\",\n \"name\": \"Assessment Plan\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Assessment Group\",\n \"link\": \"Tree/Assessment Group\",\n \"name\": \"Assessment Group\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Assessment Result\",\n \"name\": \"Assessment Result\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Assessment Criteria\",\n \"name\": \"Assessment Criteria\",\n \"type\": \"doctype\"\n }\n]" }, { "hidden": 0, @@ -67,28 +57,98 @@ }, { "hidden": 0, - "label": "Reports", - "links": "[\n {\n \"dependencies\": [\n \"Fees\"\n ],\n \"doctype\": \"Fees\",\n \"is_query_report\": true,\n \"label\": \"Student Fee Collection\",\n \"name\": \"Student Fee Collection\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Student Attendance\"\n ],\n \"doctype\": \"Student Attendance\",\n \"is_query_report\": true,\n \"label\": \"Student Monthly Attendance Sheet\",\n \"name\": \"Student Monthly Attendance Sheet\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Student Attendance\"\n ],\n \"doctype\": \"Student Attendance\",\n \"is_query_report\": true,\n \"label\": \"Absent Student Report\",\n \"name\": \"Absent Student Report\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Program Enrollment\"\n ],\n \"doctype\": \"Program Enrollment\",\n \"is_query_report\": true,\n \"label\": \"Student and Guardian Contact Details\",\n \"name\": \"Student and Guardian Contact Details\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Student Attendance\"\n ],\n \"doctype\": \"Student Attendance\",\n \"is_query_report\": true,\n \"label\": \"Student Batch-Wise Attendance\",\n \"name\": \"Student Batch-Wise Attendance\",\n \"type\": \"report\"\n }\n]" + "label": "Tools", + "links": "[\n {\n \"label\": \"Student Attendance Tool\",\n \"name\": \"Student Attendance Tool\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Assessment Result Tool\",\n \"name\": \"Assessment Result Tool\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Student Group Creation Tool\",\n \"name\": \"Student Group Creation Tool\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Program Enrollment Tool\",\n \"name\": \"Program Enrollment Tool\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Course Scheduling Tool\",\n \"name\": \"Course Scheduling Tool\",\n \"type\": \"doctype\"\n }\n]" + }, + { + "hidden": 0, + "label": "Other Reports", + "links": "[\n {\n \"dependencies\": [\n \"Program Enrollment\"\n ],\n \"doctype\": \"Program Enrollment\",\n \"is_query_report\": true,\n \"label\": \"Student and Guardian Contact Details\",\n \"name\": \"Student and Guardian Contact Details\",\n \"type\": \"report\"\n }\n]" } ], "category": "Domains", - "charts": [], + "charts": [ + { + "chart_name": "Program Enrollments", + "label": "Program Enrollments" + } + ], "creation": "2020-03-02 17:22:57.066401", "developer_mode_only": 0, "disable_user_customization": 0, "docstatus": 0, "doctype": "Desk Page", "extends_another_page": 0, + "hide_custom": 0, "idx": 0, "is_standard": 1, "label": "Education", - "modified": "2020-05-22 01:09:13.058482", + "modified": "2020-07-27 19:35:18.832694", "modified_by": "Administrator", "module": "Education", "name": "Education", + "onboarding": "Education", "owner": "Administrator", "pin_to_bottom": 0, "pin_to_top": 0, "restrict_to_domain": "Education", - "shortcuts": [] + "shortcuts": [ + { + "color": "#cef6d1", + "format": "{} Active", + "label": "Student", + "link_to": "Student", + "stats_filter": "{\n \"enabled\": 1\n}", + "type": "DocType" + }, + { + "color": "#cef6d1", + "format": "{} Active", + "label": "Instructor", + "link_to": "Instructor", + "stats_filter": "{\n \"status\": \"Active\"\n}", + "type": "DocType" + }, + { + "color": "", + "format": "", + "label": "Program", + "link_to": "Program", + "stats_filter": "", + "type": "DocType" + }, + { + "label": "Course", + "link_to": "Course", + "type": "DocType" + }, + { + "color": "#ffe8cd", + "format": "{} Unpaid", + "label": "Fees", + "link_to": "Fees", + "stats_filter": "{\n \"outstanding_amount\": [\"!=\", 0.0]\n}", + "type": "DocType" + }, + { + "label": "Student Monthly Attendance Sheet", + "link_to": "Student Monthly Attendance Sheet", + "type": "Report" + }, + { + "label": "Course Scheduling Tool", + "link_to": "Course Scheduling Tool", + "type": "DocType" + }, + { + "label": "Student Attendance Tool", + "link_to": "Student Attendance Tool", + "type": "DocType" + }, + { + "label": "Dashboard", + "link_to": "Education", + "type": "Dashboard" + } + ] } \ No newline at end of file diff --git a/erpnext/education/doctype/assessment_result/assessment_result.js b/erpnext/education/doctype/assessment_result/assessment_result.js index 12fdd91c25..63d1aee0cb 100644 --- a/erpnext/education/doctype/assessment_result/assessment_result.js +++ b/erpnext/education/doctype/assessment_result/assessment_result.js @@ -6,10 +6,11 @@ frappe.ui.form.on('Assessment Result', { if (!frm.doc.__islocal) { frm.trigger('setup_chart'); } + frm.set_df_property('details', 'read_only', 1); }, onload: function(frm) { - frm.set_query('assessment_plan', function(){ + frm.set_query('assessment_plan', function() { return { filters: { docstatus: 1 @@ -27,14 +28,14 @@ frappe.ui.form.on('Assessment Result', { }, callback: function(r) { if (r.message) { - frm.doc.details = []; + frappe.model.clear_table(frm.doc, 'details'); $.each(r.message, function(i, d) { - var row = frappe.model.add_child(frm.doc, 'Assessment Result Detail', 'details'); + var row = frm.add_child('details'); row.assessment_criteria = d.assessment_criteria; row.maximum_score = d.maximum_score; }); + frm.refresh_field('details'); } - refresh_field('details'); } }); } @@ -80,7 +81,7 @@ frappe.ui.form.on('Assessment Result Detail', { score: function(frm, cdt, cdn) { var d = locals[cdt][cdn]; - if(!d.maximum_score || !frm.doc.grading_scale) { + if (!d.maximum_score || !frm.doc.grading_scale) { d.score = ''; frappe.throw(__('Please fill in all the details to generate Assessment Result.')); } diff --git a/erpnext/education/doctype/assessment_result/assessment_result.json b/erpnext/education/doctype/assessment_result/assessment_result.json index 212d47cff0..7a893aabb8 100644 --- a/erpnext/education/doctype/assessment_result/assessment_result.json +++ b/erpnext/education/doctype/assessment_result/assessment_result.json @@ -1,724 +1,182 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, + "actions": [], "allow_import": 1, - "allow_rename": 0, "autoname": "EDU-RES-.YYYY.-.#####", - "beta": 0, "creation": "2015-11-13 17:18:06.468332", - "custom": 0, - "docstatus": 0, "doctype": "DocType", - "document_type": "", "editable_grid": 1, "engine": "InnoDB", + "field_order": [ + "assessment_plan", + "program", + "course", + "academic_year", + "academic_term", + "column_break_3", + "student", + "student_name", + "student_group", + "assessment_group", + "grading_scale", + "section_break_5", + "details", + "section_break_8", + "maximum_score", + "column_break_11", + "total_score", + "grade", + "section_break_13", + "comment", + "amended_from" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "assessment_plan", "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, - "in_standard_filter": 0, "label": "Assessment Plan", - "length": 0, - "no_copy": 0, "options": "Assessment Plan", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fetch_from": "assessment_plan.program", "fieldname": "program", "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": "Program", - "length": 0, - "no_copy": 0, - "options": "Program", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "options": "Program" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fetch_from": "assessment_plan.course", "fieldname": "course", "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": "Course", - "length": 0, - "no_copy": 0, - "options": "Course", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "options": "Course" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fetch_from": "assessment_plan.academic_year", "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 + "options": "Academic Year" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fetch_from": "assessment_plan.academic_term", "fieldname": "academic_term", "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Academic Term", - "length": 0, - "no_copy": 0, - "options": "Academic Term", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "options": "Academic Term" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "column_break_3", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldtype": "Column Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "student", "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, "in_global_search": 1, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Student", - "length": 0, - "no_copy": 0, "options": "Student", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fetch_from": "student.title", "fieldname": "student_name", "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, "in_global_search": 1, "in_list_view": 1, - "in_standard_filter": 0, "label": "Student Name", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "read_only": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fetch_from": "assessment_plan.student_group", "fieldname": "student_group", "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Student Group", - "length": 0, - "no_copy": 0, - "options": "Student Group", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "options": "Student Group" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fetch_from": "assessment_plan.assessment_group", "fieldname": "assessment_group", "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Assessment Group", - "length": 0, - "no_copy": 0, - "options": "Assessment Group", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "options": "Assessment Group" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fetch_from": "assessment_plan.grading_scale", "fieldname": "grading_scale", "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": "Grading Scale", - "length": 0, - "no_copy": 0, "options": "Grading Scale", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "read_only": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "section_break_5", "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": "Result", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Result" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "", "fieldname": "details", "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": "Details", - "length": 0, - "no_copy": 0, "options": "Assessment Result Detail", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "section_break_8", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldtype": "Section Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fetch_from": "assessment_plan.maximum_assessment_score", "fieldname": "maximum_score", "fieldtype": "Float", - "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": "Maximum Score", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "read_only": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fetch_from": "assessment_plan.maximum_assessment_score", "fieldname": "column_break_11", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldtype": "Column Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "total_score", "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, - "in_standard_filter": 0, "label": "Total Score", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "read_only": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "grade", "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, - "in_standard_filter": 0, "label": "Grade", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "read_only": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "section_break_13", "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": "Summary", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Summary" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "comment", "fieldtype": "Small Text", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Comment", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Comment" }, { - "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": "Assessment Result", - "permlevel": 0, "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "read_only": 1 } ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, "is_submittable": 1, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2018-08-30 02:10:36.813413", + "links": [], + "modified": "2020-08-03 11:47:54.119486", "modified_by": "Administrator", "module": "Education", "name": "Assessment Result", - "name_case": "", "owner": "Administrator", "permissions": [ { @@ -728,28 +186,18 @@ "delete": 1, "email": 1, "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "Academics User", - "set_user_permissions": 0, "share": 1, "submit": 1, "write": 1 } ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, "restrict_to_domain": "Education", "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "DESC", - "title_field": "student_name", - "track_changes": 0, - "track_seen": 0, - "track_views": 0 + "title_field": "student_name" } \ No newline at end of file diff --git a/erpnext/education/doctype/assessment_result_detail/assessment_result_detail.json b/erpnext/education/doctype/assessment_result_detail/assessment_result_detail.json index 85d943beaa..450f41cbbb 100644 --- a/erpnext/education/doctype/assessment_result_detail/assessment_result_detail.json +++ b/erpnext/education/doctype/assessment_result_detail/assessment_result_detail.json @@ -1,194 +1,66 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "", - "beta": 0, - "creation": "2016-12-14 17:44:35.583123", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "creation": "2016-12-14 17:44:35.583123", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "assessment_criteria", + "maximum_score", + "column_break_2", + "score", + "grade" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 4, - "fieldname": "assessment_criteria", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Assessment Criteria", - "length": 0, - "no_copy": 0, - "options": "Assessment Criteria", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "columns": 4, + "fieldname": "assessment_criteria", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Assessment Criteria", + "options": "Assessment Criteria", + "read_only": 1, + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 2, - "fieldname": "maximum_score", - "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Maximum Score", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "columns": 2, + "fieldname": "maximum_score", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Maximum Score", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_2", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 2, - "fieldname": "score", - "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Score", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "columns": 2, + "fieldname": "score", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Score", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 2, - "fieldname": "grade", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Grade", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 + "columns": 2, + "fieldname": "grade", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Grade", + "read_only": 1 } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2017-11-10 19:11:14.362410", - "modified_by": "Administrator", - "module": "Education", - "name": "Assessment Result Detail", - "name_case": "", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "restrict_to_domain": "Education", - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 0, - "track_seen": 0 + ], + "istable": 1, + "links": [], + "modified": "2020-07-31 13:27:17.699022", + "modified_by": "Administrator", + "module": "Education", + "name": "Assessment Result Detail", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "restrict_to_domain": "Education", + "sort_field": "modified", + "sort_order": "DESC" } \ No newline at end of file diff --git a/erpnext/education/doctype/fees/fees.json b/erpnext/education/doctype/fees/fees.json index 99f9f4f4cd..0fb7672890 100644 --- a/erpnext/education/doctype/fees/fees.json +++ b/erpnext/education/doctype/fees/fees.json @@ -160,7 +160,8 @@ "in_list_view": 1, "in_standard_filter": 1, "label": "Program", - "options": "Program" + "options": "Program", + "reqd": 1 }, { "fieldname": "student_batch", @@ -339,7 +340,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2020-07-18 05:00:00.621010", + "modified": "2020-08-05 14:05:47.728409", "modified_by": "Administrator", "module": "Education", "name": "Fees", diff --git a/erpnext/education/doctype/fees/test_fees.py b/erpnext/education/doctype/fees/test_fees.py index b182992922..eedc2ae730 100644 --- a/erpnext/education/doctype/fees/test_fees.py +++ b/erpnext/education/doctype/fees/test_fees.py @@ -7,7 +7,7 @@ import frappe import unittest from frappe.utils import nowdate from frappe.utils.make_random import get_random - +from erpnext.education.doctype.program.test_program import make_program_and_linked_courses # test_records = frappe.get_test_records('Fees') @@ -15,6 +15,7 @@ class TestFees(unittest.TestCase): def test_fees(self): student = get_random("Student") + program = make_program_and_linked_courses("_Test Program 1", ["_Test Course 1", "_Test Course 2"]) fee = frappe.new_doc("Fees") fee.posting_date = nowdate() fee.due_date = nowdate() @@ -23,6 +24,7 @@ class TestFees(unittest.TestCase): fee.income_account = "Sales - _TC" fee.cost_center = "_Test Cost Center - _TC" fee.company = "_Test Company" + fee.program = program.name fee.extend("components", [ { diff --git a/erpnext/education/doctype/instructor/instructor.json b/erpnext/education/doctype/instructor/instructor.json index 5367c0e66f..a417391711 100644 --- a/erpnext/education/doctype/instructor/instructor.json +++ b/erpnext/education/doctype/instructor/instructor.json @@ -1,348 +1,125 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 1, - "allow_rename": 0, - "autoname": "naming_series:", - "beta": 0, - "creation": "2015-11-04 15:56:30.004034", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "Other", - "editable_grid": 0, - "engine": "InnoDB", + "actions": [], + "allow_import": 1, + "autoname": "naming_series:", + "creation": "2015-11-04 15:56:30.004034", + "doctype": "DocType", + "document_type": "Other", + "engine": "InnoDB", + "field_order": [ + "instructor_name", + "employee", + "gender", + "column_break_5", + "status", + "naming_series", + "department", + "image", + "log_details", + "instructor_log" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "instructor_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": "Instructor 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": "instructor_name", + "fieldtype": "Data", + "in_global_search": 1, + "label": "Instructor Name", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "employee", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Employee", - "length": 0, - "no_copy": 0, - "options": "Employee", - "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": "employee", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Employee", + "options": "Employee" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_5", - "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_5", + "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": 0, - "options": "EDU-INS-.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", + "options": "EDU-INS-.YYYY.-", + "set_only_once": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_from": "employee.department", - "fieldname": "department", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 1, - "label": "Department", - "length": 0, - "no_copy": 0, - "options": "Department", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fetch_from": "employee.department", + "fieldname": "department", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Department", + "options": "Department" + }, { - "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, - "columns": 0, - "fieldname": "log_details", - "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": "Instructor Log", - "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": "log_details", + "fieldtype": "Section Break", + "label": "Instructor Log" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "instructor_log", - "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": "Instructor Log", - "length": 0, - "no_copy": 0, - "options": "Instructor Log", - "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": "instructor_log", + "fieldtype": "Table", + "label": "Instructor Log", + "options": "Instructor Log" + }, + { + "default": "Active", + "fieldname": "status", + "fieldtype": "Select", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Status", + "options": "Active\nLeft" + }, + { + "fetch_from": "employee.gender", + "fieldname": "gender", + "fieldtype": "Link", + "label": "Gender", + "options": "Gender", + "read_only_depends_on": "employee" } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_field": "image", - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "menu_index": 0, - "modified": "2019-01-30 11:28:17.571207", - "modified_by": "Administrator", - "module": "Education", - "name": "Instructor", - "name_case": "", - "owner": "Administrator", + ], + "image_field": "image", + "links": [], + "modified": "2020-07-23 18:33:57.904398", + "modified_by": "Administrator", + "module": "Education", + "name": "Instructor", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, - "create": 0, - "delete": 0, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Instructor", - "set_user_permissions": 0, - "share": 0, - "submit": 0, - "write": 0 - }, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Instructor" + }, { - "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": "Education Manager", - "set_user_permissions": 1, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Education Manager", + "set_user_permissions": 1, + "share": 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": "instructor_name", - "track_changes": 0, - "track_seen": 0, - "track_views": 0 + ], + "restrict_to_domain": "Education", + "show_name_in_global_search": 1, + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "instructor_name" } \ No newline at end of file diff --git a/erpnext/education/doctype/program_enrollment/program_enrollment.py b/erpnext/education/doctype/program_enrollment/program_enrollment.py index 7536172891..3e27670d05 100644 --- a/erpnext/education/doctype/program_enrollment/program_enrollment.py +++ b/erpnext/education/doctype/program_enrollment/program_enrollment.py @@ -97,6 +97,7 @@ class ProgramEnrollment(Document): return quiz_progress @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def get_program_courses(doctype, txt, searchfield, start, page_len, filters): if filters.get('program'): return frappe.db.sql("""select course, course_name from `tabProgram Course` @@ -115,6 +116,7 @@ def get_program_courses(doctype, txt, searchfield, start, page_len, filters): }) @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def get_students(doctype, txt, searchfield, start, page_len, filters): if not filters.get("academic_term"): filters["academic_term"] = frappe.defaults.get_defaults().academic_term diff --git a/erpnext/education/doctype/student/student.json b/erpnext/education/doctype/student/student.json index bee915e91d..ac65c0cd7b 100644 --- a/erpnext/education/doctype/student/student.json +++ b/erpnext/education/doctype/student/student.json @@ -1,1353 +1,305 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 1, - "allow_rename": 1, - "autoname": "naming_series:", - "beta": 0, - "creation": "2015-09-07 13:00:55.938280", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "Document", - "editable_grid": 0, - "engine": "InnoDB", + "actions": [], + "allow_import": 1, + "allow_rename": 1, + "autoname": "naming_series:", + "creation": "2015-09-07 13:00:55.938280", + "doctype": "DocType", + "document_type": "Document", + "engine": "InnoDB", + "field_order": [ + "section_break_1", + "enabled", + "section_break_3", + "first_name", + "middle_name", + "last_name", + "user", + "column_break_4", + "naming_series", + "student_email_id", + "student_mobile_number", + "joining_date", + "image", + "section_break_7", + "date_of_birth", + "blood_group", + "column_break_3", + "gender", + "nationality", + "student_applicant", + "section_break_22", + "address_line_1", + "address_line_2", + "pincode", + "column_break_20", + "city", + "state", + "section_break_18", + "guardians", + "section_break_20", + "siblings", + "exit", + "date_of_leaving", + "leaving_certificate_number", + "column_break_31", + "reason_for_leaving", + "title" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "section_break_1", - "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_1", + "fieldtype": "Section Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "1", - "fetch_if_empty": 0, - "fieldname": "enabled", - "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": "Enabled", - "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": "1", + "fieldname": "enabled", + "fieldtype": "Check", + "label": "Enabled" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "section_break_3", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "section_break_3", + "fieldtype": "Section Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "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, - "fetch_if_empty": 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, - "fetch_if_empty": 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, - "fetch_if_empty": 0, - "fieldname": "user", - "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": "User ID", - "length": 0, - "no_copy": 0, - "options": "User", - "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": "user", + "fieldtype": "Link", + "label": "User ID", + "options": "User" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "column_break_4", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "", - "fetch_if_empty": 0, - "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-STU-.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-STU-.YYYY.-", + "set_only_once": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "student_email_id", - "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": "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", + "in_global_search": 1, + "label": "Student Email Address", + "reqd": 1, "unique": 1 - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "student_mobile_number", - "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": "Student Mobile Number", - "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": "student_mobile_number", + "fieldtype": "Data", + "in_global_search": 1, + "label": "Student Mobile Number" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "Today", - "fetch_if_empty": 0, - "fieldname": "joining_date", - "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Joining Date", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "default": "Today", + "fieldname": "joining_date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Joining Date" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 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", "width": "10" - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": "", - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "section_break_7", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "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_7", + "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, - "fetch_if_empty": 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, - "fetch_if_empty": 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, - "fetch_if_empty": 0, - "fieldname": "column_break_3", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 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\nOther", - "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": "Link", + "label": "Gender", + "options": "Gender" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 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, - "fetch_if_empty": 0, - "fieldname": "student_applicant", - "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 Applicant", - "length": 0, - "no_copy": 0, - "options": "Student Applicant", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "student_applicant", + "fieldtype": "Link", + "label": "Student Applicant", + "options": "Student Applicant", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "section_break_22", - "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": "section_break_22", + "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, - "fetch_if_empty": 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, - "fetch_if_empty": 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, - "fetch_if_empty": 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, - "fetch_if_empty": 0, - "fieldname": "column_break_20", - "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_20", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "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, - "fetch_if_empty": 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": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "section_break_18", - "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 - }, + "fieldname": "section_break_18", + "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, - "fetch_if_empty": 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, - "fetch_if_empty": 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": "Sibling Details", - "length": 0, - "no_copy": 0, - "options": "Country", - "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": "Sibling Details", + "options": "Country" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 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": 1, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "exit", - "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": "Exit", - "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": "exit", + "fieldtype": "Section Break", + "label": "Exit" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "date_of_leaving", - "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 Leaving", - "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_leaving", + "fieldtype": "Date", + "label": "Date of Leaving" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "leaving_certificate_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": "Leaving Certificate 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": "leaving_certificate_number", + "fieldtype": "Data", + "label": "Leaving Certificate Number" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "column_break_31", - "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_31", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "reason_for_leaving", - "fieldtype": "Text", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Reason For Leaving", - "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": "reason_for_leaving", + "fieldtype": "Text", + "label": "Reason For Leaving" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "", - "fetch_if_empty": 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" } - ], - "has_web_view": 0, - "hide_toolbar": 0, - "idx": 0, - "image_field": "image", - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "menu_index": 0, - "modified": "2019-04-10 17:46:26.893020", - "modified_by": "Administrator", - "module": "Education", - "name": "Student", - "name_case": "", - "owner": "Administrator", + ], + "image_field": "image", + "links": [], + "modified": "2020-07-23 18:14:06.366442", + "modified_by": "Administrator", + "module": "Education", + "name": "Student", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, - "create": 0, - "delete": 0, - "email": 0, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 0, - "read": 1, - "report": 0, - "role": "Instructor", - "set_user_permissions": 0, - "share": 0, - "submit": 0, - "write": 0 - }, + "read": 1, + "role": "Instructor" + }, { - "amend": 0, - "cancel": 0, - "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": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "import": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Academics User", + "share": 1, "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": "Student", - "set_user_permissions": 0, - "share": 1, - "submit": 0, - "write": 0 - }, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Student", + "share": 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": "LMS User", - "set_user_permissions": 0, - "share": 1, - "submit": 0, - "write": 0 + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "LMS User", + "share": 1 } - ], - "quick_entry": 0, - "read_only": 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/education/doctype/student_group/student_group.py b/erpnext/education/doctype/student_group/student_group.py index 8b61c899bc..0260b80864 100644 --- a/erpnext/education/doctype/student_group/student_group.py +++ b/erpnext/education/doctype/student_group/student_group.py @@ -108,6 +108,7 @@ def get_program_enrollment(academic_year, academic_term=None, program=None, batc @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def fetch_students(doctype, txt, searchfield, start, page_len, filters): if filters.get("group_based_on") != "Activity": enrolled_students = get_program_enrollment(filters.get('academic_year'), filters.get('academic_term'), diff --git a/erpnext/education/education_dashboard/education/education.json b/erpnext/education/education_dashboard/education/education.json new file mode 100644 index 0000000000..41d33758b9 --- /dev/null +++ b/erpnext/education/education_dashboard/education/education.json @@ -0,0 +1,62 @@ +{ + "cards": [ + { + "card": "Total Students" + }, + { + "card": "Total Instructors" + }, + { + "card": "Program Enrollments" + }, + { + "card": "Student Applicants to Review" + } + ], + "charts": [ + { + "chart": "Program Enrollments", + "width": "Full" + }, + { + "chart": "Program wise Enrollment", + "width": "Half" + }, + { + "chart": "Course wise Enrollment", + "width": "Half" + }, + { + "chart": "Course wise Student Count", + "width": "Half" + }, + { + "chart": "Student Category wise Program Enrollments", + "width": "Half" + }, + { + "chart": "Student Gender Diversity Ratio", + "width": "Half" + }, + { + "chart": "Instructor Gender Diversity Ratio", + "width": "Half" + }, + { + "chart": "Program wise Fee Collection", + "width": "Full" + } + ], + "creation": "2020-07-22 18:51:02.195762", + "dashboard_name": "Education", + "docstatus": 0, + "doctype": "Dashboard", + "idx": 0, + "is_default": 0, + "is_standard": 1, + "modified": "2020-08-05 16:22:17.428101", + "modified_by": "Administrator", + "module": "Education", + "name": "Education", + "owner": "Administrator" +} \ No newline at end of file diff --git a/erpnext/education/module_onboarding/education/education.json b/erpnext/education/module_onboarding/education/education.json new file mode 100644 index 0000000000..e5f0fec3d1 --- /dev/null +++ b/erpnext/education/module_onboarding/education/education.json @@ -0,0 +1,50 @@ +{ + "allow_roles": [ + { + "role": "Education Manager" + } + ], + "creation": "2020-07-27 19:02:49.561391", + "docstatus": 0, + "doctype": "Module Onboarding", + "documentation_url": "https://docs.erpnext.com/docs/user/manual/en/education", + "idx": 0, + "is_complete": 0, + "modified": "2020-07-27 21:10:46.722961", + "modified_by": "Administrator", + "module": "Education", + "name": "Education", + "owner": "Administrator", + "steps": [ + { + "step": "Create a Student" + }, + { + "step": "Create an Instructor" + }, + { + "step": "Introduction to Program and Courses" + }, + { + "step": "Create a Topic" + }, + { + "step": "Create a Course" + }, + { + "step": "Create a Program" + }, + { + "step": "Enroll a Student in a Program" + }, + { + "step": "Introduction to Student Group" + }, + { + "step": "Introduction to Student Attendance" + } + ], + "subtitle": "Students, Instructors, Programs and more.", + "success_message": "The Education Module is all set up!", + "title": "Let's Set Up the Education Module." +} diff --git a/erpnext/education/number_card/program_enrollments/program_enrollments.json b/erpnext/education/number_card/program_enrollments/program_enrollments.json new file mode 100644 index 0000000000..5847679ddd --- /dev/null +++ b/erpnext/education/number_card/program_enrollments/program_enrollments.json @@ -0,0 +1,23 @@ +{ + "aggregate_function_based_on": "", + "creation": "2020-07-27 18:26:27.005186", + "docstatus": 0, + "doctype": "Number Card", + "document_type": "Program Enrollment", + "dynamic_filters_json": "[]", + "filters_json": "[[\"Program Enrollment\",\"docstatus\",\"=\",\"1\",false],[\"Program Enrollment\",\"enrollment_date\",\"Timespan\",\"this year\",false]]", + "function": "Count", + "idx": 0, + "is_public": 1, + "is_standard": 1, + "label": "Program Enrollments", + "modified": "2020-07-27 18:26:32.512624", + "modified_by": "Administrator", + "module": "Education", + "name": "Program Enrollments", + "owner": "Administrator", + "report_function": "Sum", + "show_percentage_stats": 1, + "stats_time_interval": "Yearly", + "type": "Document Type" +} \ No newline at end of file diff --git a/erpnext/education/number_card/student_applicants_to_review/student_applicants_to_review.json b/erpnext/education/number_card/student_applicants_to_review/student_applicants_to_review.json new file mode 100644 index 0000000000..258667a2d4 --- /dev/null +++ b/erpnext/education/number_card/student_applicants_to_review/student_applicants_to_review.json @@ -0,0 +1,23 @@ +{ + "aggregate_function_based_on": "", + "creation": "2020-07-27 18:42:33.366862", + "docstatus": 0, + "doctype": "Number Card", + "document_type": "Student Applicant", + "dynamic_filters_json": "[]", + "filters_json": "[[\"Student Applicant\",\"application_status\",\"=\",\"Applied\",false]]", + "function": "Count", + "idx": 0, + "is_public": 1, + "is_standard": 1, + "label": "Student Applicants to Review", + "modified": "2020-07-27 18:42:42.739710", + "modified_by": "Administrator", + "module": "Education", + "name": "Student Applicants to Review", + "owner": "Administrator", + "report_function": "Sum", + "show_percentage_stats": 1, + "stats_time_interval": "Monthly", + "type": "Document Type" +} \ No newline at end of file diff --git a/erpnext/education/number_card/total_instructors/total_instructors.json b/erpnext/education/number_card/total_instructors/total_instructors.json new file mode 100644 index 0000000000..b8d3cc0fdf --- /dev/null +++ b/erpnext/education/number_card/total_instructors/total_instructors.json @@ -0,0 +1,23 @@ +{ + "aggregate_function_based_on": "", + "creation": "2020-07-23 14:19:38.423190", + "docstatus": 0, + "doctype": "Number Card", + "document_type": "Instructor", + "dynamic_filters_json": "[]", + "filters_json": "[[\"Instructor\",\"status\",\"=\",\"Active\",false]]", + "function": "Count", + "idx": 0, + "is_public": 1, + "is_standard": 1, + "label": "Total Instructors", + "modified": "2020-07-23 14:19:47.623306", + "modified_by": "Administrator", + "module": "Education", + "name": "Total Instructors", + "owner": "Administrator", + "report_function": "Sum", + "show_percentage_stats": 1, + "stats_time_interval": "Monthly", + "type": "Document Type" +} \ No newline at end of file diff --git a/erpnext/education/number_card/total_students/total_students.json b/erpnext/education/number_card/total_students/total_students.json new file mode 100644 index 0000000000..109c3d8ad9 --- /dev/null +++ b/erpnext/education/number_card/total_students/total_students.json @@ -0,0 +1,23 @@ +{ + "aggregate_function_based_on": "", + "creation": "2020-07-23 14:18:07.732298", + "docstatus": 0, + "doctype": "Number Card", + "document_type": "Student", + "dynamic_filters_json": "[]", + "filters_json": "[[\"Student\",\"enabled\",\"=\",1,false]]", + "function": "Count", + "idx": 0, + "is_public": 1, + "is_standard": 1, + "label": "Total Students", + "modified": "2020-07-23 14:18:40.603947", + "modified_by": "Administrator", + "module": "Education", + "name": "Total Students", + "owner": "Administrator", + "report_function": "Sum", + "show_percentage_stats": 1, + "stats_time_interval": "Monthly", + "type": "Document Type" +} \ No newline at end of file diff --git a/erpnext/education/onboarding_step/create_a_course/create_a_course.json b/erpnext/education/onboarding_step/create_a_course/create_a_course.json new file mode 100644 index 0000000000..02eee14056 --- /dev/null +++ b/erpnext/education/onboarding_step/create_a_course/create_a_course.json @@ -0,0 +1,19 @@ +{ + "action": "Create Entry", + "creation": "2020-07-27 19:09:04.493932", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-07-27 19:09:04.493932", + "modified_by": "Administrator", + "name": "Create a Course", + "owner": "Administrator", + "reference_document": "Course", + "show_full_form": 1, + "title": "Create a Course", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/education/onboarding_step/create_a_program/create_a_program.json b/erpnext/education/onboarding_step/create_a_program/create_a_program.json new file mode 100644 index 0000000000..61726304e0 --- /dev/null +++ b/erpnext/education/onboarding_step/create_a_program/create_a_program.json @@ -0,0 +1,19 @@ +{ + "action": "Create Entry", + "creation": "2020-07-27 19:09:35.451945", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-07-27 19:09:35.451945", + "modified_by": "Administrator", + "name": "Create a Program", + "owner": "Administrator", + "reference_document": "Program", + "show_full_form": 1, + "title": "Create a Program", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/education/onboarding_step/create_a_student/create_a_student.json b/erpnext/education/onboarding_step/create_a_student/create_a_student.json new file mode 100644 index 0000000000..07c3f7331e --- /dev/null +++ b/erpnext/education/onboarding_step/create_a_student/create_a_student.json @@ -0,0 +1,19 @@ +{ + "action": "Create Entry", + "creation": "2020-07-27 19:17:20.326837", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 1, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-07-27 19:49:47.724289", + "modified_by": "Administrator", + "name": "Create a Student", + "owner": "Administrator", + "reference_document": "Student", + "show_full_form": 1, + "title": "Create a Student", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/education/onboarding_step/create_a_topic/create_a_topic.json b/erpnext/education/onboarding_step/create_a_topic/create_a_topic.json new file mode 100644 index 0000000000..96a536488e --- /dev/null +++ b/erpnext/education/onboarding_step/create_a_topic/create_a_topic.json @@ -0,0 +1,19 @@ +{ + "action": "Create Entry", + "creation": "2020-07-27 19:08:40.754534", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-07-27 19:09:13.231995", + "modified_by": "Administrator", + "name": "Create a Topic", + "owner": "Administrator", + "reference_document": "Topic", + "show_full_form": 1, + "title": "Create a Topic", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/education/onboarding_step/create_an_instructor/create_an_instructor.json b/erpnext/education/onboarding_step/create_an_instructor/create_an_instructor.json new file mode 100644 index 0000000000..419d6e07f1 --- /dev/null +++ b/erpnext/education/onboarding_step/create_an_instructor/create_an_instructor.json @@ -0,0 +1,19 @@ +{ + "action": "Create Entry", + "creation": "2020-07-27 19:17:39.158037", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 1, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-07-27 19:49:47.723494", + "modified_by": "Administrator", + "name": "Create an Instructor", + "owner": "Administrator", + "reference_document": "Instructor", + "show_full_form": 1, + "title": "Create an Instructor", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/education/onboarding_step/enroll_a_student_in_a_program/enroll_a_student_in_a_program.json b/erpnext/education/onboarding_step/enroll_a_student_in_a_program/enroll_a_student_in_a_program.json new file mode 100644 index 0000000000..61e48cd520 --- /dev/null +++ b/erpnext/education/onboarding_step/enroll_a_student_in_a_program/enroll_a_student_in_a_program.json @@ -0,0 +1,19 @@ +{ + "action": "Create Entry", + "creation": "2020-07-27 19:10:28.530226", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-07-27 19:10:28.530226", + "modified_by": "Administrator", + "name": "Enroll a Student in a Program", + "owner": "Administrator", + "reference_document": "Program Enrollment", + "show_full_form": 0, + "title": "Enroll a Student in a Program", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/education/onboarding_step/introduction_to_program_and_courses/introduction_to_program_and_courses.json b/erpnext/education/onboarding_step/introduction_to_program_and_courses/introduction_to_program_and_courses.json new file mode 100644 index 0000000000..a9ddfc00da --- /dev/null +++ b/erpnext/education/onboarding_step/introduction_to_program_and_courses/introduction_to_program_and_courses.json @@ -0,0 +1,19 @@ +{ + "action": "Watch Video", + "creation": "2020-07-27 19:05:12.663987", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-07-27 20:18:11.831789", + "modified_by": "Administrator", + "name": "Introduction to Program and Courses", + "owner": "Administrator", + "show_full_form": 0, + "title": "Introduction to Program and Courses", + "validate_action": 1, + "video_url": "https://www.youtube.com/watch?v=1ueE4seFTp8" +} \ No newline at end of file diff --git a/erpnext/education/onboarding_step/introduction_to_student_attendance/introduction_to_student_attendance.json b/erpnext/education/onboarding_step/introduction_to_student_attendance/introduction_to_student_attendance.json new file mode 100644 index 0000000000..3de99728ea --- /dev/null +++ b/erpnext/education/onboarding_step/introduction_to_student_attendance/introduction_to_student_attendance.json @@ -0,0 +1,19 @@ +{ + "action": "Watch Video", + "creation": "2020-07-27 19:14:57.176131", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-07-27 19:55:55.411032", + "modified_by": "Administrator", + "name": "Introduction to Student Attendance", + "owner": "Administrator", + "show_full_form": 0, + "title": "Introduction to Student Attendance", + "validate_action": 1, + "video_url": "https://youtu.be/j9pgkPuyiaI" +} \ No newline at end of file diff --git a/erpnext/education/onboarding_step/introduction_to_student_group/introduction_to_student_group.json b/erpnext/education/onboarding_step/introduction_to_student_group/introduction_to_student_group.json new file mode 100644 index 0000000000..74bdcd17be --- /dev/null +++ b/erpnext/education/onboarding_step/introduction_to_student_group/introduction_to_student_group.json @@ -0,0 +1,20 @@ +{ + "action": "Watch Video", + "creation": "2020-07-27 19:12:05.046465", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_mandatory": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2020-07-27 19:42:47.286441", + "modified_by": "Administrator", + "name": "Introduction to Student Group", + "owner": "Administrator", + "reference_document": "Student Group", + "show_full_form": 0, + "title": "Introduction to Student Group", + "validate_action": 1, + "video_url": "https://youtu.be/5K_smeeE1Q4" +} \ No newline at end of file diff --git a/erpnext/education/report/program_wise_fee_collection/__init__.py b/erpnext/education/report/program_wise_fee_collection/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/education/report/program_wise_fee_collection/program_wise_fee_collection.js b/erpnext/education/report/program_wise_fee_collection/program_wise_fee_collection.js new file mode 100644 index 0000000000..72e8f12e9d --- /dev/null +++ b/erpnext/education/report/program_wise_fee_collection/program_wise_fee_collection.js @@ -0,0 +1,22 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Program wise Fee Collection"] = { + "filters": [ + { + "fieldname": "from_date", + "label": __("From Date"), + "fieldtype": "Date", + "default": frappe.datetime.add_months(frappe.datetime.get_today(), -1), + "reqd": 1 + }, + { + "fieldname": "to_date", + "label": __("To Date"), + "fieldtype": "Date", + "default": frappe.datetime.get_today(), + "reqd": 1 + } + ] +}; diff --git a/erpnext/education/report/program_wise_fee_collection/program_wise_fee_collection.json b/erpnext/education/report/program_wise_fee_collection/program_wise_fee_collection.json new file mode 100644 index 0000000000..ee5c0dec79 --- /dev/null +++ b/erpnext/education/report/program_wise_fee_collection/program_wise_fee_collection.json @@ -0,0 +1,32 @@ +{ + "add_total_row": 1, + "creation": "2020-07-27 16:05:33.263539", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 0, + "is_standard": "Yes", + "json": "{}", + "modified": "2020-08-05 14:14:12.410515", + "modified_by": "Administrator", + "module": "Education", + "name": "Program wise Fee Collection", + "owner": "Administrator", + "prepared_report": 0, + "query": "SELECT \n FeesCollected.program AS \"Program:Link/Program:200\",\n FeesCollected.paid_amount AS \"Fees Collected:Currency:150\",\n FeesCollected.outstanding_amount AS \"Outstanding Amount:Currency:150\",\n FeesCollected.grand_total \"Grand Total:Currency:150\"\nFROM (\n SELECT \n sum(grand_total) - sum(outstanding_amount) AS paid_amount, program,\n sum(outstanding_amount) AS outstanding_amount,\n sum(grand_total) AS grand_total\n FROM `tabFees`\n WHERE docstatus = 1\n GROUP BY program\n) AS FeesCollected\nORDER BY FeesCollected.paid_amount DESC", + "ref_doctype": "Fees", + "report_name": "Program wise Fee Collection", + "report_type": "Script Report", + "roles": [ + { + "role": "Academics User" + }, + { + "role": "Accounts User" + }, + { + "role": "Accounts Manager" + } + ] +} \ No newline at end of file diff --git a/erpnext/education/report/program_wise_fee_collection/program_wise_fee_collection.py b/erpnext/education/report/program_wise_fee_collection/program_wise_fee_collection.py new file mode 100644 index 0000000000..c145359129 --- /dev/null +++ b/erpnext/education/report/program_wise_fee_collection/program_wise_fee_collection.py @@ -0,0 +1,124 @@ +# 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): + if not filters: + filters = {} + + columns = get_columns(filters) + data = get_data(filters) + chart = get_chart_data(data) + + return columns, data, None, chart + +def get_columns(filters=None): + return [ + { + 'label': _('Program'), + 'fieldname': 'program', + 'fieldtype': 'Link', + 'options': 'Program', + 'width': 300 + }, + { + 'label': _('Fees Collected'), + 'fieldname': 'fees_collected', + 'fieldtype': 'Currency', + 'width': 200 + }, + { + 'label': _('Outstanding Amount'), + 'fieldname': 'outstanding_amount', + 'fieldtype': 'Currency', + 'width': 200 + }, + { + 'label': _('Grand Total'), + 'fieldname': 'grand_total', + 'fieldtype': 'Currency', + 'width': 200 + } + ] + + +def get_data(filters=None): + data = [] + + conditions = get_filter_conditions(filters) + + fee_details = frappe.db.sql( + """ + SELECT + FeesCollected.program, + FeesCollected.paid_amount, + FeesCollected.outstanding_amount, + FeesCollected.grand_total + FROM ( + SELECT + sum(grand_total) - sum(outstanding_amount) AS paid_amount, program, + sum(outstanding_amount) AS outstanding_amount, + sum(grand_total) AS grand_total + FROM `tabFees` + WHERE + docstatus = 1 and + program IS NOT NULL + %s + GROUP BY program + ) AS FeesCollected + ORDER BY FeesCollected.paid_amount DESC + """ % (conditions) + , as_dict=1) + + for entry in fee_details: + data.append({ + 'program': entry.program, + 'fees_collected': entry.paid_amount, + 'outstanding_amount': entry.outstanding_amount, + 'grand_total': entry.grand_total + }) + + return data + +def get_filter_conditions(filters): + conditions = '' + + if filters.get('from_date') and filters.get('to_date'): + conditions += " and posting_date BETWEEN '%s' and '%s'" % (filters.get('from_date'), filters.get('to_date')) + + return conditions + + +def get_chart_data(data): + if not data: + return + + labels = [] + fees_collected = [] + outstanding_amount = [] + + for entry in data: + labels.append(entry.get('program')) + fees_collected.append(entry.get('fees_collected')) + outstanding_amount.append(entry.get('outstanding_amount')) + + return { + 'data': { + 'labels': labels, + 'datasets': [ + { + 'name': _('Fees Collected'), + 'values': fees_collected + }, + { + 'name': _('Outstanding Amt'), + 'values': outstanding_amount + } + ] + }, + 'type': 'bar' + } + 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/erpnext_integrations/taxjar_integration.py b/erpnext/erpnext_integrations/taxjar_integration.py index 633692dd24..24fc3d44b9 100644 --- a/erpnext/erpnext_integrations/taxjar_integration.py +++ b/erpnext/erpnext_integrations/taxjar_integration.py @@ -1,8 +1,5 @@ import traceback -import pycountry -import taxjar - import frappe from erpnext import get_default_company from frappe import _ @@ -32,6 +29,7 @@ def get_client(): def create_transaction(doc, method): + import taxjar """Create an order transaction in TaxJar""" if not TAXJAR_CREATE_TRANSACTIONS: @@ -208,6 +206,7 @@ def get_shipping_address_details(doc): def get_iso_3166_2_state_code(address): + import pycountry country_code = frappe.db.get_value("Country", address.get("country"), "code") error_message = _("""{0} is not a valid state! Check for typos or enter the ISO code for your state.""").format(address.get("state")) 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/healthcare_practitioner/healthcare_practitioner.py b/erpnext/healthcare/doctype/healthcare_practitioner/healthcare_practitioner.py index 3dc7c1ec39..5da5a0657c 100644 --- a/erpnext/healthcare/doctype/healthcare_practitioner/healthcare_practitioner.py +++ b/erpnext/healthcare/doctype/healthcare_practitioner/healthcare_practitioner.py @@ -71,6 +71,7 @@ def validate_service_item(item, msg): frappe.throw(_(msg)) @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def get_practitioner_list(doctype, txt, searchfield, start, page_len, filters=None): fields = ['name', 'practitioner_name', 'mobile_phone'] diff --git a/erpnext/healthcare/doctype/inpatient_record/inpatient_record.js b/erpnext/healthcare/doctype/inpatient_record/inpatient_record.js index 971e166067..60f0f9d56d 100644 --- a/erpnext/healthcare/doctype/inpatient_record/inpatient_record.js +++ b/erpnext/healthcare/doctype/inpatient_record/inpatient_record.js @@ -134,7 +134,7 @@ let transfer_patient_dialog = function(frm) { {fieldtype: 'Link', label: 'Leave From', fieldname: 'leave_from', options: 'Healthcare Service Unit', reqd: 1, read_only:1}, {fieldtype: 'Link', label: 'Service Unit Type', fieldname: 'service_unit_type', options: 'Healthcare Service Unit Type'}, {fieldtype: 'Link', label: 'Transfer To', fieldname: 'service_unit', options: 'Healthcare Service Unit', reqd: 1}, - {fieldtype: 'Datetime', label: 'Check In', fieldname: 'check_in', reqd: 1} + {fieldtype: 'Datetime', label: 'Check In', fieldname: 'check_in', reqd: 1, default: frappe.datetime.now_datetime()} ], primary_action_label: __('Transfer'), primary_action : function() { @@ -147,7 +147,12 @@ let transfer_patient_dialog = function(frm) { if(dialog.get_value('service_unit')){ service_unit = dialog.get_value('service_unit'); } - if(!check_in){ + if(check_in > frappe.datetime.now_datetime()){ + frappe.msgprint({ + title: __('Not Allowed'), + message: __('Check-in time cannot be greater than the current time'), + indicator: 'red' + }); return; } frappe.call({ diff --git a/erpnext/healthcare/doctype/inpatient_record/inpatient_record.py b/erpnext/healthcare/doctype/inpatient_record/inpatient_record.py index 69356baad5..bc76970601 100644 --- a/erpnext/healthcare/doctype/inpatient_record/inpatient_record.py +++ b/erpnext/healthcare/doctype/inpatient_record/inpatient_record.py @@ -222,6 +222,7 @@ def patient_leave_service_unit(inpatient_record, check_out, leave_from): inpatient_record.save(ignore_permissions = True) @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def get_leave_from(doctype, txt, searchfield, start, page_len, filters): docname = filters['docname'] 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/hooks.py b/erpnext/hooks.py index 95a836fe65..463ad6c94b 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -322,7 +322,8 @@ scheduler_events = { "erpnext.crm.doctype.email_campaign.email_campaign.set_email_campaign_status", "erpnext.selling.doctype.quotation.quotation.set_expired_status", "erpnext.healthcare.doctype.patient_appointment.patient_appointment.update_appointment_status", - "erpnext.buying.doctype.supplier_quotation.supplier_quotation.set_expired_status" + "erpnext.buying.doctype.supplier_quotation.supplier_quotation.set_expired_status", + "erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts.send_auto_email" ], "daily_long": [ "erpnext.setup.doctype.email_digest.email_digest.send", diff --git a/erpnext/hr/dashboard_chart/job_application_status/job_application_status.json b/erpnext/hr/dashboard_chart/job_application_status/job_application_status.json index bfcfa96752..42a830970e 100644 --- a/erpnext/hr/dashboard_chart/job_application_status/job_application_status.json +++ b/erpnext/hr/dashboard_chart/job_application_status/job_application_status.json @@ -7,14 +7,14 @@ "doctype": "Dashboard Chart", "document_type": "Job Applicant", "dynamic_filters_json": "", - "filters_json": "[[\"Job Applicant\",\"creation\",\"Previous\",\"1 month\"]]", + "filters_json": "[[\"Job Applicant\",\"creation\",\"Timespan\",\"last month\",false]]", "group_by_based_on": "status", "group_by_type": "Count", "idx": 0, "is_public": 1, "is_standard": 1, - "last_synced_on": "2020-07-22 14:27:40.118498", - "modified": "2020-07-22 14:33:00.404144", + "last_synced_on": "2020-07-28 16:19:12.109979", + "modified": "2020-07-28 16:19:45.279490", "modified_by": "Administrator", "module": "HR", "name": "Job Application Status", diff --git a/erpnext/hr/desk_page/hr/hr.json b/erpnext/hr/desk_page/hr/hr.json index 0fed8d322f..895cf7290c 100644 --- a/erpnext/hr/desk_page/hr/hr.json +++ b/erpnext/hr/desk_page/hr/hr.json @@ -78,7 +78,7 @@ "idx": 0, "is_standard": 1, "label": "HR", - "modified": "2020-06-16 19:20:50.976045", + "modified": "2020-08-11 17:04:38.655417", "modified_by": "Administrator", "module": "HR", "name": "HR", @@ -88,7 +88,7 @@ "pin_to_top": 0, "shortcuts": [ { - "color": "#9deca2", + "color": "#cef6d1", "format": "{} Active", "label": "Employee", "link_to": "Employee", @@ -96,18 +96,19 @@ "type": "DocType" }, { - "label": "Attendance", - "link_to": "Attendance", - "stats_filter": "", - "type": "DocType" - }, - { + "color": "#ffe8cd", "format": "{} Open", "label": "Leave Application", "link_to": "Leave Application", "stats_filter": "{\"status\":\"Open\"}", "type": "DocType" }, + { + "label": "Attendance", + "link_to": "Attendance", + "stats_filter": "", + "type": "DocType" + }, { "label": "Job Applicant", "link_to": "Job Applicant", 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/department/department.json b/erpnext/hr/doctype/department/department.json index a54c1d18e7..dcb6a742b7 100644 --- a/erpnext/hr/doctype/department/department.json +++ b/erpnext/hr/doctype/department/department.json @@ -17,10 +17,10 @@ "payroll_cost_center", "column_break_9", "leave_block_list", - "leave_section", + "approvers", "leave_approvers", - "expense_section", "expense_approvers", + "shift_request_approver", "lft", "rgt", "old_parent" @@ -33,14 +33,18 @@ "label": "Department", "oldfieldname": "department_name", "oldfieldtype": "Data", - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "parent_department", "fieldtype": "Link", "in_list_view": 1, "label": "Parent Department", - "options": "Department" + "options": "Department", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "company", @@ -48,7 +52,9 @@ "in_standard_filter": 1, "label": "Company", "options": "Company", - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "bold": 1, @@ -56,17 +62,23 @@ "fieldname": "is_group", "fieldtype": "Check", "in_list_view": 1, - "label": "Is Group" + "label": "Is Group", + "show_days": 1, + "show_seconds": 1 }, { "default": "0", "fieldname": "disabled", "fieldtype": "Check", - "label": "Disabled" + "label": "Disabled", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "section_break_4", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "show_days": 1, + "show_seconds": 1 }, { "description": "Days for which Holidays are blocked for this department.", @@ -74,31 +86,25 @@ "fieldtype": "Link", "in_list_view": 1, "label": "Leave Block List", - "options": "Leave Block List" + "options": "Leave Block List", + "show_days": 1, + "show_seconds": 1 }, { - "fieldname": "leave_section", - "fieldtype": "Section Break", - "label": "Leave Approvers" - }, - { - "description": "The first Leave Approver in the list will be set as the default Leave Approver.", "fieldname": "leave_approvers", "fieldtype": "Table", "label": "Leave Approver", - "options": "Department Approver" + "options": "Department Approver", + "show_days": 1, + "show_seconds": 1 }, { - "fieldname": "expense_section", - "fieldtype": "Section Break", - "label": "Expense Approvers" - }, - { - "description": "The first Expense Approver in the list will be set as the default Expense Approver.", "fieldname": "expense_approvers", "fieldtype": "Table", "label": "Expense Approver", - "options": "Department Approver" + "options": "Department Approver", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "lft", @@ -106,7 +112,9 @@ "hidden": 1, "label": "lft", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "rgt", @@ -114,7 +122,9 @@ "hidden": 1, "label": "rgt", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "old_parent", @@ -122,28 +132,52 @@ "hidden": 1, "ignore_user_permissions": 1, "label": "Old Parent", - "print_hide": 1 + "print_hide": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_3", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "payroll_cost_center", "fieldtype": "Link", "label": "Payroll Cost Center", - "options": "Cost Center" + "options": "Cost Center", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_9", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 + }, + { + "description": "The first Approver in the list will be set as the default Approver.", + "fieldname": "approvers", + "fieldtype": "Section Break", + "label": "Approvers", + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "shift_request_approver", + "fieldtype": "Table", + "label": "Shift Request Approver", + "options": "Department Approver", + "show_days": 1, + "show_seconds": 1 } ], "icon": "fa fa-sitemap", "idx": 1, "is_tree": 1, "links": [], - "modified": "2020-05-05 18:49:28.503931", + "modified": "2020-06-23 15:42:00.563272", "modified_by": "Administrator", "module": "HR", "name": "Department", diff --git a/erpnext/hr/doctype/department_approver/department_approver.py b/erpnext/hr/doctype/department_approver/department_approver.py index d4c118f802..9b2de0e1cb 100644 --- a/erpnext/hr/doctype/department_approver/department_approver.py +++ b/erpnext/hr/doctype/department_approver/department_approver.py @@ -11,15 +11,16 @@ class DepartmentApprover(Document): pass @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def get_approvers(doctype, txt, searchfield, start, page_len, filters): if not filters.get("employee"): - frappe.throw(_("Please select Employee Record first.")) + frappe.throw(_("Please select Employee first.")) approvers = [] department_details = {} department_list = [] - employee = frappe.get_value("Employee", filters.get("employee"), ["department", "leave_approver", "expense_approver"], as_dict=True) + employee = frappe.get_value("Employee", filters.get("employee"), ["department", "leave_approver", "expense_approver", "shift_request_approver"], as_dict=True) employee_department = filters.get("department") or employee.department if employee_department: @@ -36,13 +37,18 @@ def get_approvers(doctype, txt, searchfield, start, page_len, filters): if filters.get("doctype") == "Expense Claim" and employee.expense_approver: approvers.append(frappe.db.get_value("User", employee.expense_approver, ['name', 'first_name', 'last_name'])) + if filters.get("doctype") == "Shift Request" and employee.shift_request_approver: + approvers.append(frappe.db.get_value("User", employee.shift_request_approver, ['name', 'first_name', 'last_name'])) if filters.get("doctype") == "Leave Application": parentfield = "leave_approvers" field_name = "Leave Approver" - else: + elif filters.get("doctype") == "Expense Claim": parentfield = "expense_approvers" field_name = "Expense Approver" + elif filters.get("doctype") == "Shift Request": + parentfield = "shift_request_approver" + field_name = "Shift Request Approver" if department_list: for d in department_list: approvers += frappe.db.sql("""select user.name, user.first_name, user.last_name from diff --git a/erpnext/hr/doctype/employee/employee.json b/erpnext/hr/doctype/employee/employee.json index f2afe065d1..8c02e4f1d6 100644 --- a/erpnext/hr/doctype/employee/employee.json +++ b/erpnext/hr/doctype/employee/employee.json @@ -51,10 +51,14 @@ "column_break_31", "grade", "branch", + "approvers_section", + "expense_approver", + "leave_approver", + "column_break_45", + "shift_request_approver", "attendance_and_leave_details", "leave_policy", "attendance_device_id", - "leave_approver", "column_break_44", "holiday_list", "default_shift", @@ -62,7 +66,6 @@ "salary_mode", "payroll_cost_center", "column_break_52", - "expense_approver", "bank_name", "bank_ac_no", "health_insurance_section", @@ -806,14 +809,37 @@ "fieldname": "expense_approver", "fieldtype": "Link", "label": "Expense Approver", - "options": "User" + "options": "User", + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "approvers_section", + "fieldtype": "Section Break", + "label": "Approvers", + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "column_break_45", + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "shift_request_approver", + "fieldtype": "Link", + "label": "Shift Request Approver", + "options": "User", + "show_days": 1, + "show_seconds": 1 } ], "icon": "fa fa-user", "idx": 24, "image_field": "image", "links": [], - "modified": "2020-07-03 21:28:04.109189", + "modified": "2020-07-28 01:36:04.109189", "modified_by": "Administrator", "module": "HR", "name": "Employee", diff --git a/erpnext/hr/doctype/expense_claim/expense_claim_list.js b/erpnext/hr/doctype/expense_claim/expense_claim_list.js index 6195ad414a..9bafc18562 100644 --- a/erpnext/hr/doctype/expense_claim/expense_claim_list.js +++ b/erpnext/hr/doctype/expense_claim/expense_claim_list.js @@ -1,5 +1,5 @@ frappe.listview_settings['Expense Claim'] = { - add_fields: ["total_claimed_amount", "docstatus"], + add_fields: ["total_claimed_amount", "docstatus", "company"], get_indicator: function(doc) { if(doc.status == "Paid") { return [__("Paid"), "green", "status,=,Paid"]; diff --git a/erpnext/hr/doctype/job_offer/job_offer.py b/erpnext/hr/doctype/job_offer/job_offer.py index e7e1a37480..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,12 @@ 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 staffing_plan.vacancies - len(job_offers) <= 0: - frappe.throw(_("There are no vacancies under staffing plan {0}").format(frappe.bold(get_link_to_form("Staffing Plan", staffing_plan.parent)))) + 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)) + + frappe.throw(_("There are no vacancies under staffing plan {0}").format(error_variable)) def on_change(self): update_job_applicant(self.status, self.job_applicant) @@ -60,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_allocation/leave_allocation.js b/erpnext/hr/doctype/leave_allocation/leave_allocation.js index 210a73cfe5..e9e129cdd2 100755 --- a/erpnext/hr/doctype/leave_allocation/leave_allocation.js +++ b/erpnext/hr/doctype/leave_allocation/leave_allocation.js @@ -5,20 +5,23 @@ cur_frm.add_fetch('employee','employee_name','employee_name'); frappe.ui.form.on("Leave Allocation", { onload: function(frm) { + // Ignore cancellation of doctype on cancel all. + frm.ignore_doctypes_on_cancel_all = ["Leave Ledger Entry"]; + if(!frm.doc.from_date) frm.set_value("from_date", frappe.datetime.get_today()); frm.set_query("employee", function() { return { query: "erpnext.controllers.queries.employee_query" - } + }; }); frm.set_query("leave_type", function() { return { filters: { is_lwp: 0 } - } - }) + }; + }); }, refresh: function(frm) { diff --git a/erpnext/hr/doctype/leave_application/leave_application.js b/erpnext/hr/doctype/leave_application/leave_application.js index 4001a45507..d62e418b17 100755 --- a/erpnext/hr/doctype/leave_application/leave_application.js +++ b/erpnext/hr/doctype/leave_application/leave_application.js @@ -19,6 +19,10 @@ frappe.ui.form.on("Leave Application", { frm.set_query("employee", erpnext.queries.employee); }, onload: function(frm) { + + // Ignore cancellation of doctype on cancel all. + frm.ignore_doctypes_on_cancel_all = ["Leave Ledger Entry"]; + if (!frm.doc.posting_date) { frm.set_value("posting_date", frappe.datetime.get_today()); } diff --git a/erpnext/hr/doctype/leave_encashment/leave_encashment.js b/erpnext/hr/doctype/leave_encashment/leave_encashment.js index 701c2f0f31..71a34226da 100644 --- a/erpnext/hr/doctype/leave_encashment/leave_encashment.js +++ b/erpnext/hr/doctype/leave_encashment/leave_encashment.js @@ -2,6 +2,10 @@ // For license information, please see license.txt frappe.ui.form.on('Leave Encashment', { + onload: function(frm) { + // Ignore cancellation of doctype on cancel all. + frm.ignore_doctypes_on_cancel_all = ["Leave Ledger Entry"]; + }, setup: function(frm) { frm.set_query("leave_type", function() { return { @@ -33,7 +37,7 @@ frappe.ui.form.on('Leave Encashment', { doc: frm.doc, callback: function(r) { frm.refresh_fields(); - } + } }); } } 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.json b/erpnext/hr/doctype/shift_assignment/shift_assignment.json index 72cbba8a0d..ce2a10f229 100644 --- a/erpnext/hr/doctype/shift_assignment/shift_assignment.json +++ b/erpnext/hr/doctype/shift_assignment/shift_assignment.json @@ -10,9 +10,11 @@ "employee", "employee_name", "shift_type", + "status", "column_break_3", "company", - "date", + "start_date", + "end_date", "shift_request", "department", "amended_from" @@ -59,12 +61,6 @@ "options": "Company", "reqd": 1 }, - { - "fieldname": "date", - "fieldtype": "Date", - "in_list_view": 1, - "label": "Date" - }, { "fieldname": "shift_request", "fieldtype": "Link", @@ -80,11 +76,36 @@ "options": "Shift Assignment", "print_hide": 1, "read_only": 1 + }, + { + "fieldname": "start_date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Start Date", + "reqd": 1 + }, + { + "allow_on_submit": 1, + "fieldname": "end_date", + "fieldtype": "Date", + "label": "End Date", + "show_days": 1, + "show_seconds": 1 + }, + { + "allow_on_submit": 1, + "default": "Active", + "fieldname": "status", + "fieldtype": "Select", + "label": "Status", + "options": "Active\nInactive", + "show_days": 1, + "show_seconds": 1 } ], "is_submittable": 1, "links": [], - "modified": "2019-12-12 15:49:06.956901", + "modified": "2020-06-15 14:27:54.310773", "modified_by": "Administrator", "module": "HR", "name": "Shift Assignment", diff --git a/erpnext/hr/doctype/shift_assignment/shift_assignment.py b/erpnext/hr/doctype/shift_assignment/shift_assignment.py index 40c78cdf07..2c385e80f4 100644 --- a/erpnext/hr/doctype/shift_assignment/shift_assignment.py +++ b/erpnext/hr/doctype/shift_assignment/shift_assignment.py @@ -11,38 +11,63 @@ from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday from datetime import timedelta, datetime -class OverlapError(frappe.ValidationError): pass - class ShiftAssignment(Document): def validate(self): self.validate_overlapping_dates() + if self.end_date and self.end_date <= self.start_date: + frappe.throw(_("End Date must not be lesser than Start Date")) + def validate_overlapping_dates(self): - if not self.name: - self.name = "New Shift Assignment" + if not self.name: + self.name = "New Shift Assignment" - d = frappe.db.sql(""" - select - name, shift_type, date - from `tabShift Assignment` - where employee = %(employee)s and docstatus < 2 - and date = %(date)s - and name != %(name)s""", { - "employee": self.employee, - "shift_type": self.shift_type, - "date": self.date, - "name": self.name - }, as_dict = 1) + condition = """and ( + end_date is null + or + %(start_date)s between start_date and end_date + """ - for date_overlap in d: - if date_overlap['name']: - self.throw_overlap_error(date_overlap) + if self.end_date: + condition += """ or + %(end_date)s between start_date and end_date + or + start_date between %(start_date)s and %(end_date)s + ) """ + else: + condition += """ ) """ - def throw_overlap_error(self, d): - msg = _("Employee {0} has already applied for {1} on {2} : ").format(self.employee, - d['shift_type'], formatdate(d['date'])) \ - + """ {0}""".format(d["name"]) - frappe.throw(msg, OverlapError) + assigned_shifts = frappe.db.sql(""" + select name, shift_type, start_date ,end_date, docstatus, status + from `tabShift Assignment` + where + employee=%(employee)s and docstatus = 1 + and name != %(name)s + and status = "Active" + {0} + """.format(condition), { + "employee": self.employee, + "shift_type": self.shift_type, + "start_date": self.start_date, + "end_date": self.end_date, + "name": self.name + }, as_dict = 1) + + if len(assigned_shifts): + self.throw_overlap_error(assigned_shifts[0]) + + def throw_overlap_error(self, shift_details): + shift_details = frappe._dict(shift_details) + if shift_details.docstatus == 1 and shift_details.status == "Active": + msg = _("Employee {0} already has Active Shift {1}: {2}").format(frappe.bold(self.employee), frappe.bold(self.shift_type), frappe.bold(shift_details.name)) + if shift_details.start_date: + msg += _(" from {0}").format(getdate(self.start_date).strftime("%d-%m-%Y")) + title = "Ongoing Shift" + if shift_details.end_date: + msg += _(" to {0}").format(getdate(self.end_date).strftime("%d-%m-%Y")) + title = "Active Shift" + if msg: + frappe.throw(msg, title=title) @frappe.whitelist() def get_events(start, end, filters=None): @@ -62,20 +87,23 @@ def get_events(start, end, filters=None): return events def add_assignments(events, start, end, conditions=None): - query = """select name, date, employee_name, + query = """select name, start_date, end_date, employee_name, employee, docstatus from `tabShift Assignment` where - date <= %(date)s - and docstatus < 2""" + start_date >= %(start_date)s + or end_date <= %(end_date)s + or (%(start_date)s between start_date and end_date and %(end_date)s between start_date and end_date) + and docstatus = 1""" if conditions: query += conditions - for d in frappe.db.sql(query, {"date":start, "date":end}, as_dict=True): + for d in frappe.db.sql(query, {"start_date":start, "end_date":end}, as_dict=True): e = { "name": d.name, "doctype": "Shift Assignment", - "date": d.date, - "title": cstr(d.employee_name) + \ + "start_date": d.start_date, + "end_date": d.end_date if d.end_date else nowdate(), + "title": cstr(d.employee_name) + ": "+ \ cstr(d.shift_type), "docstatus": d.docstatus } @@ -92,7 +120,16 @@ def get_employee_shift(employee, for_date=nowdate(), consider_default_shift=Fals :param next_shift_direction: One of: None, 'forward', 'reverse'. Direction to look for next shift if shift not found on given date. """ default_shift = frappe.db.get_value('Employee', employee, 'default_shift') - shift_type_name = frappe.db.get_value('Shift Assignment', {'employee':employee, 'date': for_date, 'docstatus': '1'}, 'shift_type') + shift_type_name = None + shift_assignment_details = frappe.db.get_value('Shift Assignment', {'employee':employee, 'start_date':('<=', for_date), 'docstatus': '1', 'status': "Active"}, ['shift_type', 'end_date']) + + if shift_assignment_details: + shift_type_name = shift_assignment_details[0] + + # if end_date present means that shift is over after end_date else it is a ongoing shift. + if shift_assignment_details[1] and for_date >= shift_assignment_details[1] : + shift_type_name = None + if not shift_type_name and consider_default_shift: shift_type_name = default_shift if shift_type_name: @@ -117,16 +154,20 @@ def get_employee_shift(employee, for_date=nowdate(), consider_default_shift=Fals direction = '<' if next_shift_direction == 'reverse' else '>' sort_order = 'desc' if next_shift_direction == 'reverse' else 'asc' dates = frappe.db.get_all('Shift Assignment', - 'date', - {'employee':employee, 'date':(direction, for_date), 'docstatus': '1'}, + ['start_date', 'end_date'], + {'employee':employee, 'start_date':(direction, for_date), 'docstatus': '1', "status": "Active"}, as_list=True, - limit=MAX_DAYS, order_by="date "+sort_order) - for date in dates: - shift_details = get_employee_shift(employee, date[0], consider_default_shift, None) - if shift_details: - shift_type_name = shift_details.shift_type.name - for_date = date[0] - break + limit=MAX_DAYS, order_by="start_date "+sort_order) + + if dates: + for date in dates: + if date[1] and date[1] < for_date: + continue + shift_details = get_employee_shift(employee, date[0], consider_default_shift, None) + if shift_details: + shift_type_name = shift_details.shift_type.name + for_date = date[0] + break return get_shift_details(shift_type_name, for_date) @@ -134,7 +175,7 @@ def get_employee_shift(employee, for_date=nowdate(), consider_default_shift=Fals def get_employee_shift_timings(employee, for_timestamp=now_datetime(), consider_default_shift=False): """Returns previous shift, current/upcoming shift, next_shift for the given timestamp and employee """ - # write and verify a test case for midnight shift. + # write and verify a test case for midnight shift. prev_shift = curr_shift = next_shift = None curr_shift = get_employee_shift(employee, for_timestamp.date(), consider_default_shift, 'forward') if curr_shift: diff --git a/erpnext/hr/doctype/shift_assignment/shift_assignment_calendar.js b/erpnext/hr/doctype/shift_assignment/shift_assignment_calendar.js index c2c9bc073a..17a986deb2 100644 --- a/erpnext/hr/doctype/shift_assignment/shift_assignment_calendar.js +++ b/erpnext/hr/doctype/shift_assignment/shift_assignment_calendar.js @@ -3,8 +3,8 @@ frappe.views.calendar["Shift Assignment"] = { field_map: { - "start": "date", - "end": "date", + "start": "start_date", + "end": "end_date", "id": "name", "docstatus": 1 }, diff --git a/erpnext/hr/doctype/shift_assignment/test_shift_assignment.py b/erpnext/hr/doctype/shift_assignment/test_shift_assignment.py index 7fe80a236c..4c3c1ed579 100644 --- a/erpnext/hr/doctype/shift_assignment/test_shift_assignment.py +++ b/erpnext/hr/doctype/shift_assignment/test_shift_assignment.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import frappe import unittest -from frappe.utils import nowdate +from frappe.utils import nowdate, add_days test_dependencies = ["Shift Type"] @@ -20,8 +20,61 @@ class TestShiftAssignment(unittest.TestCase): "shift_type": "Day Shift", "company": "_Test Company", "employee": "_T-Employee-00001", - "date": nowdate() + "start_date": nowdate() }).insert() shift_assignment.submit() self.assertEqual(shift_assignment.docstatus, 1) + + def test_overlapping_for_ongoing_shift(self): + # shift should be Ongoing if Only start_date is present and status = Active + + shift_assignment_1 = frappe.get_doc({ + "doctype": "Shift Assignment", + "shift_type": "Day Shift", + "company": "_Test Company", + "employee": "_T-Employee-00001", + "start_date": nowdate(), + "status": 'Active' + }).insert() + shift_assignment_1.submit() + + self.assertEqual(shift_assignment_1.docstatus, 1) + + shift_assignment = frappe.get_doc({ + "doctype": "Shift Assignment", + "shift_type": "Day Shift", + "company": "_Test Company", + "employee": "_T-Employee-00001", + "start_date": add_days(nowdate(), 2) + }) + + self.assertRaises(frappe.ValidationError, shift_assignment.save) + + def test_overlapping_for_fixed_period_shift(self): + # shift should is for Fixed period if Only start_date and end_date both are present and status = Active + + shift_assignment_1 = frappe.get_doc({ + "doctype": "Shift Assignment", + "shift_type": "Day Shift", + "company": "_Test Company", + "employee": "_T-Employee-00001", + "start_date": nowdate(), + "end_date": add_days(nowdate(), 30), + "status": 'Active' + }).insert() + shift_assignment_1.submit() + + + # it should not allowed within period of any shift. + shift_assignment_3 = frappe.get_doc({ + "doctype": "Shift Assignment", + "shift_type": "Day Shift", + "company": "_Test Company", + "employee": "_T-Employee-00001", + "start_date":add_days(nowdate(), 10), + "end_date": add_days(nowdate(), 35), + "status": 'Active' + }) + + self.assertRaises(frappe.ValidationError, shift_assignment_3.save) \ No newline at end of file diff --git a/erpnext/hr/doctype/shift_request/shift_request.js b/erpnext/hr/doctype/shift_request/shift_request.js index 1db7c7d10e..b17a6f3845 100644 --- a/erpnext/hr/doctype/shift_request/shift_request.js +++ b/erpnext/hr/doctype/shift_request/shift_request.js @@ -2,7 +2,16 @@ // For license information, please see license.txt frappe.ui.form.on('Shift Request', { - refresh: function(frm) { - - } + setup: function(frm) { + frm.set_query("approver", function() { + return { + query: "erpnext.hr.doctype.department_approver.department_approver.get_approvers", + filters: { + employee: frm.doc.employee, + doctype: frm.doc.doctype + } + }; + }); + frm.set_query("employee", erpnext.queries.employee); + }, }); diff --git a/erpnext/hr/doctype/shift_request/shift_request.json b/erpnext/hr/doctype/shift_request/shift_request.json index dd056470cd..64cbdfff7d 100644 --- a/erpnext/hr/doctype/shift_request/shift_request.json +++ b/erpnext/hr/doctype/shift_request/shift_request.json @@ -1,396 +1,155 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 1, - "allow_rename": 0, - "autoname": "HR-SHR-.YY.-.MM.-.#####", - "beta": 0, - "creation": "2018-04-13 16:32:27.974273", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "allow_import": 1, + "autoname": "HR-SHR-.YY.-.MM.-.#####", + "creation": "2018-04-13 16:32:27.974273", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "shift_type", + "employee", + "employee_name", + "department", + "status", + "column_break_4", + "company", + "approver", + "from_date", + "to_date", + "amended_from" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "shift_type", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Shift Type", - "length": 0, - "no_copy": 0, - "options": "Shift Type", - "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": "shift_type", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Shift Type", + "options": "Shift Type", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "employee", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Employee", - "length": 0, - "no_copy": 0, - "options": "Employee", - "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": "employee", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Employee", + "options": "Employee", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_from": "employee.employee_name", - "fieldname": "employee_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": "Employee Name", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fetch_from": "employee.employee_name", + "fieldname": "employee_name", + "fieldtype": "Data", + "label": "Employee Name", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_from": "employee.department", - "fieldname": "department", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Department", - "length": 0, - "no_copy": 0, - "options": "Department", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fetch_from": "employee.department", + "fieldname": "department", + "fieldtype": "Link", + "label": "Department", + "options": "Department", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_4", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "company", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Company", - "length": 0, - "no_copy": 0, - "options": "Company", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "company", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Company", + "options": "Company", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "from_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": "From Date", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "from_date", + "fieldtype": "Date", + "label": "From Date", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "to_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": "To Date", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "to_date", + "fieldtype": "Date", + "label": "To Date" + }, { - "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": "Shift Request", - "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": "Shift Request", + "print_hide": 1, + "read_only": 1 + }, + { + "default": "Draft", + "fieldname": "status", + "fieldtype": "Select", + "label": "Status", + "options": "Draft\nApproved\nRejected", + "reqd": 1 + }, + { + "fetch_from": "employee.shift_request_approver", + "fetch_if_empty": 1, + "fieldname": "approver", + "fieldtype": "Link", + "label": "Approver", + "options": "User", + "reqd": 1 } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 1, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2018-08-21 16:15:36.577448", - "modified_by": "Administrator", - "module": "HR", - "name": "Shift Request", - "name_case": "", - "owner": "Administrator", + ], + "is_submittable": 1, + "links": [], + "modified": "2020-08-10 17:59:31.550558", + "modified_by": "Administrator", + "module": "HR", + "name": "Shift Request", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 0, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Employee", - "set_user_permissions": 0, - "share": 1, - "submit": 1, + "create": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Employee", + "share": 1, "write": 1 - }, + }, { - "amend": 1, - "cancel": 1, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "HR Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 1, + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR Manager", + "share": 1, + "submit": 1, "write": 1 - }, + }, { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 0, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "HR User", - "set_user_permissions": 0, - "share": 1, - "submit": 1, + "create": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR User", + "share": 1, + "submit": 1, "write": 1 } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "title_field": "employee_name", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 + ], + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "employee_name", + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/hr/doctype/shift_request/shift_request.py b/erpnext/hr/doctype/shift_request/shift_request.py index ff5de08ee5..1c2801bf08 100644 --- a/erpnext/hr/doctype/shift_request/shift_request.py +++ b/erpnext/hr/doctype/shift_request/shift_request.py @@ -14,19 +14,26 @@ class ShiftRequest(Document): def validate(self): self.validate_dates() self.validate_shift_request_overlap_dates() + self.validate_approver() + self.validate_default_shift() def on_submit(self): - date_list = self.get_working_days(self.from_date, self.to_date) - for date in date_list: + if self.status not in ["Approved", "Rejected"]: + frappe.throw(_("Only Shift Request with status 'Approved' and 'Rejected' can be submitted")) + if self.status == "Approved": assignment_doc = frappe.new_doc("Shift Assignment") assignment_doc.company = self.company assignment_doc.shift_type = self.shift_type assignment_doc.employee = self.employee - assignment_doc.date = date + assignment_doc.start_date = self.from_date + if self.to_date: + assignment_doc.end_date = self.to_date assignment_doc.shift_request = self.name assignment_doc.insert() assignment_doc.submit() + frappe.msgprint(_("Shift Assignment: {0} created for Employee: {1}").format(frappe.bold(assignment_doc.name), frappe.bold(self.employee))) + def on_cancel(self): shift_assignment_list = frappe.get_list("Shift Assignment", {'employee': self.employee, 'shift_request': self.name}) if shift_assignment_list: @@ -34,6 +41,19 @@ class ShiftRequest(Document): shift_assignment_doc = frappe.get_doc("Shift Assignment", shift['name']) shift_assignment_doc.cancel() + def validate_default_shift(self): + default_shift = frappe.get_value("Employee", self.employee, "default_shift") + if self.shift_type == default_shift: + frappe.throw(_("You can not request for your Default Shift: {0}").format(frappe.bold(self.shift_type))) + + def validate_approver(self): + department = frappe.get_value("Employee", self.employee, "department") + shift_approver = frappe.get_value("Employee", self.employee, "shift_request_approver") + approvers = frappe.db.sql("""select approver from `tabDepartment Approver` where parent= %s and parentfield = 'shift_request_approver'""", (department)) + approvers = [approver[0] for approver in approvers] + approvers.append(shift_approver) + if self.approver not in approvers: + frappe.throw(_("Only Approvers can Approve this Request.")) def validate_dates(self): if self.from_date and self.to_date and (getdate(self.to_date) < getdate(self.from_date)): @@ -68,28 +88,4 @@ class ShiftRequest(Document): msg = _("Employee {0} has already applied for {1} between {2} and {3} : ").format(self.employee, d['shift_type'], formatdate(d['from_date']), formatdate(d['to_date'])) \ + """ {0}""".format(d["name"]) - frappe.throw(msg, OverlapError) - - def get_working_days(self, start_date, end_date): - start_date, end_date = getdate(start_date), getdate(end_date) - - from datetime import timedelta - - date_list = [] - employee_holiday_list = [] - - employee_holidays = frappe.db.sql("""select holiday_date from `tabHoliday` - where parent in (select holiday_list from `tabEmployee` - where name = %s)""",self.employee,as_dict=1) - - for d in employee_holidays: - employee_holiday_list.append(d.holiday_date) - - reference_date = start_date - - while reference_date <= end_date: - if reference_date not in employee_holiday_list: - date_list.append(reference_date) - reference_date += timedelta(days=1) - - return date_list \ No newline at end of file + frappe.throw(msg, OverlapError) \ No newline at end of file diff --git a/erpnext/hr/doctype/shift_request/test_shift_request.py b/erpnext/hr/doctype/shift_request/test_shift_request.py index 1d0cf719c2..3dcfcbf4a5 100644 --- a/erpnext/hr/doctype/shift_request/test_shift_request.py +++ b/erpnext/hr/doctype/shift_request/test_shift_request.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import frappe import unittest -from frappe.utils import nowdate +from frappe.utils import nowdate, add_days class TestShiftRequest(unittest.TestCase): def setUp(self): @@ -13,14 +13,20 @@ class TestShiftRequest(unittest.TestCase): frappe.db.sql("delete from `tab{doctype}`".format(doctype=doctype)) def test_make_shift_request(self): + department = frappe.get_value("Employee", "_T-Employee-00001", 'department') + set_shift_approver(department) + approver = frappe.db.sql("""select approver from `tabDepartment Approver` where parent= %s and parentfield = 'shift_request_approver'""", (department))[0][0] + shift_request = frappe.get_doc({ "doctype": "Shift Request", "shift_type": "Day Shift", "company": "_Test Company", "employee": "_T-Employee-00001", "employee_name": "_Test Employee", - "start_date": nowdate(), - "end_date": nowdate() + "from_date": nowdate(), + "to_date": add_days(nowdate(), 10), + "approver": approver, + "status": "Approved" }) shift_request.insert() shift_request.submit() @@ -34,4 +40,10 @@ class TestShiftRequest(unittest.TestCase): self.assertEqual(shift_request.employee, employee) shift_request.cancel() shift_assignment_doc = frappe.get_doc("Shift Assignment", {"shift_request": d.get('shift_request')}) - self.assertEqual(shift_assignment_doc.docstatus, 2) \ No newline at end of file + self.assertEqual(shift_assignment_doc.docstatus, 2) + +def set_shift_approver(department): + department_doc = frappe.get_doc("Department", department) + department_doc.append('shift_request_approver',{'approver': "test1@example.com"}) + department_doc.save() + department_doc.reload() \ No newline at end of file diff --git a/erpnext/hr/doctype/shift_type/shift_type.js b/erpnext/hr/doctype/shift_type/shift_type.js index e633545630..ba53312bce 100644 --- a/erpnext/hr/doctype/shift_type/shift_type.js +++ b/erpnext/hr/doctype/shift_type/shift_type.js @@ -4,7 +4,7 @@ frappe.ui.form.on('Shift Type', { refresh: function(frm) { frm.add_custom_button( - 'Mark Auto Attendance', + 'Mark Attendance', () => frm.call({ doc: frm.doc, method: 'process_auto_attendance', diff --git a/erpnext/hr/doctype/shift_type/shift_type.py b/erpnext/hr/doctype/shift_type/shift_type.py index 19735648aa..054e7e3688 100644 --- a/erpnext/hr/doctype/shift_type/shift_type.py +++ b/erpnext/hr/doctype/shift_type/shift_type.py @@ -79,9 +79,10 @@ class ShiftType(Document): mark_attendance(employee, date, 'Absent', self.name) def get_assigned_employee(self, from_date=None, consider_default_shift=False): - filters = {'date':('>=', from_date), 'shift_type': self.name, 'docstatus': '1'} + filters = {'start_date':('>', from_date), 'shift_type': self.name, 'docstatus': '1'} if not from_date: - del filters['date'] + del filters["start_date"] + assigned_employees = frappe.get_all('Shift Assignment', 'employee', filters, as_list=True) assigned_employees = [x[0] for x in assigned_employees] 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/hr/report/monthly_attendance_sheet/monthly_attendance_sheet.js b/erpnext/hr/report/monthly_attendance_sheet/monthly_attendance_sheet.js index 4b9b928640..42f7cdb50f 100644 --- a/erpnext/hr/report/monthly_attendance_sheet/monthly_attendance_sheet.js +++ b/erpnext/hr/report/monthly_attendance_sheet/monthly_attendance_sheet.js @@ -35,7 +35,15 @@ frappe.query_reports["Monthly Attendance Sheet"] = { "fieldname":"employee", "label": __("Employee"), "fieldtype": "Link", - "options": "Employee" + "options": "Employee", + get_query: () => { + var company = frappe.query_report.get_filter_value('company'); + return { + filters: { + 'company': company + } + }; + } }, { "fieldname":"company", diff --git a/erpnext/hr/report/recruitment_analytics/recruitment_analytics.py b/erpnext/hr/report/recruitment_analytics/recruitment_analytics.py index 867209436c..e961114ac2 100644 --- a/erpnext/hr/report/recruitment_analytics/recruitment_analytics.py +++ b/erpnext/hr/report/recruitment_analytics/recruitment_analytics.py @@ -96,33 +96,35 @@ def get_data(filters): def get_parent_row(sp_jo_map, sp, jo_ja_map, ja_joff_map): data = [] - for jo in sp_jo_map[sp]: - row = { - "staffing_plan" : sp, - "job_opening" : jo["name"], - } - data.append(row) - child_row = get_child_row( jo["name"], jo_ja_map, ja_joff_map) - data += child_row + if sp in sp_jo_map.keys(): + for jo in sp_jo_map[sp]: + row = { + "staffing_plan" : sp, + "job_opening" : jo["name"], + } + data.append(row) + child_row = get_child_row( jo["name"], jo_ja_map, ja_joff_map) + data += child_row return data def get_child_row(jo, jo_ja_map, ja_joff_map): data = [] - for ja in jo_ja_map[jo]: - row = { - "indent":1, - "job_applicant": ja.name, - "applicant_name": ja.applicant_name, - "application_status": ja.status, - } - if ja.name in ja_joff_map.keys(): - jo_detail =ja_joff_map[ja.name][0] - row["job_offer"] = jo_detail.name - row["job_offer_status"] = jo_detail.status - row["offer_date"]= jo_detail.offer_date.strftime("%d-%m-%Y") - row["designation"] = jo_detail.designation + if jo in jo_ja_map.keys(): + for ja in jo_ja_map[jo]: + row = { + "indent":1, + "job_applicant": ja.name, + "applicant_name": ja.applicant_name, + "application_status": ja.status, + } + if ja.name in ja_joff_map.keys(): + jo_detail =ja_joff_map[ja.name][0] + row["job_offer"] = jo_detail.name + row["job_offer_status"] = jo_detail.status + row["offer_date"]= jo_detail.offer_date.strftime("%d-%m-%Y") + row["designation"] = jo_detail.designation - data.append(row) + data.append(row) return data def get_staffing_plan(filters): @@ -177,7 +179,7 @@ def get_job_applicant(jo_list): def get_job_offer(ja_list): ja_joff_map = {} - offers = frappe.get_all("Job offer", filters = [["job_applicant", "IN", ja_list]], fields =["name", "job_applicant", "status", 'offer_date', 'designation']) + offers = frappe.get_all("Job Offer", filters = [["job_applicant", "IN", ja_list]], fields =["name", "job_applicant", "status", 'offer_date', 'designation']) for offer in offers: if offer.job_applicant not in ja_joff_map.keys(): diff --git a/erpnext/loan_management/desk_page/loan/loan.json b/erpnext/loan_management/desk_page/loan/loan.json index 48193b0a0d..3bdd1ce56e 100644 --- a/erpnext/loan_management/desk_page/loan/loan.json +++ b/erpnext/loan_management/desk_page/loan/loan.json @@ -3,7 +3,7 @@ { "hidden": 0, "label": "Loan", - "links": "[\n {\n \"description\": \"Loan Type for interest and penalty rates\",\n \"label\": \"Loan Type\",\n \"name\": \"Loan Type\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Loan Applications from customers and employees.\",\n \"label\": \"Loan Application\",\n \"name\": \"Loan Application\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Loans provided to customers and employees.\",\n \"label\": \"Loan\",\n \"name\": \"Loan\",\n \"type\": \"doctype\"\n }\n]" + "links": "[\n {\n \"description\": \"Loan Type for interest and penalty rates\",\n \"label\": \"Loan Type\",\n \"name\": \"Loan Type\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Loan Applications from customers and employees.\",\n \"label\": \"Loan Application\",\n \"name\": \"Loan Application\",\n \"type\": \"doctype\"\n },\n { \"dependencies\": [\n \"Loan Type\"\n ],\n \"description\": \"Loans provided to customers and employees.\",\n \"label\": \"Loan\",\n \"name\": \"Loan\",\n \"type\": \"doctype\"\n }\n]" }, { "hidden": 0, 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.json b/erpnext/loan_management/doctype/loan/loan.json index 192beee7e3..aa5e21b426 100644 --- a/erpnext/loan_management/doctype/loan/loan.json +++ b/erpnext/loan_management/doctype/loan/loan.json @@ -20,8 +20,8 @@ "section_break_8", "loan_type", "loan_amount", - "is_secured_loan", "rate_of_interest", + "is_secured_loan", "disbursement_date", "disbursed_amount", "column_break_11", @@ -334,7 +334,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2020-07-02 20:46:40.128142", + "modified": "2020-08-01 12:36:11.255233", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan", 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 c65996e65f..f225409f62 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() @@ -269,7 +266,7 @@ class TestLoan(unittest.TestCase): self.assertTrue(loan_security_shortfall) self.assertEquals(loan_security_shortfall.loan_amount, 1000000.00) - self.assertEquals(loan_security_shortfall.security_value, 400000.00) + self.assertEquals(loan_security_shortfall.security_value, 800000.00) self.assertEquals(loan_security_shortfall.shortfall_amount, 600000.00) frappe.db.sql(""" UPDATE `tabLoan Security Price` SET loan_security_price = 250 @@ -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,97 @@ class TestLoan(unittest.TestCase): self.assertEqual(loan.status, 'Closed') self.assertEquals(sum(pledged_qty.values()), 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 6cf47bf85c..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({ @@ -112,16 +112,19 @@ frappe.ui.form.on('Loan Application', { frappe.ui.form.on("Proposed Pledge", { loan_security: function(frm, cdt, cdn) { let row = locals[cdt][cdn]; - frappe.call({ - method: "erpnext.loan_management.doctype.loan_security_price.loan_security_price.get_loan_security_price", - args: { - loan_security: row.loan_security - }, - callback: function(r) { - frappe.model.set_value(cdt, cdn, 'loan_security_price', r.message); - frm.events.calculate_amounts(frm, cdt, cdn); - } - }) + + if (row.loan_security) { + frappe.call({ + method: "erpnext.loan_management.doctype.loan_security_price.loan_security_price.get_loan_security_price", + args: { + loan_security: row.loan_security + }, + callback: function(r) { + frappe.model.set_value(cdt, cdn, 'loan_security_price', r.message); + frm.events.calculate_amounts(frm, cdt, cdn); + } + }) + } }, amount: function(frm, cdt, cdn) { diff --git a/erpnext/loan_management/doctype/loan_application/loan_application.py b/erpnext/loan_management/doctype/loan_application/loan_application.py index f051755f67..bac6e638d7 100644 --- a/erpnext/loan_management/doctype/loan_application/loan_application.py +++ b/erpnext/loan_management/doctype/loan_application/loan_application.py @@ -16,14 +16,16 @@ from six import string_types class LoanApplication(Document): def validate(self): - - validate_repayment_method(self.repayment_method, self.loan_amount, self.repayment_amount, - self.repayment_periods, self.is_term_loan) - - self.validate_loan_type() self.set_pledge_amount() self.set_loan_amount() self.validate_loan_amount() + + if self.is_term_loan: + validate_repayment_method(self.repayment_method, self.loan_amount, self.repayment_amount, + self.repayment_periods, self.is_term_loan) + + self.validate_loan_type() + self.get_repayment_details() self.check_sanctioned_amount_limit() @@ -106,7 +108,7 @@ class LoanApplication(Document): if self.is_secured_loan and self.proposed_pledges: self.maximum_loan_amount = 0 for security in self.proposed_pledges: - self.maximum_loan_amount += security.post_haircut_amount + self.maximum_loan_amount += flt(security.post_haircut_amount) if not self.loan_amount and self.is_secured_loan and self.proposed_pledges: self.loan_amount = self.maximum_loan_amount @@ -133,10 +135,7 @@ def create_loan(source_name, target_doc=None, submit=0): "validation": { "docstatus": ["=", 1] }, - "postprocess": update_accounts, - "field_no_map": [ - "is_secured_loan" - ] + "postprocess": update_accounts } }, target_doc) diff --git a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py index d44088bee7..260fada893 100644 --- a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py +++ b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py @@ -10,22 +10,20 @@ from frappe.utils import nowdate, getdate, add_days, flt from erpnext.controllers.accounts_controller import AccountsController from erpnext.accounts.general_ledger import make_gl_entries from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import process_loan_interest_accrual_for_demand_loans +from erpnext.loan_management.doctype.loan_security_unpledge.loan_security_unpledge import get_pledged_security_qty +from frappe.utils import get_datetime class LoanDisbursement(AccountsController): def validate(self): self.set_missing_values() - def before_submit(self): - self.set_status_and_amounts() - - def before_cancel(self): - self.set_status_and_amounts(cancel=1) - def on_submit(self): + self.set_status_and_amounts() self.make_gl_entries() def on_cancel(self): + self.set_status_and_amounts(cancel=1) self.make_gl_entries(cancel=1) self.ignore_linked_doctypes = ['GL Entry'] @@ -45,29 +43,51 @@ class LoanDisbursement(AccountsController): def set_status_and_amounts(self, cancel=0): loan_details = frappe.get_all("Loan", - fields = ["loan_amount", "disbursed_amount", "total_principal_paid", "status", "is_term_loan"], - filters= { "name": self.against_loan } - )[0] - - 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), - loan=self.against_loan) + fields = ["loan_amount", "disbursed_amount", "total_payment", "total_principal_paid", "total_interest_payable", + "status", "is_term_loan", "is_secured_loan"], filters= { "name": self.against_loan })[0] if cancel: disbursed_amount = loan_details.disbursed_amount - self.disbursed_amount + total_payment = loan_details.total_payment + + if loan_details.disbursed_amount > loan_details.loan_amount: + topup_amount = loan_details.disbursed_amount - loan_details.loan_amount + if topup_amount > self.disbursed_amount: + topup_amount = self.disbursed_amount + + total_payment = total_payment - topup_amount + if disbursed_amount == 0: status = "Sanctioned" - elif disbursed_amount >= loan_details.disbursed_amount: + elif disbursed_amount >= loan_details.loan_amount: status = "Disbursed" else: status = "Partially Disbursed" else: disbursed_amount = self.disbursed_amount + loan_details.disbursed_amount + total_payment = loan_details.total_payment - if flt(disbursed_amount) - flt(loan_details.total_principal_paid) > flt(loan_details.loan_amount): - frappe.throw(_("Disbursed Amount cannot be greater than loan amount")) + possible_disbursal_amount = get_disbursal_amount(self.against_loan) - if flt(disbursed_amount) >= loan_details.disbursed_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), + loan=self.against_loan) + + if disbursed_amount > loan_details.loan_amount: + topup_amount = disbursed_amount - loan_details.loan_amount + + if topup_amount < 0: + topup_amount = 0 + + if topup_amount > self.disbursed_amount: + topup_amount = self.disbursed_amount + + total_payment = total_payment + topup_amount + + if flt(disbursed_amount) >= loan_details.loan_amount: status = "Disbursed" else: status = "Partially Disbursed" @@ -75,7 +95,8 @@ class LoanDisbursement(AccountsController): frappe.db.set_value("Loan", self.against_loan, { "disbursement_date": self.disbursement_date, "disbursed_amount": disbursed_amount, - "status": status + "status": status, + "total_payment": total_payment }) def make_gl_entries(self, cancel=0, adv_adj=0): @@ -116,3 +137,53 @@ class LoanDisbursement(AccountsController): if gle_map: make_gl_entries(gle_map, cancel=cancel, adv_adj=adv_adj) + +def get_total_pledged_security_value(loan): + update_time = get_datetime() + + loan_security_price_map = frappe._dict(frappe.get_all("Loan Security Price", + fields=["loan_security", "loan_security_price"], + filters = { + "valid_from": ("<=", update_time), + "valid_upto": (">=", update_time) + }, as_list=1)) + + hair_cut_map = frappe._dict(frappe.get_all('Loan Security', + fields=["name", "haircut"], as_list=1)) + + security_value = 0.0 + pledged_securities = get_pledged_security_qty(loan) + + for security, qty in pledged_securities.items(): + 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 e6ceb55185..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 @@ -19,8 +19,8 @@ class LoanInterestAccrual(AccountsController): if not self.posting_date: self.posting_date = nowdate() - if not self.interest_amount: - frappe.throw(_("Interest Amount is mandatory")) + if not self.interest_amount and not self.payable_principal_amount: + frappe.throw(_("Interest Amount or Principal Amount is mandatory")) def on_submit(self): @@ -39,37 +39,38 @@ class LoanInterestAccrual(AccountsController): def make_gl_entries(self, cancel=0, adv_adj=0): gle_map = [] - gle_map.append( - self.get_gl_dict({ - "account": self.loan_account, - "party_type": self.applicant_type, - "party": self.applicant, - "against": self.interest_income_account, - "debit": self.interest_amount, - "debit_in_account_currency": self.interest_amount, - "against_voucher_type": "Loan", - "against_voucher": self.loan, - "remarks": _("Against Loan:") + self.loan, - "cost_center": erpnext.get_default_cost_center(self.company), - "posting_date": self.posting_date - }) - ) + if self.interest_amount: + gle_map.append( + self.get_gl_dict({ + "account": self.loan_account, + "party_type": self.applicant_type, + "party": self.applicant, + "against": self.interest_income_account, + "debit": self.interest_amount, + "debit_in_account_currency": self.interest_amount, + "against_voucher_type": "Loan", + "against_voucher": self.loan, + "remarks": _("Against Loan:") + self.loan, + "cost_center": erpnext.get_default_cost_center(self.company), + "posting_date": self.posting_date + }) + ) - gle_map.append( - self.get_gl_dict({ - "account": self.interest_income_account, - "party_type": self.applicant_type, - "party": self.applicant, - "against": self.loan_account, - "credit": self.interest_amount, - "credit_in_account_currency": self.interest_amount, - "against_voucher_type": "Loan", - "against_voucher": self.loan, - "remarks": _("Against Loan:") + self.loan, - "cost_center": erpnext.get_default_cost_center(self.company), - "posting_date": self.posting_date - }) - ) + gle_map.append( + self.get_gl_dict({ + "account": self.interest_income_account, + "party_type": self.applicant_type, + "party": self.applicant, + "against": self.loan_account, + "credit": self.interest_amount, + "credit_in_account_currency": self.interest_amount, + "against_voucher_type": "Loan", + "against_voucher": self.loan, + "remarks": _("Against Loan:") + self.loan, + "cost_center": erpnext.get_default_cost_center(self.company), + "posting_date": self.posting_date + }) + ) if gle_map: make_gl_entries(gle_map, cancel=cancel, adv_adj=adv_adj) @@ -84,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 = loan.total_payment - loan.total_interest_payable \ - - loan.total_amount_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 @@ -106,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 } @@ -117,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: @@ -208,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.json b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json index 789c129946..5942455919 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json @@ -173,7 +173,7 @@ { "fieldname": "references_section", "fieldtype": "Section Break", - "label": "References" + "label": "Payment References" }, { "fieldname": "reference_number", @@ -221,7 +221,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2020-04-16 18:14:45.166754", + "modified": "2020-05-16 09:40:15.581165", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Repayment", diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index 9605045777..47fb885f8a 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'): + 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.js b/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.js index 82837b3dac..11c932ff1c 100644 --- a/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.js +++ b/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.js @@ -22,16 +22,19 @@ frappe.ui.form.on('Loan Security Pledge', { frappe.ui.form.on("Pledge", { loan_security: function(frm, cdt, cdn) { let row = locals[cdt][cdn]; - frappe.call({ - method: "erpnext.loan_management.doctype.loan_security_price.loan_security_price.get_loan_security_price", - args: { - loan_security: row.loan_security - }, - callback: function(r) { - frappe.model.set_value(cdt, cdn, 'loan_security_price', r.message); - frm.events.calculate_amounts(frm, cdt, cdn); - } - }); + + if (row.loan_security) { + frappe.call({ + method: "erpnext.loan_management.doctype.loan_security_price.loan_security_price.get_loan_security_price", + args: { + loan_security: row.loan_security + }, + callback: function(r) { + frappe.model.set_value(cdt, cdn, 'loan_security_price', r.message); + frm.events.calculate_amounts(frm, cdt, cdn); + } + }); + } }, qty: function(frm, cdt, cdn) { 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_pledge/loan_security_pledge.py b/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.py index 961c05c9c1..2bb6fd84e5 100644 --- a/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.py +++ b/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.py @@ -14,6 +14,7 @@ class LoanSecurityPledge(Document): def validate(self): self.set_pledge_amount() self.validate_duplicate_securities() + self.validate_loan_security_type() def on_submit(self): if self.loan: @@ -31,6 +32,27 @@ class LoanSecurityPledge(Document): frappe.throw(_('Loan Security {0} added multiple times').format(frappe.bold( security.loan_security))) + def validate_loan_security_type(self): + existing_pledge = '' + + if self.loan: + existing_pledge = frappe.db.get_value('Loan Security Pledge', {'loan': self.loan}, ['name']) + + if existing_pledge: + loan_security_type = frappe.db.get_value('Pledge', {'parent': existing_pledge}, ['loan_security_type']) + else: + loan_security_type = self.securities[0].loan_security_type + + ltv_ratio_map = frappe._dict(frappe.get_all("Loan Security Type", + fields=["name", "loan_to_value_ratio"], as_list=1)) + + ltv_ratio = ltv_ratio_map.get(loan_security_type) + + for security in self.securities: + if ltv_ratio_map.get(security.loan_security_type) != ltv_ratio: + frappe.throw(_("Loan Securities with different LTV ratio cannot be pledged against one loan")) + + def set_pledge_amount(self): total_security_value = 0 maximum_loan_value = 0 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 ffd96737a5..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,9 +4,10 @@ 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 class LoanSecurityShortfall(Document): pass @@ -50,31 +51,36 @@ def check_for_ltv_shortfall(process_loan_security_shortfall): "valid_upto": (">=", update_time) }, as_list=1)) - ltv_ratio_map = frappe._dict(frappe.get_all("Loan Security Type", - fields=["name", "loan_to_value_ratio"], as_list=1)) - - loans = frappe.db.sql(""" SELECT l.name, l.loan_amount, l.total_principal_paid, lp.loan_security, lp.haircut, lp.qty, lp.loan_security_type - FROM `tabLoan` l, `tabPledge` lp , `tabLoan Security Pledge`p WHERE lp.parent = p.name and p.loan = l.name and l.docstatus = 1 - and l.is_secured_loan and l.status = 'Disbursed' and p.status = 'Pledged'""", as_dict=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: - loan_security_map.setdefault(loan.name, { - "loan_amount": loan.loan_amount - loan.total_principal_paid, - "security_value": 0.0 - }) + 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 - current_loan_security_amount = loan_security_price_map.get(loan.loan_security, 0) * loan.qty - ltv_ratio = ltv_ratio_map.get(loan.loan_security_type) + pledged_securities = get_pledged_security_qty(loan.name) + ltv_ratio = '' + security_value = 0.0 - loan_security_map[loan.name]['security_value'] += current_loan_security_amount - (current_loan_security_amount * loan.haircut/100) + for security, qty in pledged_securities.items(): + if not ltv_ratio: + ltv_ratio = get_ltv_ratio(security) + security_value += loan_security_price_map.get(security) * qty - for loan, value in iteritems(loan_security_map): - if (value["loan_amount"]/value['security_value'] * 100) > ltv_ratio: - create_loan_security_shortfall(loan, value, process_loan_security_shortfall) + current_ratio = (outstanding_amount/security_value) * 100 -def create_loan_security_shortfall(loan, value, process_loan_security_shortfall): + if current_ratio > ltv_ratio: + shortfall_amount = outstanding_amount - ((security_value * ltv_ratio) / 100) + create_loan_security_shortfall(loan.name, outstanding_amount, security_value, shortfall_amount, + process_loan_security_shortfall) + +def create_loan_security_shortfall(loan, loan_amount, security_value, shortfall_amount, process_loan_security_shortfall): existing_shortfall = frappe.db.get_value("Loan Security Shortfall", {"loan": loan, "status": "Pending"}, "name") @@ -85,9 +91,14 @@ def create_loan_security_shortfall(loan, value, process_loan_security_shortfall) ltv_shortfall.loan = loan ltv_shortfall.shortfall_time = get_datetime() - ltv_shortfall.loan_amount = value["loan_amount"] - ltv_shortfall.security_value = value["security_value"] - ltv_shortfall.shortfall_amount = value["loan_amount"] - value["security_value"] + ltv_shortfall.loan_amount = loan_amount + ltv_shortfall.security_value = security_value + ltv_shortfall.shortfall_amount = shortfall_amount ltv_shortfall.process_loan_security_shortfall = process_loan_security_shortfall ltv_shortfall.save() +def get_ltv_ratio(loan_security): + loan_security_type = frappe.db.get_value('Loan Security', loan_security, 'loan_security_type') + ltv_ratio = frappe.db.get_value('Loan Security Type', loan_security_type, 'loan_to_value_ratio') + return ltv_ratio + diff --git a/erpnext/loan_management/doctype/loan_security_type/loan_security_type.json b/erpnext/loan_management/doctype/loan_security_type/loan_security_type.json index f46b88cbca..871e82563a 100644 --- a/erpnext/loan_management/doctype/loan_security_type/loan_security_type.json +++ b/erpnext/loan_management/doctype/loan_security_type/loan_security_type.json @@ -29,6 +29,7 @@ "unique": 1 }, { + "description": "Haircut percentage is the percentage difference between market value of the Loan Security and the value ascribed to that Loan Security when used as collateral for that loan.", "fieldname": "haircut", "fieldtype": "Percent", "label": "Haircut %" @@ -46,13 +47,14 @@ "fieldtype": "Column Break" }, { + "description": "Loan To Value Ratio expresses the ratio of the loan amount to the value of the security pledged. A loan security shortfall will be triggered if this falls below the specified value for any loan ", "fieldname": "loan_to_value_ratio", "fieldtype": "Percent", "label": "Loan To Value Ratio" } ], "links": [], - "modified": "2020-04-28 14:06:49.046177", + "modified": "2020-05-16 09:38:45.988080", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Security Type", 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/loan_type/loan_type.json b/erpnext/loan_management/doctype/loan_type/loan_type.json index 1dd3710cd2..669490a448 100644 --- a/erpnext/loan_management/doctype/loan_type/loan_type.json +++ b/erpnext/loan_management/doctype/loan_type/loan_type.json @@ -76,6 +76,7 @@ "reqd": 1 }, { + "description": "This account is used for booking loan repayments from the borrower and also disbursing loans to the borrower", "fieldname": "payment_account", "fieldtype": "Link", "label": "Payment Account", @@ -83,6 +84,7 @@ "reqd": 1 }, { + "description": "This account is capital account which is used to allocate capital for loan disbursal account ", "fieldname": "loan_account", "fieldtype": "Link", "label": "Loan Account", @@ -94,6 +96,7 @@ "fieldtype": "Column Break" }, { + "description": "This account will be used for booking loan interest accruals", "fieldname": "interest_income_account", "fieldtype": "Link", "label": "Interest Income Account", @@ -101,6 +104,7 @@ "reqd": 1 }, { + "description": "This account will be used for booking penalties levied due to delayed repayments", "fieldname": "penalty_income_account", "fieldtype": "Link", "label": "Penalty Income Account", @@ -109,6 +113,7 @@ }, { "default": "0", + "description": "If this is not checked the loan by default will be considered as a Demand Loan", "fieldname": "is_term_loan", "fieldtype": "Check", "label": "Is Term Loan" 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 8062342cfc..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 })) @@ -911,6 +911,7 @@ def get_bom_diff(bom1, bom2): return out @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def item_query(doctype, txt, searchfield, start, page_len, filters): meta = frappe.get_meta("Item", cached=True) searchfields = meta.get_search_fields() 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/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index f962a1157b..b7d968e974 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -632,6 +632,7 @@ class WorkOrder(Document): return bom @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def get_bom_operations(doctype, txt, searchfield, start, page_len, filters): if txt: filters['operation'] = ('like', '%%%s%%' % txt) 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/manufacturing/report/bom_variance_report/bom_variance_report.py b/erpnext/manufacturing/report/bom_variance_report/bom_variance_report.py index e3e440ebc6..dc424b7605 100644 --- a/erpnext/manufacturing/report/bom_variance_report/bom_variance_report.py +++ b/erpnext/manufacturing/report/bom_variance_report/bom_variance_report.py @@ -30,7 +30,7 @@ def get_columns(filters): "width": 180 } ]) - + columns.extend([ { "label": _("Finished Good"), @@ -73,7 +73,7 @@ def get_columns(filters): ]) return columns - + def get_data(filters): cond = "1=1" @@ -95,6 +95,7 @@ def get_data(filters): return results @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def get_work_orders(doctype, txt, searchfield, start, page_len, filters): cond = "1=1" if filters.get('bom_no'): diff --git a/erpnext/manufacturing/report/downtime_analysis/downtime_analysis.js b/erpnext/manufacturing/report/downtime_analysis/downtime_analysis.js index ff32dbed98..f6486743aa 100644 --- a/erpnext/manufacturing/report/downtime_analysis/downtime_analysis.js +++ b/erpnext/manufacturing/report/downtime_analysis/downtime_analysis.js @@ -8,7 +8,7 @@ frappe.query_reports["Downtime Analysis"] = { label: __("From Date"), fieldname:"from_date", fieldtype: "Datetime", - default: frappe.datetime.add_months(frappe.datetime.now_datetime(), -1), + default: frappe.datetime.convert_to_system_tz(frappe.datetime.add_months(frappe.datetime.now_datetime(), -1)), reqd: 1 }, { diff --git a/erpnext/manufacturing/report/production_planning_report/production_planning_report.py b/erpnext/manufacturing/report/production_planning_report/production_planning_report.py index 5ac3923187..ebc01c65af 100644 --- a/erpnext/manufacturing/report/production_planning_report/production_planning_report.py +++ b/erpnext/manufacturing/report/production_planning_report/production_planning_report.py @@ -369,6 +369,3 @@ class ProductionPlanReport(object): "fieldtype": "Float", "width": 140 }]) - -def document_query(doctype, txt, searchfield, start, page_len, filters): - pass \ 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 3bd416952f..aa7996e3e1 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 @@ -679,7 +679,6 @@ erpnext.patches.v12_0.fix_quotation_expired_status erpnext.patches.v12_0.update_appointment_reminder_scheduler_entry erpnext.patches.v12_0.rename_pos_closing_doctype erpnext.patches.v13_0.replace_pos_payment_mode_table -erpnext.patches.v12_0.retain_permission_rules_for_video_doctype erpnext.patches.v12_0.remove_duplicate_leave_ledger_entries #2020-05-22 erpnext.patches.v13_0.patch_to_fix_reverse_linking_in_additional_salary_encashment_and_incentive execute:frappe.reload_doc("HR", "doctype", "Employee Advance") @@ -688,6 +687,7 @@ execute:frappe.delete_doc_if_exists("Page", "appointment-analytic") execute:frappe.rename_doc("Desk Page", "Getting Started", "Home", force=True) erpnext.patches.v12_0.unset_customer_supplier_based_on_type_of_item_price erpnext.patches.v12_0.set_valid_till_date_in_supplier_quotation +erpnext.patches.v13_0.update_old_loans erpnext.patches.v12_0.set_serial_no_status #2020-05-21 erpnext.patches.v12_0.update_price_list_currency_in_bom execute:frappe.delete_doc_if_exists('Dashboard', 'Accounts') @@ -718,3 +718,10 @@ 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 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 new file mode 100644 index 0000000000..d0dc356bd0 --- /dev/null +++ b/erpnext/patches/v12_0/rename_lost_reason_detail.py @@ -0,0 +1,18 @@ +from __future__ import unicode_literals +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") + + frappe.db.sql("""INSERT INTO `tabOpportunity Lost Reason Detail` SELECT * FROM `tabLost Reason Detail` WHERE `parenttype` = 'Opportunity'""") + + 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` + 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/v12_0/retain_permission_rules_for_video_doctype.py b/erpnext/patches/v12_0/retain_permission_rules_for_video_doctype.py deleted file mode 100644 index ca8a13b13c..0000000000 --- a/erpnext/patches/v12_0/retain_permission_rules_for_video_doctype.py +++ /dev/null @@ -1,21 +0,0 @@ -from __future__ import unicode_literals -import frappe - -def execute(): - # to retain the roles and permissions from Education Module - # after moving doctype to core - permissions = frappe.db.sql(""" - SELECT - * - FROM - `tabDocPerm` - WHERE - parent='Video' - """, as_dict=True) - - frappe.reload_doc('core', 'doctype', 'video') - doc = frappe.get_doc('DocType', 'Video') - doc.permissions = [] - for perm in permissions: - doc.append('permissions', perm) - doc.save() diff --git a/erpnext/patches/v12_0/stock_entry_enhancements.py b/erpnext/patches/v12_0/stock_entry_enhancements.py index d04b3d3862..847d92894b 100644 --- a/erpnext/patches/v12_0/stock_entry_enhancements.py +++ b/erpnext/patches/v12_0/stock_entry_enhancements.py @@ -19,7 +19,7 @@ def create_stock_entry_types(): for purpose in ["Material Issue", "Material Receipt", "Material Transfer", "Material Transfer for Manufacture", "Material Consumption for Manufacture", "Manufacture", - "Repack", "Send to Subcontractor", "Send to Warehouse", "Receive at Warehouse"]: + "Repack", "Send to Subcontractor"]: ste_type = frappe.get_doc({ 'doctype': 'Stock Entry Type', diff --git a/erpnext/patches/v12_0/update_state_code_for_daman_and_diu.py b/erpnext/patches/v12_0/update_state_code_for_daman_and_diu.py new file mode 100644 index 0000000000..7450e9cd8c --- /dev/null +++ b/erpnext/patches/v12_0/update_state_code_for_daman_and_diu.py @@ -0,0 +1,22 @@ +import frappe +from erpnext.regional.india import states + +def execute(): + + company = frappe.get_all('Company', filters = {'country': 'India'}) + if not company: + return + + # Update options in gst_state custom field + gst_state = frappe.get_doc('Custom Field', 'Address-gst_state') + gst_state.options = '\n'.join(states) + gst_state.save() + + # Update gst_state and state code in existing address + frappe.db.sql(""" + UPDATE `tabAddress` + SET + gst_state = 'Dadra and Nagar Haveli and Daman and Diu', + gst_state_number = 26 + WHERE gst_state = 'Daman and Diu' + """) \ 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/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/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 new file mode 100644 index 0000000000..0bdcc9c0e8 --- /dev/null +++ b/erpnext/patches/v13_0/stock_entry_enhancements.py @@ -0,0 +1,31 @@ +# Copyright(c) 2020, Frappe Technologies Pvt.Ltd.and Contributors +# License: GNU General Public License v3.See license.txt + +from __future__ import unicode_literals +import frappe + +def execute(): + frappe.reload_doc("stock", "doctype", "stock_entry") + if frappe.db.has_column("Stock Entry", "add_to_transit"): + frappe.db.sql(""" + UPDATE `tabStock Entry` SET + stock_entry_type = 'Material Transfer', + purpose = 'Material Transfer', + add_to_transit = 1 WHERE stock_entry_type = 'Send to Warehouse' + """) + + frappe.db.sql("""UPDATE `tabStock Entry` SET + stock_entry_type = 'Material Transfer', + purpose = 'Material Transfer' + WHERE stock_entry_type = 'Receive at Warehouse' + """) + + frappe.reload_doc("stock", "doctype", "warehouse_type") + if not frappe.db.exists('Warehouse Type', 'Transit'): + doc = frappe.new_doc('Warehouse Type') + doc.name = 'Transit' + 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_old_loans.py b/erpnext/patches/v13_0/update_old_loans.py new file mode 100644 index 0000000000..77239429c5 --- /dev/null +++ b/erpnext/patches/v13_0/update_old_loans.py @@ -0,0 +1,88 @@ +from __future__ import unicode_literals +import frappe +from frappe import _ +from frappe.utils import nowdate +from erpnext.accounts.doctype.account.test_account import create_account +from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import process_loan_interest_accrual_for_term_loans +from erpnext.loan_management.doctype.loan.loan import make_repayment_entry + +def execute(): + + # Create a penalty account for loan types + + frappe.reload_doc('loan_management', 'doctype', 'loan_type') + frappe.reload_doc('loan_management', 'doctype', 'loan') + frappe.reload_doc('loan_management', 'doctype', 'repayment_schedule') + frappe.reload_doc('loan_management', 'doctype', 'process_loan_interest_accrual') + frappe.reload_doc('loan_management', 'doctype', 'loan_repayment') + frappe.reload_doc('loan_management', 'doctype', 'loan_repayment_detail') + frappe.reload_doc('loan_management', 'doctype', 'loan_interest_accrual') + frappe.reload_doc('accounts', 'doctype', 'gl_entry') + + updated_loan_types = [] + + loans = frappe.get_all('Loan', fields=['name', 'loan_type', 'company', 'status', 'mode_of_payment', + 'applicant_type', 'applicant', 'loan_account', 'payment_account', 'interest_income_account']) + + for loan in loans: + # Update details in Loan Types and Loan + loan_type_company = frappe.db.get_value('Loan Type', loan.loan_type, 'company') + + group_income_account = frappe.get_value('Account', {'company': loan.company, + 'is_group': 1, 'root_type': 'Income', 'account_name': _('Indirect Income')}) + + if not group_income_account: + group_income_account = frappe.get_value('Account', {'company': loan.company, + 'is_group': 1, 'root_type': 'Income'}) + + penalty_account = create_account(company=loan.company, account_type='Income Account', + account_name='Penalty Account', parent_account=group_income_account) + + if not loan_type_company: + loan_type_doc = frappe.get_doc('Loan Type', loan.loan_type) + loan_type_doc.is_term_loan = 1 + loan_type_doc.company = loan.company + loan_type_doc.mode_of_payment = loan.mode_of_payment + loan_type_doc.payment_account = loan.payment_account + loan_type_doc.loan_account = loan.loan_account + loan_type_doc.interest_income_account = loan.interest_income_account + loan_type_doc.penalty_income_account = penalty_account + loan_type_doc.submit() + updated_loan_types.append(loan.loan_type) + + if loan.loan_type in updated_loan_types: + if loan.status == 'Fully Disbursed': + status = 'Disbursed' + elif loan.status == 'Repaid/Closed': + status = 'Closed' + else: + status = loan.status + + frappe.db.set_value('Loan', loan.name, { + 'is_term_loan': 1, + 'penalty_income_account': penalty_account, + 'status': status + }) + + process_loan_interest_accrual_for_term_loans(posting_date=nowdate(), loan_type=loan.loan_type, + loan=loan.name) + + payments = frappe.db.sql(''' SELECT j.name, a.debit, a.debit_in_account_currency, j.posting_date + FROM `tabJournal Entry` j, `tabJournal Entry Account` a + WHERE a.parent = j.name and a.reference_type='Loan' and a.reference_name = %s + and account = %s + ''', (loan.name, loan.loan_account), as_dict=1) + + for payment in payments: + repayment_entry = make_repayment_entry(loan.name, loan.loan_applicant_type, loan.applicant, + loan.loan_type, loan.company) + + repayment_entry.amount_paid = payment.debit_in_account_currency + repayment_entry.posting_date = payment.posting_date + repayment_entry.save() + repayment_entry.submit() + + jv = frappe.get_doc('Journal Entry', payment.name) + jv.flags.ignore_links = True + jv.cancel() + 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 new file mode 100644 index 0000000000..0f521cb57a --- /dev/null +++ b/erpnext/patches/v13_0/update_start_end_date_for_old_shift_assignment.py @@ -0,0 +1,13 @@ +# Copyright (c) 2019, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals + +import frappe + +def execute(): + frappe.reload_doc('hr', 'doctype', 'shift_assignment') + 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/desk_page/payroll/payroll.json b/erpnext/payroll/desk_page/payroll/payroll.json index b5eac465c8..285e3b3a13 100644 --- a/erpnext/payroll/desk_page/payroll/payroll.json +++ b/erpnext/payroll/desk_page/payroll/payroll.json @@ -8,7 +8,7 @@ { "hidden": 0, "label": "Taxation", - "links": "[\n {\n \"label\": \"Payroll Period\",\n \"name\": \"Payroll Period\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n \n },\n {\n \"label\": \"Income Tax Slab\",\n \"name\": \"Income Tax Slab\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n \n },\n {\n \"label\": \"Employee Tax Exemption Declaration\",\n \"name\": \"Employee Tax Exemption Declaration\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n \n },\n {\n \"label\": \"Employee Tax Exemption Proof Submission\",\n \"name\": \"Employee Tax Exemption Proof Submission\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n \n },\n {\n \"label\": \"Employee Tax Exemption Category\",\n \"name\": \"Employee Tax Exemption Category\",\n \"type\": \"doctype\"\n \n },\n {\n \"label\": \"Employee Tax Exemption Sub Category\",\n \"name\": \"Employee Tax Exemption Sub Category\",\n \"type\": \"doctype\"\n \n }\n]" + "links": "[\n {\n \"label\": \"Payroll Period\",\n \"name\": \"Payroll Period\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n \n },\n {\n \"label\": \"Income Tax Slab\",\n \"name\": \"Income Tax Slab\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n \n },\n {\n \"label\": \"Employee Other Income\",\n \"name\": \"Employee Other Income\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n \n },\n {\n \"label\": \"Employee Tax Exemption Declaration\",\n \"name\": \"Employee Tax Exemption Declaration\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n \n },\n {\n \"label\": \"Employee Tax Exemption Proof Submission\",\n \"name\": \"Employee Tax Exemption Proof Submission\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n \n },\n {\n \"label\": \"Employee Tax Exemption Category\",\n \"name\": \"Employee Tax Exemption Category\",\n \"type\": \"doctype\"\n \n },\n {\n \"label\": \"Employee Tax Exemption Sub Category\",\n \"name\": \"Employee Tax Exemption Sub Category\",\n \"type\": \"doctype\"\n \n }\n]" }, { "hidden": 0, @@ -38,7 +38,7 @@ "idx": 0, "is_standard": 1, "label": "Payroll", - "modified": "2020-06-19 12:23:06.034046", + "modified": "2020-08-10 19:38:45.976209", "modified_by": "Administrator", "module": "Payroll", "name": "Payroll", 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/payroll/doctype/employee_benefit_application/employee_benefit_application.py b/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py index d7d00e6480..ef844fbd3b 100644 --- a/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py +++ b/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py @@ -223,6 +223,7 @@ def get_benefit_amount_based_on_pro_rata(sal_struct, component_max_benefit): return benefit_amount @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def get_earning_components(doctype, txt, searchfield, start, page_len, filters): if len(filters) < 2: return {} diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py index ad9b6d86c8..30ea432678 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py @@ -90,7 +90,7 @@ class PayrollEntry(Document): cond = '' for f in ['company', 'branch', 'department', 'designation']: if self.get(f): - cond += " and t1." + f + " = '" + self.get(f).replace("'", "\'") + "'" + cond += " and t1." + f + " = " + frappe.db.escape(self.get(f)) return cond @@ -540,6 +540,7 @@ def submit_salary_slips_for_employees(payroll_entry, salary_slips, publish_progr frappe.msgprint(_("Could not submit some Salary Slips")) @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def get_payroll_entries_for_jv(doctype, txt, searchfield, start, page_len, filters): return frappe.db.sql(""" select name from `tabPayroll Entry` diff --git a/erpnext/payroll/doctype/salary_detail/salary_detail.json b/erpnext/payroll/doctype/salary_detail/salary_detail.json index adb54f26c6..cc87caeae1 100644 --- a/erpnext/payroll/doctype/salary_detail/salary_detail.json +++ b/erpnext/payroll/doctype/salary_detail/salary_detail.json @@ -7,27 +7,30 @@ "field_order": [ "salary_component", "abbr", - "statistical_component", "column_break_3", - "deduct_full_tax_on_selected_payroll_date", + "amount", + "section_break_5", + "additional_salary", + "statistical_component", "depends_on_payment_days", - "is_tax_applicable", "exempted_from_income_tax", + "is_tax_applicable", + "column_break_11", "is_flexible_benefit", "variable_based_on_taxable_salary", + "do_not_include_in_total", + "deduct_full_tax_on_selected_payroll_date", "section_break_2", "condition", + "column_break_18", "amount_based_on_formula", "formula", - "amount", - "do_not_include_in_total", + "section_break_19", "default_amount", "additional_amount", + "column_break_24", "tax_on_flexible_benefit", - "tax_on_additional_salary", - "section_break_11", - "additional_salary", - "condition_and_formula_help" + "tax_on_additional_salary" ], "fields": [ { @@ -110,9 +113,11 @@ "read_only": 1 }, { + "collapsible": 1, "depends_on": "eval:doc.is_flexible_benefit != 1", "fieldname": "section_break_2", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "label": "Condtion and formula" }, { "allow_on_submit": 1, @@ -181,23 +186,12 @@ "label": "Tax on additional salary", "read_only": 1 }, - { - "depends_on": "eval:doc.parenttype=='Salary Structure'", - "fieldname": "section_break_11", - "fieldtype": "Column Break" - }, - { - "depends_on": "eval:doc.parenttype=='Salary Structure'", - "fieldname": "condition_and_formula_help", - "fieldtype": "HTML", - "label": "Condition and Formula Help", - "options": "

Condition and Formula Help

\n\n

Notes:

\n\n
    \n
  1. Use field base for using base salary of the Employee
  2. \n
  3. Use Salary Component abbreviations in conditions and formulas. BS = Basic Salary
  4. \n
  5. Use field name for employee details in conditions and formulas. Employment Type = employment_typeBranch = branch
  6. \n
  7. Use field name from Salary Slip in conditions and formulas. Payment Days = payment_daysLeave without pay = leave_without_pay
  8. \n
  9. Direct Amount can also be entered based on Condtion. See example 3
\n\n

Examples

\n
    \n
  1. Calculating Basic Salary based on base\n
    Condition: base < 10000
    \n
    Formula: base * .2
  2. \n
  3. Calculating HRA based on Basic SalaryBS \n
    Condition: BS > 2000
    \n
    Formula: BS * .1
  4. \n
  5. Calculating TDS based on Employment Typeemployment_type \n
    Condition: employment_type==\"Intern\"
    \n
    Amount: 1000
  6. \n
" - }, { "fieldname": "additional_salary", "fieldtype": "Link", "label": "Additional Salary ", - "options": "Additional Salary" + "options": "Additional Salary", + "read_only": 1 }, { "default": "0", @@ -207,11 +201,43 @@ "fieldtype": "Check", "label": "Exempted from Income Tax", "read_only": 1 + }, + { + "collapsible": 1, + "fieldname": "section_break_5", + "fieldtype": "Section Break", + "label": "Component properties and references ", + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "column_break_11", + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "section_break_19", + "fieldtype": "Section Break", + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "column_break_18", + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "column_break_24", + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 } ], "istable": 1, "links": [], - "modified": "2020-06-22 23:21:26.300951", + "modified": "2020-07-01 12:13:41.956495", "modified_by": "Administrator", "module": "Payroll", "name": "Salary Detail", diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.js b/erpnext/payroll/doctype/salary_slip/salary_slip.js index 4b623e57b4..7b69dbe8d6 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.js +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.js @@ -123,13 +123,13 @@ frappe.ui.form.on("Salary Slip", { doc: frm.doc, callback: function(r, rt) { frm.refresh(); - if (frm.doc.absent_days){ + if (r.message){ frm.fields_dict.absent_days.set_description("Unmarked Days is treated as "+ r.message +". You can can change this in " + frappe.utils.get_form_link("Payroll Settings", "Payroll Settings", true)); } } }); } -}) +}); frappe.ui.form.on('Salary Slip Timesheet', { time_sheet: function(frm, dt, dn) { diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.json b/erpnext/payroll/doctype/salary_slip/salary_slip.json index 27a974ac83..619c45fa4a 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.json +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.json @@ -20,15 +20,17 @@ "company", "letter_head", "section_break_10", - "salary_slip_based_on_timesheet", "start_date", "end_date", "salary_structure", + "column_break_18", + "salary_slip_based_on_timesheet", "payroll_frequency", - "column_break_15", + "section_break_20", "total_working_days", "unmarked_days", "leave_without_pay", + "column_break_24", "absent_days", "payment_days", "hourly_wages", @@ -200,10 +202,6 @@ "fieldtype": "Date", "label": "End Date" }, - { - "fieldname": "column_break_15", - "fieldtype": "Column Break" - }, { "fieldname": "salary_structure", "fieldtype": "Link", @@ -490,13 +488,25 @@ "fieldtype": "Float", "hidden": 1, "label": "Unmarked days" + }, + { + "fieldname": "section_break_20", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_24", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_18", + "fieldtype": "Column Break" } ], "icon": "fa fa-file-text", "idx": 9, "is_submittable": 1, "links": [], - "modified": "2020-07-22 12:41:03.659422", + "modified": "2020-08-11 17:37:54.274384", "modified_by": "Administrator", "module": "Payroll", "name": "Salary Slip", 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/doctype/products_settings/products_settings.py b/erpnext/portal/doctype/products_settings/products_settings.py index b984aeb67d..ae7dc68020 100644 --- a/erpnext/portal/doctype/products_settings/products_settings.py +++ b/erpnext/portal/doctype/products_settings/products_settings.py @@ -11,9 +11,9 @@ from frappe.model.document import Document class ProductsSettings(Document): def validate(self): if self.home_page_is_products: - frappe.db.set_value("Website Settings", "home_page", "products") + frappe.db.set_value("Website Settings", None, "home_page", "products") elif frappe.db.get_single_value("Website Settings", "home_page") == 'products': - frappe.db.set_value("Website Settings", "home_page", "home") + frappe.db.set_value("Website Settings", None, "home_page", "home") self.validate_field_filters() self.validate_attribute_filters() diff --git a/erpnext/portal/product_configurator/utils.py b/erpnext/portal/product_configurator/utils.py index f8af30a1c3..9eef16bed3 100644 --- a/erpnext/portal/product_configurator/utils.py +++ b/erpnext/portal/product_configurator/utils.py @@ -239,7 +239,8 @@ def get_next_attribute_and_values(item_code, selected_attributes): if exact_match: data = get_product_info_for_website(exact_match[0]) product_info = data.product_info - product_info["allow_items_not_in_stock"] = cint(data.cart_settings.allow_items_not_in_stock) + if product_info: + product_info["allow_items_not_in_stock"] = cint(data.cart_settings.allow_items_not_in_stock) if not data.cart_settings.show_price: product_info = None else: diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py index 6350f86abb..5bbd29c4c4 100644 --- a/erpnext/projects/doctype/project/project.py +++ b/erpnext/projects/doctype/project/project.py @@ -239,6 +239,7 @@ def get_list_context(context=None): } @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def get_users_for_project(doctype, txt, searchfield, start, page_len, filters): conditions = [] return frappe.db.sql("""select name, concat_ws(' ', first_name, middle_name, last_name) diff --git a/erpnext/projects/doctype/task/task.py b/erpnext/projects/doctype/task/task.py index cf2fd26e57..fb84094ffe 100755 --- a/erpnext/projects/doctype/task/task.py +++ b/erpnext/projects/doctype/task/task.py @@ -193,6 +193,7 @@ def check_if_child_exists(name): @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def get_project(doctype, txt, searchfield, start, page_len, filters): from erpnext.controllers.queries import get_match_cond return frappe.db.sql(""" select name from `tabProject` diff --git a/erpnext/projects/doctype/timesheet/timesheet.js b/erpnext/projects/doctype/timesheet/timesheet.js index 5de2930c1c..607c3fd974 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.js +++ b/erpnext/projects/doctype/timesheet/timesheet.js @@ -94,13 +94,6 @@ frappe.ui.form.on("Timesheet", { } }, - company: function(frm) { - frappe.db.get_value('Company', { 'company_name' : frm.doc.company }, 'standard_working_hours') - .then(({ message }) => { - (frappe.working_hours = message.standard_working_hours || 0); - }); - }, - make_invoice: function(frm) { let dialog = new frappe.ui.Dialog({ title: __("Select Item (optional)"), diff --git a/erpnext/projects/doctype/timesheet/timesheet.py b/erpnext/projects/doctype/timesheet/timesheet.py index 7fe22bec4b..9e807f728e 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.py +++ b/erpnext/projects/doctype/timesheet/timesheet.py @@ -214,6 +214,7 @@ def get_projectwise_timesheet_data(project, parent=None): and sales_invoice is null""".format(cond), {'project': project, 'parent': parent}, as_dict=1) @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def get_timesheet(doctype, txt, searchfield, start, page_len, filters): if not filters: filters = {} diff --git a/erpnext/projects/utils.py b/erpnext/projects/utils.py index d0d88ebdf0..c39f908e43 100644 --- a/erpnext/projects/utils.py +++ b/erpnext/projects/utils.py @@ -7,6 +7,7 @@ from __future__ import unicode_literals import frappe @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def query_task(doctype, txt, searchfield, start, page_len, filters): from frappe.desk.reportview import build_match_conditions diff --git a/erpnext/public/js/communication.js b/erpnext/public/js/communication.js index 5316eb45b5..26e5ab8b32 100644 --- a/erpnext/public/js/communication.js +++ b/erpnext/public/js/communication.js @@ -7,13 +7,13 @@ 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")]), () => { frm.trigger('make_issue_from_communication'); }) - }, "Make"); + }, "Create"); } if(!in_list(["Lead", "Opportunity"], frm.doc.reference_doctype)) { @@ -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 405a33c72a..6951539026 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -163,9 +163,11 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ $.each(me.frm.doc["items"] || [], function(n, item) { var item_tax_map = me._load_item_tax_rate(item.item_tax_rate); var cumulated_tax_fraction = 0.0; - + var total_inclusive_tax_amount_per_qty = 0; $.each(me.frm.doc["taxes"] || [], function(i, tax) { - tax.tax_fraction_for_current_item = me.get_current_tax_fraction(tax, item_tax_map); + var current_tax_fraction = me.get_current_tax_fraction(tax, item_tax_map); + tax.tax_fraction_for_current_item = current_tax_fraction[0]; + var inclusive_tax_amount_per_qty = current_tax_fraction[1]; if(i==0) { tax.grand_total_fraction_for_current_item = 1 + tax.tax_fraction_for_current_item; @@ -176,10 +178,12 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ } cumulated_tax_fraction += tax.tax_fraction_for_current_item; + total_inclusive_tax_amount_per_qty += inclusive_tax_amount_per_qty * flt(item.qty); }); - if(cumulated_tax_fraction && !me.discount_amount_applied) { - item.net_amount = flt(item.amount / (1 + cumulated_tax_fraction)); + if(!me.discount_amount_applied && item.qty && (total_inclusive_tax_amount_per_qty || cumulated_tax_fraction)) { + var amount = flt(item.amount) - total_inclusive_tax_amount_per_qty; + item.net_amount = flt(amount / (1 + cumulated_tax_fraction)); item.net_rate = item.qty ? flt(item.net_amount / item.qty, precision("net_rate", item)) : 0; me.set_in_company_currency(item, ["net_rate", "net_amount"]); @@ -191,6 +195,7 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ // Get tax fraction for calculating tax exclusive amount // from tax inclusive amount var current_tax_fraction = 0.0; + var inclusive_tax_amount_per_qty = 0; if(cint(tax.included_in_print_rate)) { var tax_rate = this._get_tax_rate(tax, item_tax_map); @@ -205,13 +210,16 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ } else if(tax.charge_type == "On Previous Row Total") { current_tax_fraction = (tax_rate / 100.0) * this.frm.doc["taxes"][cint(tax.row_id) - 1].grand_total_fraction_for_current_item; + } else if (tax.charge_type == "On Item Quantity") { + inclusive_tax_amount_per_qty = flt(tax_rate); } } - if(tax.add_deduct_tax) { - current_tax_fraction *= (tax.add_deduct_tax == "Deduct") ? -1.0 : 1.0; + if(tax.add_deduct_tax && tax.add_deduct_tax == "Deduct") { + current_tax_fraction *= -1; + inclusive_tax_amount_per_qty *= -1; } - return current_tax_fraction; + return [current_tax_fraction, inclusive_tax_amount_per_qty]; }, _get_tax_rate: function(tax, item_tax_map) { @@ -360,8 +368,9 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ } else if(tax.charge_type == "On Previous Row Total") { current_tax_amount = (tax_rate / 100.0) * this.frm.doc["taxes"][cint(tax.row_id) - 1].grand_total_for_current_item; + } else if (tax.charge_type == "On Item Quantity") { + current_tax_amount = tax_rate * item.qty; } - this.set_item_wise_tax(item, tax, tax_rate, current_tax_amount); return current_tax_amount; @@ -573,7 +582,7 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ var actual_taxes_dict = {}; $.each(this.frm.doc["taxes"] || [], function(i, tax) { - if (tax.charge_type == "Actual") { + if (in_list(["Actual", "On Item Quantity"], tax.charge_type)) { var tax_amount = (tax.category == "Valuation") ? 0.0 : tax.tax_amount; tax_amount *= (tax.add_deduct_tax == "Deduct") ? -1.0 : 1.0; actual_taxes_dict[tax.idx] = tax_amount; @@ -586,7 +595,7 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ $.each(actual_taxes_dict, function(key, value) { if (value) total_actual_tax += value; }); - + return flt(this.frm.doc.grand_total - total_actual_tax, precision("grand_total")); } }, diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 4e50f3d7f6..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); } @@ -1821,7 +1834,6 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ }, set_query_for_item_tax_template: function(doc, cdt, cdn) { - var item = frappe.get_doc(cdt, cdn); if(!item.item_code) { frappe.throw(__("Please enter Item Code to get item taxes")); @@ -1829,7 +1841,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ let filters = { 'item_code': item.item_code, - 'valid_from': doc.transaction_date || doc.bill_date || doc.posting_date, + 'valid_from': ["<=", doc.transaction_date || doc.bill_date || doc.posting_date], 'item_group': item.item_group, } 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/public/less/website.less b/erpnext/public/less/website.less index 57a0a332a9..ac878de105 100644 --- a/erpnext/public/less/website.less +++ b/erpnext/public/less/website.less @@ -297,6 +297,10 @@ margin-top: 30px; } +.item-group-slideshow { + margin-bottom: 1rem; +} + .product-image-img { border: 1px solid @light-border-color; border-radius: 3px; diff --git a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py index 2d306ba172..787d557e80 100644 --- a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py +++ b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py @@ -234,9 +234,6 @@ class GSTR3BReport(Document): self.report_dict[supply_type][supply_category][account_map.get(account_type)] += \ flt(tax_details.get((account_name, gst_category), {}).get("amount"), 2) - for k, v in iteritems(account_map): - txval -= self.report_dict.get(supply_type, {}).get(supply_category, {}).get(v, 0) - self.report_dict[supply_type][supply_category]["txval"] += flt(txval, 2) def set_inter_state_supply(self, inter_state_supply): @@ -256,7 +253,7 @@ class GSTR3BReport(Document): def get_total_taxable_value(self, doctype, reverse_charge): return frappe._dict(frappe.db.sql(""" - select gst_category, sum(base_grand_total) as total + select gst_category, sum(net_total) as total from `tab{doctype}` where docstatus = 1 and month(posting_date) = %s and year(posting_date) = %s and reverse_charge = %s @@ -309,26 +306,27 @@ class GSTR3BReport(Document): inter_state_supply_tax_mapping.setdefault(d.name, { 'place_of_supply': d.place_of_supply, 'taxable_value': d.net_total, + 'gst_category': d.gst_category, 'camt': 0.0, 'samt': 0.0, 'iamt': 0.0, 'csamt': 0.0 }) - if d.account_head in [d.cgst_account for d in self.account_heads]: + if d.account_head in [a.cgst_account for a in self.account_heads]: inter_state_supply_tax_mapping[d.name]['camt'] += d.tax_amount - if d.account_head in [d.sgst_account for d in self.account_heads]: + if d.account_head in [a.sgst_account for a in self.account_heads]: inter_state_supply_tax_mapping[d.name]['samt'] += d.tax_amount - if d.account_head in [d.igst_account for d in self.account_heads]: + if d.account_head in [a.igst_account for a in self.account_heads]: inter_state_supply_tax_mapping[d.name]['iamt'] += d.tax_amount - if d.account_head in [d.cess_account for d in self.account_heads]: + if d.account_head in [a.cess_account for a in self.account_heads]: inter_state_supply_tax_mapping[d.name]['csamt'] += d.tax_amount for key, value in iteritems(inter_state_supply_tax_mapping): - if d.place_of_supply: + if value.get('place_of_supply'): osup_det = self.report_dict["sup_details"]["osup_det"] osup_det["txval"] = flt(osup_det["txval"] + value['taxable_value'], 2) osup_det["iamt"] = flt(osup_det["iamt"] + value['iamt'], 2) @@ -336,15 +334,15 @@ class GSTR3BReport(Document): osup_det["samt"] = flt(osup_det["samt"] + value['samt'], 2) osup_det["csamt"] = flt(osup_det["csamt"] + value['csamt'], 2) - if state_number != d.place_of_supply.split("-")[0]: - inter_state_supply_details.setdefault((d.gst_category, d.place_of_supply), { + if state_number != value.get('place_of_supply').split("-")[0]: + inter_state_supply_details.setdefault((value.get('gst_category'), value.get('place_of_supply')), { "txval": 0.0, - "pos": d.place_of_supply.split("-")[0], + "pos": value.get('place_of_supply').split("-")[0], "iamt": 0.0 }) - inter_state_supply_details[(d.gst_category, d.place_of_supply)]['txval'] += value['taxable_value'] - inter_state_supply_details[(d.gst_category, d.place_of_supply)]['iamt'] += value['iamt'] + inter_state_supply_details[(value.get('gst_category'), value.get('place_of_supply'))]['txval'] += value['taxable_value'] + inter_state_supply_details[(value.get('gst_category'), value.get('place_of_supply'))]['iamt'] += value['iamt'] return inter_state_supply_details diff --git a/erpnext/regional/india/__init__.py b/erpnext/regional/india/__init__.py index 0ed98b74ee..d6221a80aa 100644 --- a/erpnext/regional/india/__init__.py +++ b/erpnext/regional/india/__init__.py @@ -10,8 +10,7 @@ states = [ 'Bihar', 'Chandigarh', 'Chhattisgarh', - 'Dadra and Nagar Haveli', - 'Daman and Diu', + 'Dadra and Nagar Haveli and Daman and Diu', 'Delhi', 'Goa', 'Gujarat', @@ -50,8 +49,7 @@ state_numbers = { "Bihar": "10", "Chandigarh": "04", "Chhattisgarh": "22", - "Dadra and Nagar Haveli": "26", - "Daman and Diu": "25", + "Dadra and Nagar Haveli and Daman and Diu": "26", "Delhi": "07", "Goa": "30", "Gujarat": "24", diff --git a/erpnext/regional/india/gst_state_code_data.json b/erpnext/regional/india/gst_state_code_data.json index 6dab81d668..ff88e0f9d6 100644 --- a/erpnext/regional/india/gst_state_code_data.json +++ b/erpnext/regional/india/gst_state_code_data.json @@ -134,15 +134,10 @@ "state_code": "DL", "state_name": "Delhi" }, - { - "state_number": "25", - "state_code": "DD", - "state_name": "Daman and Diu" - }, { "state_number": "26", "state_code": "DN", - "state_name": "Dadra and Nagar Haveli" + "state_name": "Dadra and Nagar Haveli and Daman and Diu" }, { "state_number": "22", 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 4d36cff1e6..3b6a28f52c 100644 --- a/erpnext/regional/india/taxes.js +++ b/erpnext/regional/india/taxes.js @@ -6,10 +6,15 @@ 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'); }, get_tax_template: function(frm) { + if (!frm.doc.company) return; + let party_details = { 'shipping_address': frm.doc.shipping_address || '', 'shipping_address_name': frm.doc.shipping_address_name || '', diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index fe7e0c807c..69e47a43c4 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals import frappe, re, json from frappe import _ +import erpnext from frappe.utils import cstr, flt, date_diff, nowdate, round_based_on_smallest_currency_fraction, money_in_words from erpnext.regional.india import states, state_numbers from erpnext.controllers.taxes_and_totals import get_itemised_tax, get_itemised_taxable_amount @@ -673,25 +674,34 @@ 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') \ + gst_accounts.get('igst_account') + base_gst_tax = 0 gst_tax = 0 + for tax in doc.get('taxes'): if tax.category not in ("Total", "Valuation and Total"): continue if flt(tax.base_tax_amount_after_discount_amount) and tax.account_head in gst_account_list: - gst_tax += tax.base_tax_amount_after_discount_amount + base_gst_tax += tax.base_tax_amount_after_discount_amount + gst_tax += tax.tax_amount_after_discount_amount doc.taxes_and_charges_added -= gst_tax doc.total_taxes_and_charges -= gst_tax + doc.base_taxes_and_charges_added -= base_gst_tax + doc.base_total_taxes_and_charges -= base_gst_tax - update_totals(gst_tax, doc) + update_totals(gst_tax, base_gst_tax, doc) -def update_totals(gst_tax, doc): +def update_totals(gst_tax, base_gst_tax, doc): + doc.base_grand_total -= base_gst_tax doc.grand_total -= gst_tax if doc.meta.get_field("rounded_total"): @@ -707,13 +717,14 @@ def update_totals(gst_tax, doc): doc.outstanding_amount = doc.rounded_total or doc.grand_total doc.in_words = money_in_words(doc.grand_total, doc.currency) + doc.base_in_words = money_in_words(doc.base_grand_total, erpnext.get_company_currency(doc.company)) doc.set_payment_schedule() 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) @@ -724,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) @@ -733,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_1/gstr_1.py b/erpnext/regional/report/gstr_1/gstr_1.py index 8885b88c2a..282efe4790 100644 --- a/erpnext/regional/report/gstr_1/gstr_1.py +++ b/erpnext/regional/report/gstr_1/gstr_1.py @@ -131,6 +131,9 @@ class Gstr1Report(object): taxable_value += abs(net_amount) elif tax_rate: taxable_value += abs(net_amount) + elif not tax_rate and self.filters.get('type_of_business') == 'EXPORT' \ + and invoice_details.get('export_type') == "Without Payment of Tax": + taxable_value += abs(net_amount) row += [tax_rate or 0, taxable_value] 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 222dfa1eb7..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,9 +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) @@ -21,21 +25,24 @@ def _execute(filters=None): itemised_tax, tax_columns = get_tax_accounts(item_list, columns, company_currency) data = [] + added_item = [] for d in item_list: - row = [d.gst_hsn_code, d.description, d.stock_uom, d.stock_qty] - total_tax = 0 - for tax in tax_columns: - item_tax = itemised_tax.get(d.name, {}).get(tax, {}) - total_tax += flt(item_tax.get("tax_amount")) + if (d.parent, d.item_code) not in added_item: + row = [d.gst_hsn_code, d.description, d.stock_uom, d.stock_qty] + total_tax = 0 + for tax in tax_columns: + item_tax = itemised_tax.get((d.parent, d.item_code), {}).get(tax, {}) + total_tax += flt(item_tax.get("tax_amount", 0)) - row += [d.base_net_amount + total_tax] - row += [d.base_net_amount] + row += [d.base_net_amount + total_tax] + row += [d.base_net_amount] - for tax in tax_columns: - item_tax = itemised_tax.get(d.name, {}).get(tax, {}) - row += [item_tax.get("tax_amount", 0)] + for tax in tax_columns: + item_tax = itemised_tax.get((d.parent, d.item_code), {}).get(tax, {}) + row += [item_tax.get("tax_amount", 0)] - data.append(row) + data.append(row) + added_item.append((d.parent, d.item_code)) if data: data = get_merged_data(columns, data) # merge same hsn code data return columns, data @@ -103,7 +110,7 @@ def get_items(filters): match_conditions = " and {0} ".format(match_conditions) - return frappe.db.sql(""" + items = frappe.db.sql(""" select `tabSales Invoice Item`.name, `tabSales Invoice Item`.base_price_list_rate, `tabSales Invoice Item`.gst_hsn_code, `tabSales Invoice Item`.stock_qty, @@ -118,10 +125,9 @@ def get_items(filters): """ % (conditions, match_conditions), filters, as_dict=1) + return items -def get_tax_accounts(item_list, columns, company_currency, - doctype="Sales Invoice", tax_doctype="Sales Taxes and Charges"): - import json +def get_tax_accounts(item_list, columns, company_currency, doctype="Sales Invoice", tax_doctype="Sales Taxes and Charges"): item_row_map = {} tax_columns = [] invoice_item_row = {} @@ -137,7 +143,7 @@ def get_tax_accounts(item_list, columns, company_currency, 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 @@ -149,11 +155,11 @@ def get_tax_accounts(item_list, columns, company_currency, """ % (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: @@ -171,50 +177,113 @@ def get_tax_accounts(item_list, columns, company_currency, for d in item_row_map.get(parent, {}).get(item_code, []): item_tax_amount = tax_amount if item_tax_amount: - itemised_tax.setdefault(d.name, {})[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: - columns.append(desc + " Amount:Currency/currency:160") + for account_head in tax_columns: + columns.append({ + "label": account_head, + "fieldname": frappe.scrub(account_head), + "fieldtype": "Float", + "width": 110 + }) - # columns += ["Total Amount:Currency/currency:110"] return itemised_tax, tax_columns def get_merged_data(columns, data): merged_hsn_dict = {} # to group same hsn under one key and perform row addition - add_column_index = [] # store index of columns that needs to be added - tax_col = len(get_columns()) - fields_to_merge = ["stock_qty", "total_amount", "taxable_amount"] # columns for which index needs to be found - - for i,d in enumerate(columns): - # check if fieldname in to_merge list and ignore tax-columns - if i < tax_col and d["fieldname"] in fields_to_merge: - add_column_index.append(i) + result = [] for row in data: - if row[0] in merged_hsn_dict: - to_add_row = merged_hsn_dict.get(row[0]) + merged_hsn_dict.setdefault(row[0], {}) + for i, d in enumerate(columns): + if d['fieldtype'] not in ('Int', 'Float', 'Currency'): + merged_hsn_dict[row[0]][d['fieldname']] = row[i] + else: + if merged_hsn_dict.get(row[0], {}).get(d['fieldname'], ''): + merged_hsn_dict[row[0]][d['fieldname']] += row[i] + else: + merged_hsn_dict[row[0]][d['fieldname']] = row[i] - # add columns from the add_column_index table - for k in add_column_index: - to_add_row[k] += row[k] + for key, value in iteritems(merged_hsn_dict): + result.append(value) - # add tax columns - for k in range(len(columns)): - if tax_col <= k < len(columns): - to_add_row[k] += row[k] + return result - # update hsn dict with the newly added data - merged_hsn_dict[row[0]] = to_add_row - else: - merged_hsn_dict[row[0]] = row +@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"]) - # extract data rows to be displayed in report - data = [merged_hsn_dict[d] for d in merged_hsn_dict] + 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/selling/desk_page/retail/retail.json b/erpnext/selling/desk_page/retail/retail.json index 7b30af20cc..581e14cf81 100644 --- a/erpnext/selling/desk_page/retail/retail.json +++ b/erpnext/selling/desk_page/retail/retail.json @@ -3,7 +3,7 @@ { "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]" + "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\": \"Point of Sale\",\n \"name\": \"point-of-sale\",\n \"onboard\": 1,\n \"type\": \"page\"\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\": \"Cashier Closing\",\n \"label\": \"Cashier Closing\",\n \"name\": \"Cashier Closing\",\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]" } ], "category": "Domains", @@ -14,10 +14,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-08-20 18:00:07.515691", "modified_by": "Administrator", "module": "Selling", "name": "Retail", @@ -25,5 +26,27 @@ "pin_to_bottom": 0, "pin_to_top": 0, "restrict_to_domain": "Retail", - "shortcuts": [] + "shortcuts": [ + { + "color": "#9deca2", + "doc_view": "", + "format": "{} Active", + "label": "Point of Sale Profile", + "link_to": "POS Profile", + "stats_filter": "{\n \"disabled\": 0\n}", + "type": "DocType" + }, + { + "doc_view": "", + "label": "Point of Sale", + "link_to": "point-of-sale", + "type": "Page" + }, + { + "doc_view": "", + "label": "POS Settings", + "link_to": "POS Settings", + "type": "DocType" + } + ] } \ No newline at end of file diff --git a/erpnext/selling/desk_page/selling/selling.json b/erpnext/selling/desk_page/selling/selling.json index 225238233a..4c09ee94e0 100644 --- a/erpnext/selling/desk_page/selling/selling.json +++ b/erpnext/selling/desk_page/selling/selling.json @@ -18,7 +18,7 @@ { "hidden": 0, "label": "Key Reports", - "links": "[\n {\n \"dependencies\": [\n \"Sales Order\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Sales Analytics\",\n \"name\": \"Sales Analytics\",\n \"onboard\": 1,\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 \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"icon\": \"fa fa-bar-chart\",\n \"label\": \"Sales Funnel\",\n \"name\": \"sales-funnel\",\n \"onboard\": 1,\n \"type\": \"page\"\n },\n {\n \"dependencies\": [\n \"Sales Order\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Sales Order Trends\",\n \"name\": \"Sales Order Trends\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Quotation\"\n ],\n \"doctype\": \"Quotation\",\n \"is_query_report\": true,\n \"label\": \"Quotation Trends\",\n \"name\": \"Quotation Trends\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Customer\"\n ],\n \"doctype\": \"Customer\",\n \"icon\": \"fa fa-bar-chart\",\n \"is_query_report\": true,\n \"label\": \"Customer Acquisition and Loyalty\",\n \"name\": \"Customer Acquisition and Loyalty\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Order\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Inactive Customers\",\n \"name\": \"Inactive Customers\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Order\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Sales Person-wise Transaction Summary\",\n \"name\": \"Sales Person-wise Transaction Summary\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"doctype\": \"Item\",\n \"is_query_report\": true,\n \"label\": \"Item-wise Sales History\",\n \"name\": \"Item-wise Sales History\",\n \"type\": \"report\"\n }\n]" + "links": "[\n {\n \n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Sales Analytics\",\n \"name\": \"Sales Analytics\",\n \"onboard\": 1,\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 \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"icon\": \"fa fa-bar-chart\",\n \"label\": \"Sales Funnel\",\n \"name\": \"sales-funnel\",\n \"onboard\": 1,\n \"type\": \"page\"\n },\n {\n \"dependencies\": [\n \"Sales Order\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Sales Order Trends\",\n \"name\": \"Sales Order Trends\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Quotation\"\n ],\n \"doctype\": \"Quotation\",\n \"is_query_report\": true,\n \"label\": \"Quotation Trends\",\n \"name\": \"Quotation Trends\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Customer\"\n ],\n \"doctype\": \"Customer\",\n \"icon\": \"fa fa-bar-chart\",\n \"is_query_report\": true,\n \"label\": \"Customer Acquisition and Loyalty\",\n \"name\": \"Customer Acquisition and Loyalty\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Order\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Inactive Customers\",\n \"name\": \"Inactive Customers\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Order\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Sales Person-wise Transaction Summary\",\n \"name\": \"Sales Person-wise Transaction Summary\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"doctype\": \"Item\",\n \"is_query_report\": true,\n \"label\": \"Item-wise Sales History\",\n \"name\": \"Item-wise Sales History\",\n \"type\": \"report\"\n }\n]" }, { "hidden": 0, @@ -44,7 +44,7 @@ "idx": 0, "is_standard": 1, "label": "Selling", - "modified": "2020-06-29 19:26:35.139097", + "modified": "2020-08-15 10:12:53.131621", "modified_by": "Administrator", "module": "Selling", "name": "Selling", diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index e614acdb82..1f955fcd52 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -185,6 +185,14 @@ class Customer(TransactionBase): 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: + return + company_record = [] for limit in self.credit_limits: if limit.company in company_record: @@ -340,6 +348,7 @@ def get_loyalty_programs(doc): return lp_details @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def get_customer_list(doctype, txt, searchfield, start, page_len, filters=None): from erpnext.controllers.queries import get_fields fields = ["name", "customer_name", "customer_group", "territory"] @@ -387,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 @@ -418,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 @@ -542,6 +550,7 @@ def make_address(args, is_primary_address=1): return address @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def get_customer_primary_contact(doctype, txt, searchfield, start, page_len, filters): customer = filters.get('customer') return frappe.db.sql(""" diff --git a/erpnext/selling/doctype/customer/customer_dashboard.py b/erpnext/selling/doctype/customer/customer_dashboard.py index 22e30e3113..532c11b86e 100644 --- a/erpnext/selling/doctype/customer/customer_dashboard.py +++ b/erpnext/selling/doctype/customer/customer_dashboard.py @@ -12,7 +12,8 @@ def get_data(): 'Payment Entry': 'party', 'Quotation': 'party_name', 'Opportunity': 'party_name', - 'Bank Account': 'party' + 'Bank Account': 'party', + 'Subscription': 'party' }, 'dynamic_links': { 'party_name': ['Customer', 'quotation_to'] @@ -32,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/lead_source/lead_source.json b/erpnext/selling/doctype/lead_source/lead_source.json index 868f6d11d0..373e83af9c 100644 --- a/erpnext/selling/doctype/lead_source/lead_source.json +++ b/erpnext/selling/doctype/lead_source/lead_source.json @@ -1,7 +1,7 @@ { "allow_copy": 0, "allow_import": 0, - "allow_rename": 0, + "allow_rename": 1, "autoname": "field:source_name", "beta": 0, "creation": "2016-09-16 01:47:47.382372", @@ -74,7 +74,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2016-09-16 02:03:01.441622", + "modified": "2020-09-16 02:03:01.441622", "modified_by": "Administrator", "module": "Selling", "name": "Lead Source", @@ -128,4 +128,4 @@ "sort_field": "modified", "sort_order": "DESC", "track_seen": 0 -} \ No newline at end of file +} diff --git a/erpnext/selling/doctype/product_bundle/product_bundle.py b/erpnext/selling/doctype/product_bundle/product_bundle.py index 0c85a1b53c..d3281f733f 100644 --- a/erpnext/selling/doctype/product_bundle/product_bundle.py +++ b/erpnext/selling/doctype/product_bundle/product_bundle.py @@ -29,6 +29,7 @@ class ProductBundle(Document): frappe.throw(_("Row #{0}: Child Item should not be a Product Bundle. Please remove Item {1} and Save").format(item.idx, frappe.bold(item.item_code))) @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def get_new_item_code(doctype, txt, searchfield, start, page_len, filters): from erpnext.controllers.queries import get_match_cond diff --git a/erpnext/selling/doctype/quotation/quotation.json b/erpnext/selling/doctype/quotation/quotation.json index 6d34c2ac7f..5b85187ccb 100644 --- a/erpnext/selling/doctype/quotation/quotation.json +++ b/erpnext/selling/doctype/quotation/quotation.json @@ -923,7 +923,7 @@ "fieldname": "lost_reasons", "fieldtype": "Table MultiSelect", "label": "Lost Reasons", - "options": "Lost Reason Detail", + "options": "Quotation Lost Reason Detail", "read_only": 1 } ], @@ -932,7 +932,7 @@ "is_submittable": 1, "links": [], "max_attachments": 1, - "modified": "2020-07-18 04:59:09.960118", + "modified": "2020-07-26 17:46:19.951223", "modified_by": "Administrator", "module": "Selling", "name": "Quotation", diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index 449a968a4f..20ae19f5db 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -68,7 +68,7 @@ class Quotation(SellingController): def declare_enquiry_lost(self, lost_reasons_list, detailed_reason=None): if not self.has_sales_order(): - get_lost_reasons = frappe.get_list('Opportunity Lost Reason', + get_lost_reasons = frappe.get_list('Quotation Lost Reason', fields = ["name"]) lost_reasons_lst = [reason.get('name') for reason in get_lost_reasons] frappe.db.set(self, 'status', 'Lost') @@ -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.py b/erpnext/selling/doctype/sales_order/sales_order.py index ffb66354fa..f88289871e 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -888,6 +888,7 @@ def make_purchase_order(source_name, for_supplier=None, selected_items=[], targe @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def get_supplier(doctype, txt, searchfield, start, page_len, filters): supp_master_name = frappe.defaults.get_user_default("supp_master_name") if supp_master_name == "Supplier Name": 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/page/point_of_sale/point_of_sale.py b/erpnext/selling/page/point_of_sale/point_of_sale.py index f7b7ed8b89..83bd71d5f3 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.py +++ b/erpnext/selling/page/point_of_sale/point_of_sale.py @@ -14,10 +14,9 @@ from six import string_types def get_items(start, page_length, price_list, item_group, search_value="", pos_profile=None): data = dict() warehouse = "" - display_items_in_stock = 0 if pos_profile: - warehouse, display_items_in_stock = frappe.db.get_value('POS Profile', pos_profile, ['warehouse', 'display_items_in_stock']) + warehouse = frappe.db.get_value('POS Profile', pos_profile, ['warehouse']) if not frappe.db.exists('Item Group', item_group): item_group = get_root_of('Item Group') @@ -85,7 +84,7 @@ def get_items(start, page_length, price_list, item_group, search_value="", pos_p item_price = item_prices.get(item_code) or {} item_stock_qty = get_stock_availability(item_code, warehouse) - if display_items_in_stock and not item_stock_qty: + if not item_stock_qty: pass else: row = {} @@ -160,6 +159,7 @@ def get_item_group_condition(pos_profile): return cond % tuple(item_groups) @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def item_group_query(doctype, txt, searchfield, start, page_len, filters): item_groups = [] cond = "1=1" @@ -179,12 +179,12 @@ def item_group_query(doctype, txt, searchfield, start, page_len, filters): @frappe.whitelist() def check_opening_entry(user): - open_vouchers = frappe.db.get_all("POS Opening Entry", - filters = { - "user": user, + open_vouchers = frappe.db.get_all("POS Opening Entry", + filters = { + "user": user, "pos_closing_entry": ["in", ["", None]], "docstatus": 1 - }, + }, fields = ["name", "company", "pos_profile", "period_start_date"], order_by = "period_start_date desc" ) @@ -229,7 +229,7 @@ def get_past_order_list(search_term, status, limit=20): invoice_list = frappe.db.get_all('POS Invoice', filters={ 'status': status }, fields=fields) - + return invoice_list @frappe.whitelist() @@ -244,7 +244,7 @@ def set_customer_info(fieldname, customer, value=""): if fieldname == 'email_id': contact_doc.set('email_ids', [{ 'email_id': value, 'is_primary': 1}]) frappe.db.set_value('Customer', customer, 'email_id', value) - elif fieldname == 'mobile_no': + elif fieldname == 'mobile_no': contact_doc.set('phone_nos', [{ 'phone': value, 'is_primary_mobile_no': 1}]) frappe.db.set_value('Customer', customer, 'mobile_no', value) contact_doc.save() \ 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 483ef78d64..ae5471b900 100644 --- a/erpnext/selling/page/point_of_sale/pos_controller.js +++ b/erpnext/selling/page/point_of_sale/pos_controller.js @@ -35,7 +35,8 @@ erpnext.PointOfSale.Controller = class { create_opening_voucher() { 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", in_list_view: 1, label: "Opening Amount", options: "company:company_currency", reqd: 1 } + { fieldname: "opening_amount", fieldtype: "Currency", default: 0, in_list_view: 1, label: "Opening Amount", + options: "company:company_currency", reqd: 1 } ]; const dialog = new frappe.ui.Dialog({ @@ -66,7 +67,7 @@ erpnext.PointOfSale.Controller = class { 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, closing_amount } = pay; + const { mode_of_payment } = pay; dialog.fields_dict.balance_details.df.data.push({ mode_of_payment: mode_of_payment }); @@ -152,7 +153,7 @@ erpnext.PointOfSale.Controller = class { }, () => this.make_new_invoice(), () => frappe.dom.unfreeze(), - () => this.page.set_title(__('Point of Sale Beta')), + () => this.page.set_title(__('Point of Sale')), ]); } 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..eadeb8fde8 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_cart.js +++ b/erpnext/selling/page/point_of_sale/pos_item_cart.js @@ -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); } }, @@ -948,4 +948,4 @@ erpnext.PointOfSale.ItemCart = class { 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_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_order_analysis/sales_order_analysis.py b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py index 7e8e6e9e8b..f5feb95f1a 100644 --- a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py +++ b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py @@ -96,7 +96,7 @@ def prepare_data(data, filters): # prepare data for report view row["qty_to_bill"] = flt(row["qty"]) - flt(row["billed_qty"]) - row["delay"] = 0 if row["delay"] < 0 else row["delay"] + row["delay"] = 0 if row["delay"] and row["delay"] < 0 else row["delay"] if filters.get("group_by_so"): so_name = row["sales_order"] 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/selling/sales_common.js b/erpnext/selling/sales_common.js index 333a563aa5..002cfe41e1 100644 --- a/erpnext/selling/sales_common.js +++ b/erpnext/selling/sales_common.js @@ -494,13 +494,18 @@ frappe.ui.form.on(cur_frm.doctype, { var dialog = new frappe.ui.Dialog({ title: __("Set as Lost"), fields: [ - {"fieldtype": "Table MultiSelect", - "label": __("Lost Reasons"), - "fieldname": "lost_reason", - "options": "Lost Reason Detail", - "reqd": 1}, - - {"fieldtype": "Text", "label": __("Detailed Reason"), "fieldname": "detailed_reason"}, + { + "fieldtype": "Table MultiSelect", + "label": __("Lost Reasons"), + "fieldname": "lost_reason", + "options": frm.doctype === 'Opportunity' ? 'Opportunity Lost Reason Detail': 'Quotation Lost Reason Detail', + "reqd": 1 + }, + { + "fieldtype": "Text", + "label": __("Detailed Reason"), + "fieldname": "detailed_reason" + }, ], primary_action: function() { var values = dialog.get_values(); diff --git a/erpnext/setup/doctype/company/company.js b/erpnext/setup/doctype/company/company.js index 7ae5385a23..f882db60c5 100644 --- a/erpnext/setup/doctype/company/company.js +++ b/erpnext/setup/doctype/company/company.js @@ -34,6 +34,16 @@ frappe.ui.form.on("Company", { frm.set_query("default_buying_terms", function() { return { filters: { buying: 1 } }; }); + + frm.set_query("default_in_transit_warehouse", function() { + return { + filters:{ + 'warehouse_type' : 'Transit', + 'is_group': 0, + 'company': frm.doc.company + } + }; + }); }, company_name: function(frm) { diff --git a/erpnext/setup/doctype/company/company.json b/erpnext/setup/doctype/company/company.json index 221044df3a..4a26a71970 100644 --- a/erpnext/setup/doctype/company/company.json +++ b/erpnext/setup/doctype/company/company.json @@ -25,6 +25,7 @@ "default_selling_terms", "default_buying_terms", "default_warehouse_for_sales_return", + "default_in_transit_warehouse", "column_break_10", "country", "create_chart_of_accounts_based_on", @@ -242,7 +243,7 @@ { "fieldname": "default_warehouse_for_sales_return", "fieldtype": "Link", - "label": "Default warehouse for Sales Return", + "label": "Default Warehouse for Sales Return", "options": "Warehouse" }, { @@ -733,6 +734,12 @@ "fieldname": "enable_perpetual_inventory_for_non_stock_items", "fieldtype": "Check", "label": "Enable Perpetual Inventory For Non Stock Items" + }, + { + "fieldname": "default_in_transit_warehouse", + "fieldtype": "Link", + "label": "Default In Transit Warehouse", + "options": "Warehouse" } ], "icon": "fa fa-building", @@ -740,7 +747,7 @@ "image_field": "company_logo", "is_tree": 1, "links": [], - "modified": "2020-06-24 12:45:31.462195", + "modified": "2020-08-06 00:38:08.311216", "modified_by": "Administrator", "module": "Setup", "name": "Company", @@ -801,4 +808,4 @@ "sort_field": "modified", "sort_order": "ASC", "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index 47b41a97ad..8e707fe3f4 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -140,7 +140,8 @@ class Company(NestedSet): {"warehouse_name": _("All Warehouses"), "is_group": 1}, {"warehouse_name": _("Stores"), "is_group": 0}, {"warehouse_name": _("Work In Progress"), "is_group": 0}, - {"warehouse_name": _("Finished Goods"), "is_group": 0}]: + {"warehouse_name": _("Finished Goods"), "is_group": 0}, + {"warehouse_name": _("Goods In Transit"), "is_group": 0, "warehouse_type": "Transit"}]: if not frappe.db.exists("Warehouse", "{0} - {1}".format(wh_detail["warehouse_name"], self.abbr)): warehouse = frappe.get_doc({ @@ -149,7 +150,8 @@ class Company(NestedSet): "is_group": wh_detail["is_group"], "company": self.name, "parent_warehouse": "{0} - {1}".format(_("All Warehouses"), self.abbr) \ - if not wh_detail["is_group"] else "" + if not wh_detail["is_group"] else "", + "warehouse_type" : wh_detail["warehouse_type"] if "warehouse_type" in wh_detail else None }) warehouse.flags.ignore_permissions = True warehouse.flags.ignore_mandatory = True 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/doctype/party_type/party_type.py b/erpnext/setup/doctype/party_type/party_type.py index b29c305ee7..96e60936a4 100644 --- a/erpnext/setup/doctype/party_type/party_type.py +++ b/erpnext/setup/doctype/party_type/party_type.py @@ -10,6 +10,7 @@ class PartyType(Document): pass @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def get_party_type(doctype, txt, searchfield, start, page_len, filters): cond = '' if filters and filters.get('account'): diff --git a/erpnext/setup/doctype/quotation_lost_reason_detail/__init__.py b/erpnext/setup/doctype/quotation_lost_reason_detail/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/setup/doctype/quotation_lost_reason_detail/quotation_lost_reason_detail.json b/erpnext/setup/doctype/quotation_lost_reason_detail/quotation_lost_reason_detail.json new file mode 100644 index 0000000000..543214101a --- /dev/null +++ b/erpnext/setup/doctype/quotation_lost_reason_detail/quotation_lost_reason_detail.json @@ -0,0 +1,31 @@ +{ + "actions": [], + "creation": "2020-07-14 09:21:44.057724", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "lost_reason" + ], + "fields": [ + { + "fieldname": "lost_reason", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Quotation Lost Reason", + "options": "Quotation Lost Reason" + } + ], + "istable": 1, + "links": [], + "modified": "2020-07-26 17:58:56.373775", + "modified_by": "Administrator", + "module": "Setup", + "name": "Quotation Lost Reason Detail", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/setup/doctype/quotation_lost_reason_detail/quotation_lost_reason_detail.py b/erpnext/setup/doctype/quotation_lost_reason_detail/quotation_lost_reason_detail.py new file mode 100644 index 0000000000..7bb8d02670 --- /dev/null +++ b/erpnext/setup/doctype/quotation_lost_reason_detail/quotation_lost_reason_detail.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class QuotationLostReasonDetail(Document): + pass 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/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py index ad063cfc9d..72ed00293e 100644 --- a/erpnext/setup/setup_wizard/operations/install_fixtures.py +++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py @@ -95,8 +95,6 @@ def install(country=None): {'doctype': 'Stock Entry Type', 'name': 'Send to Subcontractor', 'purpose': 'Send to Subcontractor'}, {'doctype': 'Stock Entry Type', 'name': 'Material Transfer for Manufacture', 'purpose': 'Material Transfer for Manufacture'}, {'doctype': 'Stock Entry Type', 'name': 'Material Consumption for Manufacture', 'purpose': 'Material Consumption for Manufacture'}, - {'doctype': 'Stock Entry Type', 'name': 'Send to Warehouse', 'purpose': 'Send to Warehouse'}, - {'doctype': 'Stock Entry Type', 'name': 'Receive at Warehouse', 'purpose': 'Receive at Warehouse'}, # Designation {'doctype': 'Designation', 'designation_name': _('CEO')}, @@ -244,7 +242,10 @@ def install(country=None): {"doctype": "Sales Stage", "stage_name": _("Identifying Decision Makers")}, {"doctype": "Sales Stage", "stage_name": _("Perception Analysis")}, {"doctype": "Sales Stage", "stage_name": _("Proposal/Price Quote")}, - {"doctype": "Sales Stage", "stage_name": _("Negotiation/Review")} + {"doctype": "Sales Stage", "stage_name": _("Negotiation/Review")}, + + # Warehouse Type + {'doctype': 'Warehouse Type', 'name': 'Transit'}, ] from erpnext.setup.setup_wizard.data.industry_type import get_industry_types diff --git a/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.js b/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.js index 14500ba6b3..21fa4c3065 100644 --- a/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.js +++ b/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.js @@ -12,5 +12,11 @@ frappe.ui.form.on("Shopping Cart Settings", { if (frm.doc.enabled === 1) { frm.set_value('enable_variants', 1); } + else { + frm.set_value('company', ''); + frm.set_value('price_list', ''); + frm.set_value('default_customer_group', ''); + frm.set_value('quotation_series', ''); + } } }); diff --git a/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.json b/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.json index c574afa68c..32004efdca 100644 --- a/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.json +++ b/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.json @@ -95,15 +95,16 @@ "fieldtype": "Link", "in_list_view": 1, "label": "Company", + "mandatory_depends_on": "eval: doc.enabled === 1", "options": "Company", - "remember_last_selected_value": 1, - "reqd": 1 + "remember_last_selected_value": 1 }, { "description": "Prices will not be shown if Price List is not set", "fieldname": "price_list", "fieldtype": "Link", "label": "Price List", + "mandatory_depends_on": "eval: doc.enabled === 1", "options": "Price List" }, { @@ -115,14 +116,14 @@ "fieldtype": "Link", "ignore_user_permissions": 1, "label": "Default Customer Group", - "options": "Customer Group", - "reqd": 1 + "mandatory_depends_on": "eval: doc.enabled === 1", + "options": "Customer Group" }, { "fieldname": "quotation_series", "fieldtype": "Select", "label": "Quotation Series", - "reqd": 1 + "mandatory_depends_on": "eval: doc.enabled === 1" }, { "collapsible": 1, @@ -171,7 +172,7 @@ "idx": 1, "issingle": 1, "links": [], - "modified": "2020-07-17 17:53:22.667228", + "modified": "2020-08-02 18:21:43.873303", "modified_by": "Administrator", "module": "Shopping Cart", "name": "Shopping Cart Settings", 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/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json index 2b47d8c248..b7f080f983 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.json +++ b/erpnext/stock/doctype/delivery_note/delivery_note.json @@ -176,7 +176,7 @@ "no_copy": 1, "oldfieldname": "naming_series", "oldfieldtype": "Select", - "options": "MAT-DN-.YYYY.-", + "options": "MAT-DN-.YYYY.-\nMAT-DN-RET-.YYYY.-", "print_hide": 1, "reqd": 1, "set_only_once": 1 @@ -1266,7 +1266,7 @@ "idx": 146, "is_submittable": 1, "links": [], - "modified": "2020-07-31 19:56:19.800171", + "modified": "2020-08-03 23:18:47.739997", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note", diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index 735f35f36f..38e5fe53a7 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -117,7 +117,7 @@ frappe.ui.form.on("Item", { const stock_exists = (frm.doc.__onload && frm.doc.__onload.stock_exists) ? 1 : 0; - ['is_stock_item', 'has_serial_no', 'has_batch_no'].forEach((fieldname) => { + ['is_stock_item', 'has_serial_no', 'has_batch_no', 'has_variants'].forEach((fieldname) => { frm.set_df_property(fieldname, 'read_only', stock_exists); }); diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json index 963c87a0af..d07b3dc4fe 100644 --- a/erpnext/stock/doctype/item/item.json +++ b/erpnext/stock/doctype/item/item.json @@ -123,6 +123,7 @@ "weightage", "slideshow", "website_image", + "website_image_alt", "thumbnail", "cb72", "website_warehouse", @@ -473,6 +474,7 @@ }, { "default": "0", + "depends_on": "has_batch_no", "fieldname": "retain_sample", "fieldtype": "Check", "label": "Retain Sample" @@ -499,7 +501,7 @@ "oldfieldtype": "Select" }, { - "depends_on": "eval:doc.is_stock_item || doc.is_fixed_asset", + "depends_on": "has_serial_no", "description": "Example: ABCD.#####\nIf series is set and Serial No is not mentioned in transactions, then automatic serial number will be created based on this series. If you always want to explicitly mention Serial Nos for this item. leave this blank.", "fieldname": "serial_no_series", "fieldtype": "Data", @@ -1053,15 +1055,21 @@ "fieldtype": "Data", "label": "Default Manufacturer Part No", "read_only": 1 + }, + { + "fieldname": "website_image_alt", + "fieldtype": "Data", + "label": "Image Description" } ], "has_web_view": 1, "icon": "fa fa-tag", "idx": 2, "image_field": "image", + "index_web_pages_for_search": 1, "links": [], "max_attachments": 1, - "modified": "2020-06-30 12:01:07.534447", + "modified": "2020-08-07 14:24:58.384992", "modified_by": "Administrator", "module": "Stock", "name": "Item", @@ -1123,4 +1131,4 @@ "sort_order": "DESC", "title_field": "item_name", "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index d7b43bf399..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() @@ -343,7 +344,7 @@ class Item(WebsiteGenerator): if variant: context.variant = frappe.get_doc("Item", variant) - for fieldname in ("website_image", "web_long_description", "description", + for fieldname in ("website_image", "website_image_alt", "web_long_description", "description", "website_specifications"): if context.variant.get(fieldname): value = context.variant.get(fieldname) @@ -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/item_alternative/item_alternative.py b/erpnext/stock/doctype/item_alternative/item_alternative.py index 522dfc67a9..190cb62e99 100644 --- a/erpnext/stock/doctype/item_alternative/item_alternative.py +++ b/erpnext/stock/doctype/item_alternative/item_alternative.py @@ -43,6 +43,7 @@ class ItemAlternative(Document): frappe.throw(_("Already record exists for the item {0}").format(self.item_code)) @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def get_alternative_items(doctype, txt, searchfield, start, page_len, filters): return frappe.db.sql(""" (select alternative_item_code from `tabItem Alternative` where item_code = %(item_code)s and alternative_item_code like %(txt)s) diff --git a/erpnext/stock/doctype/material_request/material_request.json b/erpnext/stock/doctype/material_request/material_request.json index d1f29e364a..44503d22a3 100644 --- a/erpnext/stock/doctype/material_request/material_request.json +++ b/erpnext/stock/doctype/material_request/material_request.json @@ -11,6 +11,7 @@ "naming_series", "title", "material_request_type", + "transfer_status", "customer", "column_break_2", "schedule_date", @@ -303,13 +304,22 @@ "fieldtype": "Link", "label": "Set From Warehouse", "options": "Warehouse" + }, + { + "allow_on_submit": 1, + "depends_on": "eval:doc.add_to_transit == 1", + "fieldname": "transfer_status", + "fieldtype": "Select", + "label": "Transfer Status", + "options": "\nNot Started\nIn Transit\nCompleted", + "read_only": 1 } ], "icon": "fa fa-ticket", "idx": 70, "is_submittable": 1, "links": [], - "modified": "2020-05-01 20:21:09.990867", + "modified": "2020-08-10 13:27:54.891058", "modified_by": "Administrator", "module": "Stock", "name": "Material Request", diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index 25f1ed9505..335175f21d 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -370,6 +370,7 @@ def get_items_based_on_default_supplier(supplier): return supplier_items @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def get_material_requests_based_on_supplier(doctype, txt, searchfield, start, page_len, filters): conditions = "" if txt: @@ -403,6 +404,7 @@ def get_material_requests_based_on_supplier(doctype, txt, searchfield, start, pa return material_requests @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def get_default_supplier_query(doctype, txt, searchfield, start, page_len, filters): doc = frappe.get_doc("Material Request", filters.get("doc")) item_list = [] diff --git a/erpnext/stock/doctype/material_request/material_request_list.js b/erpnext/stock/doctype/material_request/material_request_list.js index 614ecb8a8f..0d7095875c 100644 --- a/erpnext/stock/doctype/material_request/material_request_list.js +++ b/erpnext/stock/doctype/material_request/material_request_list.js @@ -1,8 +1,16 @@ frappe.listview_settings['Material Request'] = { - add_fields: ["material_request_type", "status", "per_ordered", "per_received"], + add_fields: ["material_request_type", "status", "per_ordered", "per_received", "transfer_status"], get_indicator: function(doc) { if(doc.status=="Stopped") { return [__("Stopped"), "red", "status,=,Stopped"]; + } else if(doc.transfer_status && doc.docstatus != 2) { + if (doc.transfer_status == "Not Started") { + return [__("Not Started"), "orange"]; + } else if (doc.transfer_status == "In Transit") { + return [__("In Transit"), "yellow"]; + } else if (doc.transfer_status == "Completed") { + return [__("Completed"), "green"]; + } } else if(doc.docstatus==1 && flt(doc.per_ordered, 2) == 0) { return [__("Pending"), "orange", "per_ordered,=,0"]; } else if(doc.docstatus==1 && flt(doc.per_ordered, 2) < 100) { diff --git a/erpnext/stock/doctype/packing_slip/packing_slip.py b/erpnext/stock/doctype/packing_slip/packing_slip.py index 4f831d7a85..a7a29cca7f 100644 --- a/erpnext/stock/doctype/packing_slip/packing_slip.py +++ b/erpnext/stock/doctype/packing_slip/packing_slip.py @@ -176,6 +176,7 @@ class PackingSlip(Document): self.update_item_details() @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def item_details(doctype, txt, searchfield, start, page_len, filters): from erpnext.controllers.queries import get_match_cond return frappe.db.sql("""select name, item_name, description from `tabItem` diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json index fac8909e53..723908854d 100755 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json @@ -160,7 +160,7 @@ "no_copy": 1, "oldfieldname": "naming_series", "oldfieldtype": "Select", - "options": "MAT-PRE-.YYYY.-", + "options": "MAT-PRE-.YYYY.-\nMAT-PR-RET-.YYYY.-", "print_hide": 1, "reqd": 1, "set_only_once": 1 @@ -1120,7 +1120,7 @@ "idx": 261, "is_submittable": 1, "links": [], - "modified": "2020-07-31 15:16:26.811384", + "modified": "2020-08-03 23:20:26.381024", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt", diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index dafaae28c5..630446a5e5 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -240,6 +240,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, @@ -255,16 +263,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.py b/erpnext/stock/doctype/quality_inspection/quality_inspection.py index 568e742876..c3bb514184 100644 --- a/erpnext/stock/doctype/quality_inspection/quality_inspection.py +++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.py @@ -59,6 +59,7 @@ class QualityInspection(Document): (quality_inspection, self.modified, self.reference_name, self.item_code)) @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def item_query(doctype, txt, searchfield, start, page_len, filters): if filters.get("from"): from frappe.desk.reportview import get_match_cond @@ -88,6 +89,7 @@ def item_query(doctype, txt, searchfield, start, page_len, filters): {'parent': filters.get('parent'), 'txt': "%%%s%%" % txt}) @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def quality_inspection_query(doctype, txt, searchfield, start, page_len, filters): return frappe.get_all('Quality Inspection', limit_start=start, diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 53b986cb72..9845bc2f70 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -19,7 +19,6 @@ frappe.ui.form.on('Stock Entry', { filters: [ ['Stock Entry', 'docstatus', '=', 1], ['Stock Entry', 'per_transferred', '<','100'], - ['Stock Entry', 'purpose', '=', 'Send to Warehouse'] ] } }); @@ -171,9 +170,9 @@ frappe.ui.form.on('Stock Entry', { } } - if (frm.doc.docstatus === 1 && frm.doc.purpose == 'Send to Warehouse') { - if (frm.doc.per_transferred < 100) { - frm.add_custom_button(__('Receive at Warehouse Entry'), function() { + if (frm.doc.docstatus === 1) { + if (frm.doc.add_to_transit && frm.doc.purpose=='Material Transfer' && frm.doc.per_transferred < 100) { + frm.add_custom_button('End Transit', function() { frappe.model.open_mapped_doc({ method: "erpnext.stock.doctype.stock_entry.stock_entry.make_stock_in_entry", frm: frm @@ -266,6 +265,7 @@ frappe.ui.form.on('Stock Entry', { stock_entry_type: function(frm){ frm.remove_custom_button('Bill of Materials', "Get items from"); frm.events.show_bom_custom_button(frm); + frm.trigger('add_to_transit'); }, purpose: function(frm) { @@ -532,6 +532,26 @@ frappe.ui.form.on('Stock Entry', { target_warehouse_address: function(frm) { erpnext.utils.get_address_display(frm, 'target_warehouse_address', 'target_address_display', false); + }, + + add_to_transit: function(frm) { + if(frm.doc.add_to_transit && frm.doc.purpose=='Material Transfer') { + frm.set_value('stock_entry_type', 'Material Transfer'); + frm.fields_dict.to_warehouse.get_query = function() { + return { + filters:{ + 'warehouse_type' : 'Transit', + 'is_group': 0, + 'company': frm.doc.company + } + }; + }; + frappe.db.get_value('Company', frm.doc.company, 'default_in_transit_warehouse', (r) => { + if (r.default_in_transit_warehouse) { + frm.set_value('to_warehouse', r.default_in_transit_warehouse); + } + }); + } } }) @@ -754,6 +774,7 @@ erpnext.stock.StockEntry = erpnext.stock.StockController.extend({ } erpnext.hide_company(); erpnext.utils.add_item(this.frm); + this.frm.trigger('add_to_transit'); }, scan_barcode: function() { @@ -919,8 +940,6 @@ erpnext.stock.StockEntry = erpnext.stock.StockController.extend({ doc.purpose!='Material Issue'); this.frm.fields_dict["items"].grid.set_column_disp("additional_cost", doc.purpose!='Material Issue'); - this.frm.toggle_reqd("outgoing_stock_entry", - doc.purpose == 'Receive at Warehouse' ? 1: 0); }, supplier: function(doc) { diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.json b/erpnext/stock/doctype/stock_entry/stock_entry.json index 704ae41bc5..61e0df6723 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.json +++ b/erpnext/stock/doctype/stock_entry/stock_entry.json @@ -13,6 +13,7 @@ "stock_entry_type", "outgoing_stock_entry", "purpose", + "add_to_transit", "work_order", "purchase_order", "delivery_note_no", @@ -116,11 +117,12 @@ "reqd": 1 }, { - "depends_on": "eval:doc.purpose == 'Receive at Warehouse'", + "depends_on": "eval:doc.purpose == 'Material Transfer'", "fieldname": "outgoing_stock_entry", "fieldtype": "Link", "label": "Stock Entry (Outward GIT)", - "options": "Stock Entry" + "options": "Stock Entry", + "read_only": 1 }, { "bold": 1, @@ -132,7 +134,7 @@ "label": "Purpose", "oldfieldname": "purpose", "oldfieldtype": "Select", - "options": "Material Issue\nMaterial Receipt\nMaterial Transfer\nMaterial Transfer for Manufacture\nMaterial Consumption for Manufacture\nManufacture\nRepack\nSend to Subcontractor\nSend to Warehouse\nReceive at Warehouse", + "options": "Material Issue\nMaterial Receipt\nMaterial Transfer\nMaterial Transfer for Manufacture\nMaterial Consumption for Manufacture\nManufacture\nRepack\nSend to Subcontractor", "read_only": 1 }, { @@ -630,13 +632,21 @@ { "fieldname": "print_settings_col_break", "fieldtype": "Column Break" + }, + { + "default": "0", + "depends_on": "eval: doc.purpose=='Material Transfer' && !doc.outgoing_stock_entry", + "fieldname": "add_to_transit", + "fieldtype": "Check", + "label": "Add to Transit", + "no_copy": 1 } ], "icon": "fa fa-file-text", "idx": 1, "is_submittable": 1, "links": [], - "modified": "2020-04-23 12:56:52.881752", + "modified": "2020-08-11 19:10:07.954981", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry", diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 229cf027bd..a92d04ff8c 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -96,6 +96,11 @@ class StockEntry(StockController): self.update_quality_inspection() if self.work_order and self.purpose == "Manufacture": self.update_so_in_serial_number() + + if self.purpose == 'Material Transfer' and self.add_to_transit: + self.set_material_request_transfer_status('In Transit') + if self.purpose == 'Material Transfer' and self.outgoing_stock_entry: + self.set_material_request_transfer_status('Completed') def on_cancel(self): @@ -116,6 +121,11 @@ class StockEntry(StockController): self.update_quality_inspection() self.delete_auto_created_batches() + if self.purpose == 'Material Transfer' and self.add_to_transit: + self.set_material_request_transfer_status('Not Started') + if self.purpose == 'Material Transfer' and self.outgoing_stock_entry: + self.set_material_request_transfer_status('In Transit') + def set_job_card_data(self): if self.job_card and not self.work_order: data = frappe.db.get_value('Job Card', @@ -133,7 +143,7 @@ class StockEntry(StockController): def validate_purpose(self): valid_purposes = ["Material Issue", "Material Receipt", "Material Transfer", "Material Transfer for Manufacture", "Manufacture", "Repack", "Send to Subcontractor", - "Material Consumption for Manufacture", "Send to Warehouse", "Receive at Warehouse"] + "Material Consumption for Manufacture"] if self.purpose not in valid_purposes: frappe.throw(_("Purpose must be one of {0}").format(comma_or(valid_purposes))) @@ -199,7 +209,8 @@ class StockEntry(StockController): item.set(f, item_details.get(f)) if not item.transfer_qty and item.qty: - item.transfer_qty = item.qty * item.conversion_factor + item.transfer_qty = flt(flt(item.qty) * flt(item.conversion_factor), + self.precision("transfer_qty", item)) if (self.purpose in ("Material Transfer", "Material Transfer for Manufacture") and not item.serial_no @@ -258,10 +269,10 @@ class StockEntry(StockController): """perform various (sometimes conditional) validations on warehouse""" source_mandatory = ["Material Issue", "Material Transfer", "Send to Subcontractor", "Material Transfer for Manufacture", - "Material Consumption for Manufacture", "Send to Warehouse", "Receive at Warehouse"] + "Material Consumption for Manufacture"] target_mandatory = ["Material Receipt", "Material Transfer", "Send to Subcontractor", - "Material Transfer for Manufacture", "Send to Warehouse", "Receive at Warehouse"] + "Material Transfer for Manufacture"] validate_for_manufacture = any([d.bom_no for d in self.get("items")]) @@ -502,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": @@ -809,7 +820,7 @@ class StockEntry(StockController): def set_items_for_stock_in(self): self.items = [] - if self.outgoing_stock_entry and self.purpose == 'Receive at Warehouse': + if self.outgoing_stock_entry and self.purpose == 'Material Transfer': doc = frappe.get_doc('Stock Entry', self.outgoing_stock_entry) if doc.per_transferred == 100: @@ -1210,13 +1221,25 @@ class StockEntry(StockController): def validate_with_material_request(self): for item in self.get("items"): - if item.material_request: + material_request = item.material_request or None + material_request_item = item.material_request_item or None + if self.purpose == 'Material Transfer' and self.outgoing_stock_entry: + parent_se = frappe.get_value("Stock Entry Detail", item.ste_detail, ['material_request','material_request_item'],as_dict=True) + if parent_se: + material_request = parent_se.material_request + material_request_item = parent_se.material_request_item + + if material_request: mreq_item = frappe.db.get_value("Material Request Item", - {"name": item.material_request_item, "parent": item.material_request}, + {"name": material_request_item, "parent": material_request}, ["item_code", "warehouse", "idx"], as_dict=True) - if mreq_item.item_code != item.item_code or \ - mreq_item.warehouse != (item.s_warehouse if self.purpose== "Material Issue" else item.t_warehouse): - frappe.throw(_("Item or Warehouse for row {0} does not match Material Request").format(item.idx), + if mreq_item.item_code != item.item_code: + frappe.throw(_("Item for row {0} does not match Material Request").format(item.idx), + frappe.MappingMismatchError) + elif self.purpose == "Material Transfer" and self.add_to_transit: + continue + elif mreq_item.warehouse != (item.s_warehouse if self.purpose == "Material Issue" else item.t_warehouse): + frappe.throw(_("Warehouse for row {0} does not match Material Request").format(item.idx), frappe.MappingMismatchError) def validate_batch(self): @@ -1284,7 +1307,7 @@ class StockEntry(StockController): to fullfill Sales Order {2}.").format(item.item_code, sr, sales_order)) def update_transferred_qty(self): - if self.purpose == 'Receive at Warehouse': + if self.purpose == 'Material Transfer' and self.outgoing_stock_entry: stock_entries = {} stock_entries_child_list = [] for d in self.items: @@ -1342,6 +1365,20 @@ class StockEntry(StockController): 'reference_type': reference_type, 'reference_name': reference_name }) + def set_material_request_transfer_status(self, status): + material_requests = [] + if self.outgoing_stock_entry: + parent_se = frappe.get_value("Stock Entry", self.outgoing_stock_entry, 'add_to_transit') + + for item in self.items: + material_request = item.material_request or None + if self.purpose == "Material Transfer" and material_request not in material_requests: + if self.outgoing_stock_entry and parent_se: + material_request = frappe.get_value("Stock Entry Detail", item.ste_detail, 'material_request') + + if material_request and material_request not in material_requests: + material_requests.append(material_request) + frappe.db.set_value('Material Request', material_request, 'transfer_status', status) @frappe.whitelist() def move_sample_to_retention_warehouse(company, items): @@ -1381,12 +1418,19 @@ def move_sample_to_retention_warehouse(company, items): @frappe.whitelist() def make_stock_in_entry(source_name, target_doc=None): + def set_missing_values(source, target): - target.purpose = 'Receive at Warehouse' target.set_stock_entry_type() def update_item(source_doc, target_doc, source_parent): target_doc.t_warehouse = '' + + if source_doc.material_request_item and source_doc.material_request : + add_to_transit = frappe.db.get_value('Stock Entry', source_name, 'add_to_transit') + if add_to_transit: + warehouse = frappe.get_value('Material Request Item', source_doc.material_request_item, 'warehouse') + target_doc.t_warehouse = warehouse + target_doc.s_warehouse = source_doc.t_warehouse target_doc.qty = source_doc.qty - source_doc.transferred_qty diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 8e25804511..d98870de3e 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -737,34 +737,6 @@ class TestStockEntry(unittest.TestCase): self.assertEqual(se.get("items")[0].allow_zero_valuation_rate, 1) self.assertEqual(se.get("items")[0].amount, 0) - def test_goods_in_transit(self): - from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse - warehouse = "_Test Warehouse FG 1 - _TC" - - if not frappe.db.exists('Warehouse', warehouse): - create_warehouse("_Test Warehouse FG 1") - - outward_entry = make_stock_entry(item_code="_Test Item", - purpose="Send to Warehouse", - source="_Test Warehouse - _TC", - target="_Test Warehouse 1 - _TC", qty=50, basic_rate=100) - - inward_entry1 = make_stock_in_entry(outward_entry.name) - inward_entry1.items[0].t_warehouse = warehouse - inward_entry1.items[0].qty = 25 - inward_entry1.submit() - - doc = frappe.get_doc('Stock Entry', outward_entry.name) - self.assertEqual(doc.per_transferred, 50) - - inward_entry2 = make_stock_in_entry(outward_entry.name) - inward_entry2.items[0].t_warehouse = warehouse - inward_entry2.items[0].qty = 25 - inward_entry2.submit() - - doc = frappe.get_doc('Stock Entry', outward_entry.name) - self.assertEqual(doc.per_transferred, 100) - def test_gle_for_opening_stock_entry(self): mr = make_stock_entry(item_code="_Test Item", target="Stores - TCP1", company="_Test Company with perpetual inventory",qty=50, basic_rate=100, expense_account="Stock Adjustment - TCP1", is_opening="Yes", do_not_save=True) diff --git a/erpnext/stock/doctype/stock_entry_type/stock_entry_type.json b/erpnext/stock/doctype/stock_entry_type/stock_entry_type.json index edee3c7dc9..0f2b55ec34 100644 --- a/erpnext/stock/doctype/stock_entry_type/stock_entry_type.json +++ b/erpnext/stock/doctype/stock_entry_type/stock_entry_type.json @@ -1,156 +1,83 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, + "actions": [], "autoname": "Prompt", - "beta": 0, "creation": "2019-03-13 16:23:46.636769", - "custom": 0, - "docstatus": 0, "doctype": "DocType", - "document_type": "", "editable_grid": 1, "engine": "InnoDB", + "field_order": [ + "purpose" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "Material Issue", - "fetch_if_empty": 0, "fieldname": "purpose", "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, - "in_standard_filter": 0, "label": "Purpose", - "length": 0, - "no_copy": 0, - "options": "\nMaterial Issue\nMaterial Receipt\nMaterial Transfer\nMaterial Transfer for Manufacture\nMaterial Consumption for Manufacture\nManufacture\nRepack\nSend to Subcontractor\nSend to Warehouse\nReceive at Warehouse", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, + "options": "\nMaterial Issue\nMaterial Receipt\nMaterial Transfer\nMaterial Transfer for Manufacture\nMaterial Consumption for Manufacture\nManufacture\nRepack\nSend to Subcontractor", "reqd": 1, - "search_index": 0, - "set_only_once": 1, - "translatable": 0, - "unique": 0 + "set_only_once": 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": "2019-03-26 12:02:42.144377", + "links": [], + "modified": "2020-08-10 23:24:37.160817", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry Type", - "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": "Manufacturing 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": "Stock 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": "Stock User", - "set_user_permissions": 0, "share": 1, - "submit": 0, "write": 1 } ], "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, "sort_field": "modified", "sort_order": "ASC", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 + "track_changes": 1 } \ No newline at end of file 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/doctype/warehouse/warehouse.json b/erpnext/stock/doctype/warehouse/warehouse.json index 3b49c4ca52..1cc600b9ca 100644 --- a/erpnext/stock/doctype/warehouse/warehouse.json +++ b/erpnext/stock/doctype/warehouse/warehouse.json @@ -45,7 +45,6 @@ "oldfieldtype": "Section Break" }, { - "description": "If blank, parent Warehouse Account or company default will be considered", "fieldname": "warehouse_name", "fieldtype": "Data", "label": "Warehouse Name", @@ -86,6 +85,7 @@ "fieldtype": "Column Break" }, { + "description": "If blank, parent Warehouse Account or company default will be considered in transactions", "fieldname": "account", "fieldtype": "Link", "label": "Account", @@ -236,7 +236,7 @@ "idx": 1, "is_tree": 1, "links": [], - "modified": "2020-07-16 15:43:50.653256", + "modified": "2020-08-03 18:41:52.442502", "modified_by": "Administrator", "module": "Stock", "name": "Warehouse", 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_configure.js b/erpnext/templates/generators/item/item_configure.js index 163c955c56..5fd901169f 100644 --- a/erpnext/templates/generators/item/item_configure.js +++ b/erpnext/templates/generators/item/item_configure.js @@ -193,17 +193,14 @@ class ItemConfigure { filtered_items_count === 1 ? filtered_items[0] : ''; - // Allow Add to Cart if adding out of stock items enabled in Shopping Cart else check stock. - const in_stock = product_info.allow_items_not_in_stock ? 1 : product_info.in_stock; - const add_to_cart = `${__('Add to cart')}`; - const product_action = in_stock ? add_to_cart : `${__('Not in Stock')}`; - const item_add_to_cart = one_item ? ` `: ''; diff --git a/erpnext/templates/generators/item/item_image.html b/erpnext/templates/generators/item/item_image.html index 0dd4c3505e..5d46a45053 100644 --- a/erpnext/templates/generators/item/item_image.html +++ b/erpnext/templates/generators/item/item_image.html @@ -23,7 +23,7 @@ }) {% else %} -{{ product_image(website_image or image or 'no-image.jpg') }} +{{ product_image(website_image or image or 'no-image.jpg', alt=website_image_alt or item_name) }} {% 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/generators/item_group.html b/erpnext/templates/generators/item_group.html index 3f98453603..40a064fc76 100644 --- a/erpnext/templates/generators/item_group.html +++ b/erpnext/templates/generators/item_group.html @@ -4,7 +4,7 @@ {% block page_content %}
    -
    +
    {% if slideshow %} {% include "templates/includes/slideshow.html" %} {% endif %} diff --git a/erpnext/templates/includes/macros.html b/erpnext/templates/includes/macros.html index 3c82e90cc0..ea6b00fc58 100644 --- a/erpnext/templates/includes/macros.html +++ b/erpnext/templates/includes/macros.html @@ -7,9 +7,9 @@
    {% endmacro %} -{% macro product_image(website_image, css_class="") %} +{% macro product_image(website_image, css_class="", alt="") %}
    - + {{ alt }}
    {% endmacro %} 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/erpnext/www/support/index.html b/erpnext/www/support/index.html index 93da503dbb..12b4c2c081 100644 --- a/erpnext/www/support/index.html +++ b/erpnext/www/support/index.html @@ -9,6 +9,33 @@

    {{ greeting_subtitle }}

    {% endif %}
    +
    + + +
    @@ -54,5 +81,21 @@ {% endif %} +{% endblock %} -{% endblock %} \ No newline at end of file +{%- block script -%} + +{%- endblock -%} + +{%- block style -%} + +{%- endblock -%} diff --git a/yarn.lock b/yarn.lock index c5509d6e1a..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== @@ -1084,9 +1084,9 @@ lodash.set@^4.3.2: integrity sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM= lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.7.14: - version "4.17.15" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" - integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== + version "4.17.19" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b" + integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ== lowercase-keys@^1.0.0: version "1.0.1" @@ -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"