diff --git a/erpnext/accounts/desk_page/accounting/accounting.json b/erpnext/accounts/desk_page/accounting/accounting.json index a2497838eed..2c5231491cd 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/bank_transaction/test_bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py index 0fe57c32399..27546335c91 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/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js index a09face7918..409c15f75ce 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"; + } + 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 219871b1d66..d0116890b65 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 diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index 0599707446c..f735d87a764 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)) @@ -141,7 +144,7 @@ def get_gl_entries(filters, accounting_dimensions): 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, @@ -155,7 +158,7 @@ def get_gl_entries(filters, accounting_dimensions): against_voucher_type, against_voucher, account_currency, - remarks, against, + remarks, against, is_opening, `tabGL Entry`.creation {select_fields_with_percentage} FROM `tabGL Entry`, ( diff --git a/erpnext/accounts/report/gross_profit/gross_profit.json b/erpnext/accounts/report/gross_profit/gross_profit.json index 9cfb0627d30..cd6bac2d77d 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/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 0bd03a8dbe2..9d08d9212d1 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -146,7 +146,7 @@ class Asset(AccountsController): 'assets': assets, 'purpose': 'Receipt', 'company': self.company, - 'transaction_date': getdate(nowdate()), + 'transaction_date': getdate(self.purchase_date), 'reference_doctype': reference_doctype, 'reference_name': reference_docname }).insert() diff --git a/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py index 3de9526c4f2..019cefc0bd1 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 a76ffeec2ec..518d665e7ef 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 a33867a525e..4426560c162 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 cfc08eb084e..31e79f17cd2 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 5fe3c4edbb8..b31b757a376 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/status_updater.py b/erpnext/controllers/status_updater.py index 0dc9878afd0..9feac787709 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -255,7 +255,7 @@ class StatusUpdater(Document): args['second_source_condition'] = """ + ifnull((select sum(%(second_source_field)s) from `tab%(second_source_dt)s` where `%(second_join_field)s`="%(detail_id)s" - and (`tab%(second_source_dt)s`.docstatus=1) %(second_source_extra_cond)s), 0) """ % args + and (`tab%(second_source_dt)s`.docstatus=1) %(second_source_extra_cond)s FOR UPDATE), 0)""" % args if args['detail_id']: if not args.get("extra_cond"): args["extra_cond"] = "" diff --git a/erpnext/crm/doctype/opportunity/opportunity.py b/erpnext/crm/doctype/opportunity/opportunity.py index 6096053136a..47b05f306b7 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.py +++ b/erpnext/crm/doctype/opportunity/opportunity.py @@ -267,6 +267,9 @@ def make_quotation(source_name, target_doc=None): @frappe.whitelist() def make_request_for_quotation(source_name, target_doc=None): + def update_item(obj, target, source_parent): + target.conversion_factor = 1.0 + doclist = get_mapped_doc("Opportunity", source_name, { "Opportunity": { "doctype": "Request for Quotation" @@ -277,7 +280,8 @@ def make_request_for_quotation(source_name, target_doc=None): ["name", "opportunity_item"], ["parent", "opportunity"], ["uom", "uom"] - ] + ], + "postprocess": update_item } }, target_doc) diff --git a/erpnext/crm/doctype/opportunity/test_opportunity.py b/erpnext/crm/doctype/opportunity/test_opportunity.py index 33d90076c4a..04cd8a26cad 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/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 00000000000..8dcc77d1749 --- /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/hr/doctype/attendance/attendance.py b/erpnext/hr/doctype/attendance/attendance.py index 45b7060610d..373b94008e7 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 104f09d69ff..45664896965 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/leave_ledger_entry/leave_ledger_entry.json b/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.json index a5ac3f3d471..4abba5f2d4a 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 00000000000..889325bf2b7 --- /dev/null +++ b/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry_list.js @@ -0,0 +1,13 @@ +frappe.listview_settings['Leave Ledger Entry'] = { + onload: function(listview) { + if(listview.page.fields_dict.transaction_type) { + listview.page.fields_dict.transaction_type.get_query = function() { + return { + "filters": { + "name": ["in", ["Leave Allocation", "Leave Application", "Leave Encashment"]], + } + }; + }; + } + } +}; diff --git a/erpnext/hr/doctype/shift_assignment/shift_assignment.py b/erpnext/hr/doctype/shift_assignment/shift_assignment.py index f8b73349c19..2c385e80f46 100644 --- a/erpnext/hr/doctype/shift_assignment/shift_assignment.py +++ b/erpnext/hr/doctype/shift_assignment/shift_assignment.py @@ -103,7 +103,7 @@ def add_assignments(events, start, end, conditions=None): "doctype": "Shift Assignment", "start_date": d.start_date, "end_date": d.end_date if d.end_date else nowdate(), - "title": cstr(d.employee_name) + \ + "title": cstr(d.employee_name) + ": "+ \ cstr(d.shift_type), "docstatus": d.docstatus } diff --git a/erpnext/loan_management/doctype/loan/loan.js b/erpnext/loan_management/doctype/loan/loan.js index ffef60b6b0a..9b4c21770ed 100644 --- a/erpnext/loan_management/doctype/loan/loan.js +++ b/erpnext/loan_management/doctype/loan/loan.js @@ -73,8 +73,8 @@ frappe.ui.form.on('Loan', { loan_type: function(frm) { frm.toggle_reqd("repayment_method", frm.doc.is_term_loan); - frm.toggle_display("repayment_method", 1 - frm.doc.is_term_loan); - frm.toggle_display("repayment_periods", s1 - frm.doc.is_term_loan); + frm.toggle_display("repayment_method", frm.doc.is_term_loan); + frm.toggle_display("repayment_periods", frm.doc.is_term_loan); }, @@ -119,12 +119,10 @@ frappe.ui.form.on('Loan', { create_loan_security_unpledge: function(frm) { frappe.call({ - method: "erpnext.loan_management.doctype.loan.loan.create_loan_security_unpledge", + method: "erpnext.loan_management.doctype.loan.loan.unpledge_security", args : { "loan": frm.doc.name, - "applicant_type": frm.doc.applicant_type, - "applicant": frm.doc.applicant, - "company": frm.doc.company + "as_dict": 1 }, callback: function(r) { if (r.message) diff --git a/erpnext/loan_management/doctype/loan/loan.py b/erpnext/loan_management/doctype/loan/loan.py index e20b484fc0e..d1b7589a17e 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 23815d5982e..5a4a19a0fbd 100644 --- a/erpnext/loan_management/doctype/loan/test_loan.py +++ b/erpnext/loan_management/doctype/loan/test_loan.py @@ -14,9 +14,11 @@ from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_ process_loan_interest_accrual_for_term_loans) from erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual import days_in_year from erpnext.loan_management.doctype.process_loan_security_shortfall.process_loan_security_shortfall import create_process_loan_security_shortfall -from erpnext.loan_management.doctype.loan.loan import create_loan_security_unpledge +from erpnext.loan_management.doctype.loan.loan import unpledge_security from erpnext.loan_management.doctype.loan_security_unpledge.loan_security_unpledge import get_pledged_security_qty from erpnext.loan_management.doctype.loan_application.loan_application import create_pledge +from erpnext.loan_management.doctype.loan_disbursement.loan_disbursement import get_disbursal_amount +from erpnext.loan_management.doctype.loan_repayment.loan_repayment import calculate_amounts class TestLoan(unittest.TestCase): def setUp(self): @@ -193,18 +195,13 @@ class TestLoan(unittest.TestCase): make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date) process_loan_interest_accrual_for_demand_loans(posting_date = last_date) - repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5), + repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 6), "Loan Closure", flt(loan.loan_amount + accrued_interest_amount)) repayment_entry.submit() - amounts = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['paid_interest_amount', - 'paid_principal_amount']) + amount = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['sum(paid_interest_amount)']) - unaccrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * 6) \ - / (days_in_year(get_datetime(first_date).year) * 100) - - self.assertEquals(flt(amounts[0] + unaccrued_interest_amount, 3), - flt(accrued_interest_amount, 3)) + self.assertEquals(flt(amount, 2),flt(accrued_interest_amount, 2)) self.assertEquals(flt(repayment_entry.penalty_amount, 5), 0) loan.load_from_db() @@ -306,13 +303,10 @@ class TestLoan(unittest.TestCase): "Loan Closure", flt(loan.loan_amount + accrued_interest_amount)) repayment_entry.submit() - amounts = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['paid_interest_amount', - 'paid_principal_amount']) - loan.load_from_db() self.assertEquals(loan.status, "Loan Closure Requested") - unpledge_request = create_loan_security_unpledge(loan.name, loan.applicant_type, loan.applicant, loan.company, as_dict=0) + unpledge_request = unpledge_security(loan=loan.name, save=1) unpledge_request.submit() unpledge_request.status = 'Approved' unpledge_request.save() @@ -323,6 +317,102 @@ class TestLoan(unittest.TestCase): self.assertEqual(loan.status, 'Closed') self.assertEquals(sum(pledged_qty.values()), 0) + amounts = amounts = calculate_amounts(loan.name, add_days(last_date, 6), "Regular Repayment") + self.assertEqual(amounts['pending_principal_amount'], 0) + self.assertEqual(amounts['payable_principal_amount'], 0) + self.assertEqual(amounts['interest_amount'], 0) + + def test_disbursal_check_with_shortfall(self): + pledges = [{ + "loan_security": "Test Security 2", + "qty": 8000.00, + "haircut": 50, + }] + + loan_application = create_loan_application('_Test Company', self.applicant2, + 'Stock Loan', pledges, "Repay Over Number of Periods", 12) + + create_pledge(loan_application) + + loan = create_loan_with_security(self.applicant2, "Stock Loan", "Repay Over Number of Periods", 12, loan_application) + loan.submit() + + #Disbursing 7,00,000 from the allowed 10,00,000 according to security pledge + make_loan_disbursement_entry(loan.name, 700000) + + frappe.db.sql("""UPDATE `tabLoan Security Price` SET loan_security_price = 100 + where loan_security='Test Security 2'""") + + create_process_loan_security_shortfall() + loan_security_shortfall = frappe.get_doc("Loan Security Shortfall", {"loan": loan.name}) + self.assertTrue(loan_security_shortfall) + + self.assertEqual(get_disbursal_amount(loan.name), 0) + + frappe.db.sql(""" UPDATE `tabLoan Security Price` SET loan_security_price = 250 + where loan_security='Test Security 2'""") + + def test_disbursal_check_without_shortfall(self): + pledges = [{ + "loan_security": "Test Security 2", + "qty": 8000.00, + "haircut": 50, + }] + + loan_application = create_loan_application('_Test Company', self.applicant2, + 'Stock Loan', pledges, "Repay Over Number of Periods", 12) + + create_pledge(loan_application) + + loan = create_loan_with_security(self.applicant2, "Stock Loan", "Repay Over Number of Periods", 12, loan_application) + loan.submit() + + #Disbursing 7,00,000 from the allowed 10,00,000 according to security pledge + make_loan_disbursement_entry(loan.name, 700000) + + self.assertEqual(get_disbursal_amount(loan.name), 300000) + + def test_pending_loan_amount_after_closure_request(self): + pledge = [{ + "loan_security": "Test Security 1", + "qty": 4000.00 + }] + + loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge) + create_pledge(loan_application) + + loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date=get_first_day(nowdate())) + loan.submit() + + self.assertEquals(loan.loan_amount, 1000000) + + first_date = '2019-10-01' + last_date = '2019-10-30' + + no_of_days = date_diff(last_date, first_date) + 1 + + no_of_days += 6 + + accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \ + / (days_in_year(get_datetime(first_date).year) * 100) + + make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date) + process_loan_interest_accrual_for_demand_loans(posting_date = last_date) + + amounts = calculate_amounts(loan.name, add_days(last_date, 6), "Regular Repayment") + + repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 6), + "Loan Closure", flt(loan.loan_amount + accrued_interest_amount)) + repayment_entry.submit() + + amounts = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['paid_interest_amount', + 'paid_principal_amount']) + + loan.load_from_db() + self.assertEquals(loan.status, "Loan Closure Requested") + + amounts = calculate_amounts(loan.name, add_days(last_date, 6), "Regular Repayment") + self.assertEquals(amounts['pending_principal_amount'], 0.0) def create_loan_accounts(): if not frappe.db.exists("Account", "Loans and Advances (Assets) - _TC"): diff --git a/erpnext/loan_management/doctype/loan_application/loan_application.js b/erpnext/loan_management/doctype/loan_application/loan_application.js index b56fce1d7cb..13652749711 100644 --- a/erpnext/loan_management/doctype/loan_application/loan_application.js +++ b/erpnext/loan_management/doctype/loan_application/loan_application.js @@ -33,18 +33,18 @@ frappe.ui.form.on('Loan Application', { if (frm.doc.is_secured_loan) { frappe.db.get_value("Loan Security Pledge", {"loan_application": frm.doc.name, "docstatus": 1}, "name", (r) => { - if (!r) { + if (Object.keys(r).length === 0) { frm.add_custom_button(__('Loan Security Pledge'), function() { - frm.trigger('create_loan_security_pledge') + frm.trigger('create_loan_security_pledge'); },__('Create')) } }); } frappe.db.get_value("Loan", {"loan_application": frm.doc.name, "docstatus": 1}, "name", (r) => { - if (!r) { + if (Object.keys(r).length === 0) { frm.add_custom_button(__('Loan'), function() { - frm.trigger('create_loan') + frm.trigger('create_loan'); },__('Create')) } else { frm.set_df_property('status', 'read_only', 1); @@ -54,7 +54,7 @@ frappe.ui.form.on('Loan Application', { }, create_loan: function(frm) { if (frm.doc.status != "Approved") { - frappe.throw(__("Cannot create loan until application is approved")) + frappe.throw(__("Cannot create loan until application is approved")); } frappe.model.open_mapped_doc({ diff --git a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py index 6c27e121347..260fada8936 100644 --- a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py +++ b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py @@ -67,28 +67,10 @@ class LoanDisbursement(AccountsController): disbursed_amount = self.disbursed_amount + loan_details.disbursed_amount total_payment = loan_details.total_payment - if disbursed_amount > loan_details.loan_amount and loan_details.is_term_loan: - frappe.throw(_("Disbursed Amount cannot be greater than loan amount")) + possible_disbursal_amount = get_disbursal_amount(self.against_loan) - if loan_details.status == 'Disbursed': - pending_principal_amount = flt(loan_details.total_payment) - flt(loan_details.total_interest_payable) \ - - flt(loan_details.total_principal_paid) - else: - pending_principal_amount = loan_details.disbursed_amount - - security_value = 0.0 - if loan_details.is_secured_loan: - security_value = get_total_pledged_security_value(self.against_loan) - - if not security_value: - security_value = loan_details.loan_amount - - if pending_principal_amount + self.disbursed_amount > flt(security_value): - allowed_amount = security_value - pending_principal_amount - if allowed_amount < 0: - allowed_amount = 0 - - frappe.throw(_("Disbursed Amount cannot be greater than {0}").format(allowed_amount)) + if self.disbursed_amount > possible_disbursal_amount: + frappe.throw(_("Disbursed Amount cannot be greater than {0}").format(possible_disbursal_amount)) if loan_details.status == "Disbursed" and not loan_details.is_term_loan: process_loan_interest_accrual_for_demand_loans(posting_date=add_days(self.disbursement_date, -1), @@ -176,3 +158,32 @@ def get_total_pledged_security_value(loan): security_value += (loan_security_price_map.get(security) * qty * hair_cut_map.get(security))/100 return security_value + +@frappe.whitelist() +def get_disbursal_amount(loan): + loan_details = frappe.get_all("Loan", fields = ["loan_amount", "disbursed_amount", "total_payment", + "total_principal_paid", "total_interest_payable", "status", "is_term_loan", "is_secured_loan"], + filters= { "name": loan })[0] + + if loan_details.is_secured_loan and frappe.get_all('Loan Security Shortfall', filters={'loan': loan, + 'status': 'Pending'}): + return 0 + + if loan_details.status == 'Disbursed': + pending_principal_amount = flt(loan_details.total_payment) - flt(loan_details.total_interest_payable) \ + - flt(loan_details.total_principal_paid) + else: + pending_principal_amount = flt(loan_details.disbursed_amount) + + security_value = 0.0 + if loan_details.is_secured_loan: + security_value = get_total_pledged_security_value(loan) + + if not security_value and not loan_details.is_secured_loan: + security_value = flt(loan_details.loan_amount) + + disbursal_amount = flt(security_value) - flt(pending_principal_amount) + + return disbursal_amount + + diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py index c5111fdc930..2d959bf3be6 100644 --- a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py +++ b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py @@ -85,8 +85,11 @@ def calculate_accrual_amount_for_demand_loans(loan, posting_date, process_loan_i if no_of_days <= 0: return - pending_principal_amount = flt(loan.total_payment) - flt(loan.total_interest_payable) \ - - flt(loan.total_principal_paid) + if loan.status == 'Disbursed': + pending_principal_amount = flt(loan.total_payment) - flt(loan.total_interest_payable) \ + - flt(loan.total_principal_paid) + else: + pending_principal_amount = loan.disbursed_amount interest_per_day = (pending_principal_amount * loan.rate_of_interest) / (days_in_year(get_datetime(posting_date).year) * 100) payable_interest = interest_per_day * no_of_days @@ -107,7 +110,7 @@ def calculate_accrual_amount_for_demand_loans(loan, posting_date, process_loan_i def make_accrual_interest_entry_for_demand_loans(posting_date, process_loan_interest, open_loans=None, loan_type=None): query_filters = { - "status": "Disbursed", + "status": ('in', ['Disbursed', 'Partially Disbursed']), "docstatus": 1 } @@ -118,8 +121,9 @@ def make_accrual_interest_entry_for_demand_loans(posting_date, process_loan_inte if not open_loans: open_loans = frappe.get_all("Loan", - fields=["name", "total_payment", "total_amount_paid", "loan_account", "interest_income_account", "is_term_loan", - "disbursement_date", "applicant_type", "applicant", "rate_of_interest", "total_interest_payable", "repayment_start_date"], + fields=["name", "total_payment", "total_amount_paid", "loan_account", "interest_income_account", + "is_term_loan", "status", "disbursement_date", "disbursed_amount", "applicant_type", "applicant", + "rate_of_interest", "total_interest_payable", "total_principal_paid", "repayment_start_date"], filters=query_filters) for loan in open_loans: @@ -209,7 +213,8 @@ def get_last_accural_date_in_current_month(loan): WHERE loan = %s""", (loan.name)) if last_posting_date[0][0]: - return last_posting_date[0][0] + # interest for last interest accrual date is already booked, so add 1 day + return add_days(last_posting_date[0][0], 1) else: return loan.disbursement_date diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index 9605045777d..97dbc44bf13 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -13,6 +13,7 @@ from frappe.utils import date_diff, add_days, getdate, add_months, get_first_day from erpnext.controllers.accounts_controller import AccountsController from erpnext.accounts.general_ledger import make_gl_entries from erpnext.loan_management.doctype.loan_security_shortfall.loan_security_shortfall import update_shortfall_status +from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import process_loan_interest_accrual_for_demand_loans class LoanRepayment(AccountsController): @@ -22,6 +23,9 @@ class LoanRepayment(AccountsController): self.validate_amount() self.allocate_amounts(amounts['pending_accrual_entries']) + def before_submit(self): + self.book_unaccrued_interest() + def on_submit(self): self.update_paid_amount() self.make_gl_entries() @@ -72,6 +76,26 @@ class LoanRepayment(AccountsController): msg = _("Amount of {0} is required for Loan closure").format(self.payable_amount) frappe.throw(msg) + def book_unaccrued_interest(self): + if self.payment_type == 'Loan Closure': + total_interest_paid = 0 + for payment in self.repayment_details: + total_interest_paid += payment.paid_interest_amount + + if total_interest_paid < self.interest_payable: + if not self.is_term_loan: + process = process_loan_interest_accrual_for_demand_loans(posting_date=self.posting_date, + loan=self.against_loan) + + lia = frappe.db.get_value('Loan Interest Accrual', {'process_loan_interest_accrual': + process}, ['name', 'interest_amount', 'payable_principal_amount'], as_dict=1) + + self.append('repayment_details', { + 'loan_interest_accrual': lia.name, + 'paid_interest_amount': lia.interest_amount, + 'paid_principal_amount': lia.payable_principal_amount + }) + def update_paid_amount(self): precision = cint(frappe.db.get_default("currency_precision")) or 2 @@ -116,6 +140,7 @@ class LoanRepayment(AccountsController): def allocate_amounts(self, paid_entries): self.set('repayment_details', []) self.principal_amount_paid = 0 + total_interest_paid = 0 interest_paid = self.amount_paid - self.penalty_amount if self.amount_paid - self.penalty_amount > 0 and paid_entries: @@ -137,12 +162,17 @@ class LoanRepayment(AccountsController): interest_paid = 0 paid_principal=0 + total_interest_paid += interest_amount self.append('repayment_details', { 'loan_interest_accrual': lia, 'paid_interest_amount': interest_amount, 'paid_principal_amount': paid_principal }) + if self.payment_type == 'Loan Closure' and total_interest_paid < self.interest_payable: + unaccrued_interest = self.interest_payable - total_interest_paid + interest_paid -= unaccrued_interest + if interest_paid: self.principal_amount_paid += interest_paid @@ -281,7 +311,7 @@ def get_amounts(amounts, against_loan, posting_date, payment_type): due_date = add_days(entry.posting_date, 1) no_of_late_days = date_diff(posting_date, - add_days(due_date, loan_type_details.grace_period_in_days)) + add_days(due_date, loan_type_details.grace_period_in_days)) if no_of_late_days > 0 and (not against_loan_doc.repay_from_salary): penalty_amount += (entry.interest_amount * (loan_type_details.penalty_interest_rate / 100) * no_of_late_days)/365 @@ -297,7 +327,10 @@ def get_amounts(amounts, against_loan, posting_date, payment_type): if not final_due_date: final_due_date = add_days(due_date, loan_type_details.grace_period_in_days) - pending_principal_amount = against_loan_doc.total_payment - against_loan_doc.total_principal_paid - against_loan_doc.total_interest_payable + if against_loan_doc.status in ('Disbursed', 'Loan Closure Requested', 'Closed'): + pending_principal_amount = against_loan_doc.total_payment - against_loan_doc.total_principal_paid - against_loan_doc.total_interest_payable + else: + pending_principal_amount = against_loan_doc.disbursed_amount if payment_type == "Loan Closure": if due_date: diff --git a/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.json b/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.json index 4572e992998..7dd5725e2e7 100644 --- a/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.json +++ b/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.json @@ -21,6 +21,10 @@ "total_security_value", "column_break_11", "maximum_loan_value", + "more_information_section", + "reference_no", + "column_break_18", + "description", "amended_from" ], "fields": [ @@ -129,11 +133,34 @@ "label": "Applicant Type", "options": "Employee\nMember\nCustomer", "reqd": 1 + }, + { + "collapsible": 1, + "fieldname": "more_information_section", + "fieldtype": "Section Break", + "label": "More Information" + }, + { + "allow_on_submit": 1, + "fieldname": "reference_no", + "fieldtype": "Data", + "label": "Reference No" + }, + { + "fieldname": "column_break_18", + "fieldtype": "Column Break" + }, + { + "allow_on_submit": 1, + "fieldname": "description", + "fieldtype": "Text", + "label": "Description" } ], + "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-07-02 23:38:24.002382", + "modified": "2020-09-04 22:38:19.894488", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Security Pledge", diff --git a/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py b/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py index 02efe240bd1..0f42bde3c4e 100644 --- a/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py +++ b/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals import frappe -from frappe.utils import get_datetime +from frappe.utils import get_datetime, flt from frappe.model.document import Document from six import iteritems from erpnext.loan_management.doctype.loan_security_unpledge.loan_security_unpledge import get_pledged_security_qty @@ -51,13 +51,19 @@ def check_for_ltv_shortfall(process_loan_security_shortfall): "valid_upto": (">=", update_time) }, as_list=1)) - loans = frappe.get_all('Loan', fields=['name', 'loan_amount', 'total_principal_paid'], - filters={'status': 'Disbursed', 'is_secured_loan': 1}) + loans = frappe.get_all('Loan', fields=['name', 'loan_amount', 'total_principal_paid', 'total_payment', + 'total_interest_payable', 'disbursed_amount', 'status'], + filters={'status': ('in',['Disbursed','Partially Disbursed']), 'is_secured_loan': 1}) loan_security_map = {} for loan in loans: - outstanding_amount = loan.loan_amount - loan.total_principal_paid + if loan.status == 'Disbursed': + outstanding_amount = flt(loan.total_payment) - flt(loan.total_interest_payable) \ + - flt(loan.total_principal_paid) + else: + outstanding_amount = loan.disbursed_amount + pledged_securities = get_pledged_security_qty(loan.name) ltv_ratio = '' security_value = 0.0 diff --git a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.json b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.json index aece46ffda6..2e2b2518d2c 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 5e9d82aa918..b3eb6001e42 100644 --- a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py +++ b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py @@ -17,7 +17,6 @@ class LoanSecurityUnpledge(Document): self.validate_unpledge_qty() def on_cancel(self): - self.update_loan_security_pledge(cancel=1) self.update_loan_status(cancel=1) self.db_set('status', 'Requested') @@ -43,13 +42,14 @@ class LoanSecurityUnpledge(Document): "valid_upto": (">=", get_datetime()) }, as_list=1)) - loan_amount, principal_paid = frappe.get_value("Loan", self.loan, ['loan_amount', 'total_principal_paid']) - pending_principal_amount = loan_amount - principal_paid + total_payment, principal_paid, interest_payable = frappe.get_value("Loan", self.loan, ['total_payment', 'total_principal_paid', + 'total_interest_payable']) + + pending_principal_amount = flt(total_payment) - flt(interest_payable) - flt(principal_paid) security_value = 0 for security in self.securities: - pledged_qty = pledge_qty_map.get(security.loan_security) - + pledged_qty = pledge_qty_map.get(security.loan_security, 0) if security.qty > pledged_qty: frappe.throw(_("""Row {0}: {1} {2} of {3} is pledged against Loan {4}. You are trying to unpledge more""").format(security.idx, pledged_qty, security.uom, @@ -58,16 +58,23 @@ class LoanSecurityUnpledge(Document): qty_after_unpledge = pledged_qty - security.qty ltv_ratio = ltv_ratio_map.get(security.loan_security_type) - security_value += qty_after_unpledge * loan_security_price_map.get(security.loan_security) + current_price = loan_security_price_map.get(security.loan_security) + if not current_price: + frappe.throw(_("No valid Loan Security Price found for {0}").format(frappe.bold(security.loan_security))) - if not security_value and pending_principal_amount > 0: + security_value += qty_after_unpledge * current_price + + if not security_value and flt(pending_principal_amount, 2) > 0: frappe.throw("Cannot Unpledge, loan to value ratio is breaching") - if security_value and (pending_principal_amount/security_value) * 100 > ltv_ratio: + if security_value and flt(pending_principal_amount/security_value) * 100 > ltv_ratio: frappe.throw("Cannot Unpledge, loan to value ratio is breaching") def on_update_after_submit(self): - if self.status == "Approved": + self.approve() + + def approve(self): + if self.status == "Approved" and not self.unpledge_time: self.update_loan_status() self.db_set('unpledge_time', get_datetime()) diff --git a/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.py b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.py index cd3cf7ec96b..0fa96860d08 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 add7bbfa57d..cba6a2d0149 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 d8ae17b4c7f..3c307e920fc 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/job_card/job_card.js b/erpnext/manufacturing/doctype/job_card/job_card.js index bab0dfb6b43..b051b3243fd 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 fba670c1c15..087ab6b484b 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 c29d4ba3d5c..8855e0acf59 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 ca05fea0f6f..b6a6c33d37f 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 2260befb3f8..b7c7c328697 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -7,7 +7,7 @@ import unittest import frappe from frappe.utils import flt, time_diff_in_hours, now, add_months, cint, today from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory -from erpnext.manufacturing.doctype.work_order.work_order import (make_stock_entry, +from erpnext.manufacturing.doctype.work_order.work_order import (make_stock_entry, ItemHasVariantError, stop_unstop, StockOverProductionError, OverProductionError, CapacityError) from erpnext.stock.doctype.stock_entry import test_stock_entry from erpnext.stock.utils import get_bin diff --git a/erpnext/manufacturing/doctype/workstation/test_workstation.py b/erpnext/manufacturing/doctype/workstation/test_workstation.py index 21692608548..8266cf7b779 100644 --- a/erpnext/manufacturing/doctype/workstation/test_workstation.py +++ b/erpnext/manufacturing/doctype/workstation/test_workstation.py @@ -20,3 +20,18 @@ class TestWorkstation(unittest.TestCase): "_Test Workstation 1", "Operation 1", "2013-02-02 05:00:00", "2013-02-02 20:00:00") self.assertRaises(WorkstationHolidayError, check_if_within_operating_hours, "_Test Workstation 1", "Operation 1", "2013-02-01 10:00:00", "2013-02-02 20:00:00") + +def make_workstation(**args): + args = frappe._dict(args) + + try: + doc = frappe.get_doc({ + "doctype": "Workstation", + "workstation_name": args.workstation_name + }) + + doc.insert() + + return doc + except frappe.DuplicateEntryError: + return frappe.get_doc("Workstation", args.workstation_name) \ No newline at end of file diff --git a/erpnext/non_profit/doctype/member/member.json b/erpnext/non_profit/doctype/member/member.json index bb73a843eeb..77cdb94b3d2 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 c52082ca23d..44b975e9e9d 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 554549a0bd1..ee8a8c0a7ba 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 238f4c31fd3..95bb3a5d849 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 729e111e577..f058004ff91 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 8c0e3a4fa76..1d894027b01 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 52b9d01088b..5b6bab5b0a0 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 226981dc783..43311a2c965 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 319078fd6c3..6ce1ecde123 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 ed6b5496003..b95b04316f2 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 a8648f561e9..6c58f2f452d 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -632,7 +632,7 @@ execute:frappe.reload_doc('desk', 'doctype', 'dashboard_chart_source') execute:frappe.reload_doc('desk', 'doctype', 'dashboard_chart') execute:frappe.reload_doc('desk', 'doctype', 'dashboard_chart_field') erpnext.patches.v12_0.remove_bank_remittance_custom_fields -erpnext.patches.v12_0.generate_leave_ledger_entries +erpnext.patches.v12_0.generate_leave_ledger_entries #27-08-2020 execute:frappe.delete_doc_if_exists("Report", "Loan Repayment") erpnext.patches.v12_0.move_credit_limit_to_customer_credit_limit erpnext.patches.v12_0.add_variant_of_in_item_attribute_table @@ -697,7 +697,7 @@ erpnext.patches.v12_0.update_bom_in_so_mr execute:frappe.delete_doc("Report", "Department Analytics") execute:frappe.rename_doc("Desk Page", "Loan Management", "Loan", force=True) erpnext.patches.v12_0.update_uom_conversion_factor -execute:frappe.delete_doc_if_exists("Page", "pos") #29-05-2020 +erpnext.patches.v13_0.replace_pos_page_with_point_of_sale_page erpnext.patches.v13_0.delete_old_purchase_reports erpnext.patches.v12_0.set_italian_import_supplier_invoice_permissions erpnext.patches.v13_0.update_subscription @@ -722,4 +722,6 @@ 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/generate_leave_ledger_entries.py b/erpnext/patches/v12_0/generate_leave_ledger_entries.py index c5bec19fed4..342c12996d1 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/v13_0/drop_razorpay_payload_column.py b/erpnext/patches/v13_0/drop_razorpay_payload_column.py new file mode 100644 index 00000000000..8980fd00392 --- /dev/null +++ b/erpnext/patches/v13_0/drop_razorpay_payload_column.py @@ -0,0 +1,7 @@ +from __future__ import unicode_literals +import frappe + +def execute(): + if frappe.db.exists("DocType", "Membership"): + if 'webhook_payload' in frappe.db.get_table_columns("Membership"): + frappe.db.sql("alter table `tabMembership` drop column webhook_payload") \ No newline at end of file diff --git a/erpnext/patches/v13_0/replace_pos_page_with_point_of_sale_page.py b/erpnext/patches/v13_0/replace_pos_page_with_point_of_sale_page.py new file mode 100644 index 00000000000..390e217cada --- /dev/null +++ b/erpnext/patches/v13_0/replace_pos_page_with_point_of_sale_page.py @@ -0,0 +1,6 @@ +from __future__ import unicode_literals +import frappe + +def execute(): + if frappe.db.exists("Page", "point-of-sale"): + frappe.rename_doc("Page", "pos", "point-of-sale", 1, 1) \ No newline at end of file diff --git a/erpnext/patches/v13_0/setting_custom_roles_for_some_regional_reports.py b/erpnext/patches/v13_0/setting_custom_roles_for_some_regional_reports.py new file mode 100644 index 00000000000..ecc7822e1d7 --- /dev/null +++ b/erpnext/patches/v13_0/setting_custom_roles_for_some_regional_reports.py @@ -0,0 +1,10 @@ +from __future__ import unicode_literals +import frappe +from erpnext.regional.india.setup import add_custom_roles_for_reports + +def execute(): + company = frappe.get_all('Company', filters = {'country': 'India'}) + if not company: + return + + add_custom_roles_for_reports() \ No newline at end of file diff --git a/erpnext/patches/v13_0/stock_entry_enhancements.py b/erpnext/patches/v13_0/stock_entry_enhancements.py index dcc4f956f77..0bdcc9c0e88 100644 --- a/erpnext/patches/v13_0/stock_entry_enhancements.py +++ b/erpnext/patches/v13_0/stock_entry_enhancements.py @@ -24,4 +24,8 @@ def execute(): if not frappe.db.exists('Warehouse Type', 'Transit'): doc = frappe.new_doc('Warehouse Type') doc.name = 'Transit' - doc.insert() \ No newline at end of file + doc.insert() + + frappe.reload_doc("stock", "doctype", "stock_entry_type") + frappe.delete_doc_if_exists("Stock Entry Type", "Send to Warehouse") + frappe.delete_doc_if_exists("Stock Entry Type", "Receive at Warehouse") \ No newline at end of file diff --git a/erpnext/payroll/doctype/additional_salary/additional_salary.py b/erpnext/payroll/doctype/additional_salary/additional_salary.py index ef174bdea20..e3dc9070ec5 100644 --- a/erpnext/payroll/doctype/additional_salary/additional_salary.py +++ b/erpnext/payroll/doctype/additional_salary/additional_salary.py @@ -5,8 +5,8 @@ from __future__ import unicode_literals import frappe from frappe.model.document import Document -from frappe import _ -from frappe.utils import getdate, date_diff +from frappe import _, bold +from frappe.utils import getdate, date_diff, comma_and, formatdate class AdditionalSalary(Document): @@ -22,9 +22,37 @@ class AdditionalSalary(Document): def validate(self): self.validate_dates() + self.validate_recurring_additional_salary_overlap() if self.amount < 0: frappe.throw(_("Amount should not be less than zero.")) + def validate_recurring_additional_salary_overlap(self): + if self.is_recurring: + additional_salaries = frappe.db.sql(""" + SELECT + name + FROM `tabAdditional Salary` + WHERE + employee=%s + AND name <> %s + AND docstatus=1 + AND is_recurring=1 + AND salary_component = %s + AND to_date >= %s + AND from_date <= %s""", + (self.employee, self.name, self.salary_component, self.from_date, self.to_date), as_dict = 1) + + additional_salaries = [salary.name for salary in additional_salaries] + + if additional_salaries and len(additional_salaries): + frappe.throw(_("Additional Salary: {0} already exist for Salary Component: {1} for period {2} and {3}").format( + bold(comma_and(additional_salaries)), + bold(self.salary_component), + bold(formatdate(self.from_date)), + bold(formatdate(self.to_date) + ))) + + def validate_dates(self): date_of_joining, relieving_date = frappe.db.get_value("Employee", self.employee, ["date_of_joining", "relieving_date"]) diff --git a/erpnext/portal/product_configurator/utils.py b/erpnext/portal/product_configurator/utils.py index 9eef16bed36..9ba4cdc5145 100644 --- a/erpnext/portal/product_configurator/utils.py +++ b/erpnext/portal/product_configurator/utils.py @@ -13,13 +13,15 @@ def get_field_filter_data(): for f in fields: doctype = f.get_link_doctype() - # apply enable/disable filter + # apply enable/disable/show_in_website filter meta = frappe.get_meta(doctype) filters = {} if meta.has_field('enabled'): filters['enabled'] = 1 if meta.has_field('disabled'): filters['disabled'] = 0 + if meta.has_field('show_in_website'): + filters['show_in_website'] = 1 values = [d.name for d in frappe.get_all(doctype, filters)] filter_data.append([f, values]) diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js index a4cc68b3e2c..cb76c87b625 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/transaction.js b/erpnext/public/js/controllers/transaction.js index 436a232a551..792235f7a30 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -781,10 +781,23 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ else var date = this.frm.doc.transaction_date; if (frappe.meta.get_docfield(this.frm.doctype, "shipping_address") && - in_list(['Purchase Order', 'Purchase Receipt', 'Purchase Invoice'], this.frm.doctype)){ + in_list(['Purchase Order', 'Purchase Receipt', 'Purchase Invoice'], this.frm.doctype)) { erpnext.utils.get_shipping_address(this.frm, function(){ set_party_account(set_pricing); }) + + // Get default company billing address in Purchase Invoice, Order and Receipt + frappe.call({ + 'method': 'frappe.contacts.doctype.address.address.get_default_address', + 'args': { + 'doctype': 'Company', + 'name': this.frm.doc.company + }, + 'callback': function(r) { + me.frm.set_value('billing_address', r.message); + } + }); + } else { set_party_account(set_pricing); } diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index 290694a7899..cbcd6e3203a 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 fbccc6b0780..3b6a28f52c0 100644 --- a/erpnext/regional/india/taxes.js +++ b/erpnext/regional/india/taxes.js @@ -6,6 +6,9 @@ erpnext.setup_auto_gst_taxation = (doctype) => { shipping_address: function(frm) { frm.trigger('get_tax_template'); }, + supplier_address: function(frm) { + frm.trigger('get_tax_template'); + }, tax_category: function(frm) { frm.trigger('get_tax_template'); }, diff --git a/erpnext/regional/report/gstr_1/gstr_1.json b/erpnext/regional/report/gstr_1/gstr_1.json index 2012bb8840b..75aed8cffcc 100644 --- a/erpnext/regional/report/gstr_1/gstr_1.json +++ b/erpnext/regional/report/gstr_1/gstr_1.json @@ -7,7 +7,7 @@ "doctype": "Report", "idx": 0, "is_standard": "Yes", - "modified": "2019-06-30 19:33:59.769385", + "modified": "2019-09-03 19:33:59.769385", "modified_by": "Administrator", "module": "Regional", "name": "GSTR-1", @@ -16,15 +16,5 @@ "ref_doctype": "GL Entry", "report_name": "GSTR-1", "report_type": "Script Report", - "roles": [ - { - "role": "Accounts User" - }, - { - "role": "Accounts Manager" - }, - { - "role": "Auditor" - } - ] + "roles": [] } \ No newline at end of file diff --git a/erpnext/regional/report/gstr_2/gstr_2.json b/erpnext/regional/report/gstr_2/gstr_2.json index 929ed914d83..b70d0f9416b 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 dfdf9dc0958..b757d53aa23 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 124a7201349..cc6ad574af7 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 a3ed4cebb12..59389ce3269 100644 --- a/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py +++ b/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py @@ -4,11 +4,13 @@ from __future__ import unicode_literals import frappe, erpnext from frappe import _ -from frappe.utils import flt +from frappe.utils import flt, getdate, cstr from frappe.model.meta import get_field_precision from frappe.utils.xlsxutils import handle_html from six import iteritems import json +from erpnext.regional.india.utils import get_gst_accounts +from erpnext.regional.report.gstr_1.gstr_1 import get_company_gstin_number def execute(filters=None): return _execute(filters) @@ -141,7 +143,7 @@ def get_tax_accounts(item_list, columns, company_currency, doctype="Sales Invoic tax_details = frappe.db.sql(""" select - parent, description, item_wise_tax_detail, + parent, account_head, item_wise_tax_detail, base_tax_amount_after_discount_amount from `tab%s` where @@ -153,11 +155,11 @@ def get_tax_accounts(item_list, columns, company_currency, doctype="Sales Invoic """ % (tax_doctype, '%s', ', '.join(['%s']*len(invoice_item_row)), conditions), tuple([doctype] + list(invoice_item_row))) - for parent, description, item_wise_tax_detail, tax_amount in tax_details: - description = handle_html(description) - if description not in tax_columns and tax_amount: + for parent, account_head, item_wise_tax_detail, tax_amount in tax_details: + + if account_head not in tax_columns and tax_amount: # as description is text editor earlier and markup can break the column convention in reports - tax_columns.append(description) + tax_columns.append(account_head) if item_wise_tax_detail: try: @@ -175,17 +177,17 @@ def get_tax_accounts(item_list, columns, company_currency, doctype="Sales Invoic for d in item_row_map.get(parent, {}).get(item_code, []): item_tax_amount = tax_amount if item_tax_amount: - itemised_tax.setdefault((parent, item_code), {})[description] = frappe._dict({ + itemised_tax.setdefault((parent, item_code), {})[account_head] = frappe._dict({ "tax_amount": flt(item_tax_amount, tax_amount_precision) }) except ValueError: continue tax_columns.sort() - for desc in tax_columns: + for account_head in tax_columns: columns.append({ - "label": desc, - "fieldname": frappe.scrub(desc), + "label": account_head, + "fieldname": frappe.scrub(account_head), "fieldtype": "Float", "width": 110 }) @@ -212,3 +214,76 @@ def get_merged_data(columns, data): return result +@frappe.whitelist() +def get_json(filters, report_name, data): + filters = json.loads(filters) + report_data = json.loads(data) + gstin = filters.get('company_gstin') or get_company_gstin_number(filters["company"]) + + if not filters.get('from_date') or not filters.get('to_date'): + frappe.throw(_("Please enter From Date and To Date to generate JSON")) + + fp = "%02d%s" % (getdate(filters["to_date"]).month, getdate(filters["to_date"]).year) + + gst_json = {"version": "GST2.3.4", + "hash": "hash", "gstin": gstin, "fp": fp} + + gst_json["hsn"] = { + "data": get_hsn_wise_json_data(filters, report_data) + } + + return { + 'report_name': report_name, + 'data': gst_json + } + +@frappe.whitelist() +def download_json_file(): + '''download json content in a file''' + data = frappe._dict(frappe.local.form_dict) + frappe.response['filename'] = frappe.scrub("{0}".format(data['report_name'])) + '.json' + frappe.response['filecontent'] = data['data'] + frappe.response['content_type'] = 'application/json' + frappe.response['type'] = 'download' + +def get_hsn_wise_json_data(filters, report_data): + + filters = frappe._dict(filters) + gst_accounts = get_gst_accounts(filters.company) + data = [] + count = 1 + + for hsn in report_data: + row = { + "num": count, + "hsn_sc": hsn.get("gst_hsn_code"), + "desc": hsn.get("description"), + "uqc": hsn.get("stock_uom").upper(), + "qty": hsn.get("stock_qty"), + "val": flt(hsn.get("total_amount"), 2), + "txval": flt(hsn.get("taxable_amount", 2)), + "iamt": 0.0, + "camt": 0.0, + "samt": 0.0, + "csamt": 0.0 + + } + + for account in gst_accounts.get('igst_account'): + row['iamt'] += flt(hsn.get(frappe.scrub(cstr(account)), 0.0), 2) + + for account in gst_accounts.get('cgst_account'): + row['camt'] += flt(hsn.get(frappe.scrub(cstr(account)), 0.0), 2) + + for account in gst_accounts.get('sgst_account'): + row['samt'] += flt(hsn.get(frappe.scrub(cstr(account)), 0.0), 2) + + for account in gst_accounts.get('cess_account'): + row['csamt'] += flt(hsn.get(frappe.scrub(cstr(account)), 0.0), 2) + + data.append(row) + count +=1 + + return data + + diff --git a/erpnext/regional/report/irs_1099/irs_1099.py b/erpnext/regional/report/irs_1099/irs_1099.py index 67834d12210..d3509e500f8 100644 --- a/erpnext/regional/report/irs_1099/irs_1099.py +++ b/erpnext/regional/report/irs_1099/irs_1099.py @@ -16,9 +16,15 @@ from frappe.utils.jinja import render_template def execute(filters=None): filters = filters if isinstance(filters, _dict) else _dict(filters) + if not filters: filters.setdefault('fiscal_year', get_fiscal_year(nowdate())[0]) filters.setdefault('company', frappe.db.get_default("company")) + + region = frappe.db.get_value("Company", fieldname = ["country"], filters = { "name": filters.company }) + if region != 'United States': + return [],[] + data = [] columns = get_columns() data = frappe.db.sql(""" diff --git a/erpnext/regional/united_states/test_united_states.py b/erpnext/regional/united_states/test_united_states.py index 688f14576c8..ad95010a9ac 100644 --- a/erpnext/regional/united_states/test_united_states.py +++ b/erpnext/regional/united_states/test_united_states.py @@ -24,7 +24,7 @@ class TestUnitedStates(unittest.TestCase): def test_irs_1099_report(self): make_payment_entry_to_irs_1099_supplier() - filters = frappe._dict({"fiscal_year": "_Test Fiscal Year 2016", "company": "_Test Company"}) + filters = frappe._dict({"fiscal_year": "_Test Fiscal Year 2016", "company": "_Test Company 1"}) columns, data = execute_1099_report(filters) print(columns, data) expected_row = {'supplier': '_US 1099 Test Supplier', @@ -42,10 +42,10 @@ def make_payment_entry_to_irs_1099_supplier(): pe = frappe.new_doc("Payment Entry") pe.payment_type = "Pay" - pe.company = "_Test Company" + pe.company = "_Test Company 1" pe.posting_date = "2016-01-10" - pe.paid_from = "_Test Bank USD - _TC" - pe.paid_to = "_Test Payable USD - _TC" + pe.paid_from = "_Test Bank USD - _TC1" + pe.paid_to = "_Test Payable USD - _TC1" pe.paid_amount = 100 pe.received_amount = 100 pe.reference_no = "For IRS 1099 testing" diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index 93d48321739..1f955fcd52e 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -184,10 +184,10 @@ class Customer(TransactionBase): def validate_credit_limit_on_change(self): if self.get("__islocal") or not self.credit_limits: return - + past_credit_limits = [d.credit_limit for d in frappe.db.get_all("Customer Credit Limit", filters={'parent': self.name}, fields=["credit_limit"], order_by="company")] - + current_credit_limits = [d.credit_limit for d in sorted(self.credit_limits, key=lambda k: k.company)] if past_credit_limits == current_credit_limits: @@ -396,13 +396,12 @@ def check_credit_limit(customer, company, ignore_outstanding_sales_order=False, credit_controller_users = get_users_with_role(credit_controller_role or "Sales Master Manager") # form a list of emails and names to show to the user - credit_controller_users = [get_formatted_email(user).replace("<", "(").replace(">", ")") for user in credit_controller_users] - - if not credit_controller_users: + credit_controller_users_formatted = [get_formatted_email(user).replace("<", "(").replace(">", ")") for user in credit_controller_users] + if not credit_controller_users_formatted: frappe.throw(_("Please contact your administrator to extend the credit limits for {0}.".format(customer))) message = """Please contact any of the following users to extend the credit limits for {0}: -

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

    """.format(customer, '
  • '.join(credit_controller_users_formatted)) # if the current user does not have permissions to override credit limit, # prompt them to send out an email to the controller users @@ -427,7 +426,7 @@ def send_emails(args): subject = (_("Credit limit reached for customer {0}").format(args.get('customer'))) message = (_("Credit limit has been crossed for customer {0} ({1}/{2})") .format(args.get('customer'), args.get('customer_outstanding'), args.get('credit_limit'))) - frappe.sendmail(recipients=[args.get('credit_controller_users_list')], subject=subject, message=message) + frappe.sendmail(recipients=args.get('credit_controller_users_list'), subject=subject, message=message) def get_customer_outstanding(customer, company, ignore_outstanding_sales_order=False, cost_center=None): # Outstanding based on GL Entries diff --git a/erpnext/selling/page/sales_funnel/sales_funnel.py b/erpnext/selling/page/sales_funnel/sales_funnel.py index dba24ef5b00..b613718c7e2 100644 --- a/erpnext/selling/page/sales_funnel/sales_funnel.py +++ b/erpnext/selling/page/sales_funnel/sales_funnel.py @@ -20,29 +20,28 @@ def get_funnel_data(from_date, to_date, company): validate_filters(from_date, to_date, company) active_leads = frappe.db.sql("""select count(*) from `tabLead` - where (date(`modified`) between %s and %s) - and status != "Do Not Contact" and company=%s""", (from_date, to_date, company))[0][0] - - active_leads += frappe.db.sql("""select count(distinct contact.name) from `tabContact` contact - left join `tabDynamic Link` dl on (dl.parent=contact.name) where dl.link_doctype='Customer' - and (date(contact.modified) between %s and %s) and status != "Passive" """, (from_date, to_date))[0][0] + where (date(`creation`) between %s and %s) + and company=%s""", (from_date, to_date, company))[0][0] opportunities = frappe.db.sql("""select count(*) from `tabOpportunity` where (date(`creation`) between %s and %s) - and status != "Lost" and company=%s""", (from_date, to_date, company))[0][0] + and opportunity_from='Lead' and company=%s""", (from_date, to_date, company))[0][0] quotations = frappe.db.sql("""select count(*) from `tabQuotation` where docstatus = 1 and (date(`creation`) between %s and %s) - and status != "Lost" and company=%s""", (from_date, to_date, company))[0][0] + and (opportunity!="" or quotation_to="Lead") and company=%s""", (from_date, to_date, company))[0][0] + + converted = frappe.db.sql("""select count(*) from `tabCustomer` + JOIN `tabLead` ON `tabLead`.name = `tabCustomer`.lead_name + WHERE (date(`tabCustomer`.creation) between %s and %s) + and `tabLead`.company=%s""", (from_date, to_date, company))[0][0] - sales_orders = frappe.db.sql("""select count(*) from `tabSales Order` - where docstatus = 1 and (date(`creation`) between %s and %s) and company=%s""", (from_date, to_date, company))[0][0] return [ - { "title": _("Active Leads / Customers"), "value": active_leads, "color": "#B03B46" }, + { "title": _("Active Leads"), "value": active_leads, "color": "#B03B46" }, { "title": _("Opportunities"), "value": opportunities, "color": "#F09C00" }, { "title": _("Quotations"), "value": quotations, "color": "#006685" }, - { "title": _("Sales Orders"), "value": sales_orders, "color": "#00AD65" } + { "title": _("Converted"), "value": converted, "color": "#00AD65" } ] @frappe.whitelist() diff --git a/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/item_group_wise_sales_target_variance.py b/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/item_group_wise_sales_target_variance.py index 857b9823e03..ae216ca5d69 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 f99f68c5240..38bb127e235 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 9f6bfc41dfb..a8e2fad3734 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 dd9607ffbdb..263391a7f72 100644 --- a/erpnext/selling/report/territory_target_variance_based_on_item_group/territory_target_variance_based_on_item_group.js +++ b/erpnext/selling/report/territory_target_variance_based_on_item_group/territory_target_variance_based_on_item_group.js @@ -44,5 +44,20 @@ frappe.query_reports["Territory Target Variance Based On Item Group"] = { options: "Quantity\nAmount", default: "Quantity" }, - ] + ], + "formatter": function (value, row, column, data, default_formatter) { + value = default_formatter(value, row, column, data); + + if (column.fieldname.includes('variance')) { + + if (data[column.fieldname] < 0) { + value = "" + value + ""; + } + else if (data[column.fieldname] > 0) { + value = "" + value + ""; + } + } + + return value; + } } diff --git a/erpnext/setup/install.py b/erpnext/setup/install.py index 4f0f5721c2f..2225fe169f5 100644 --- a/erpnext/setup/install.py +++ b/erpnext/setup/install.py @@ -20,6 +20,7 @@ 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() @@ -66,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'), diff --git a/erpnext/startup/leaderboard.py b/erpnext/startup/leaderboard.py index 5545f13e8ca..ef238f1165d 100644 --- a/erpnext/startup/leaderboard.py +++ b/erpnext/startup/leaderboard.py @@ -123,7 +123,8 @@ def get_all_suppliers(date_range, company, field, limit = None): if field == "outstanding_amount": filters = [['docstatus', '=', '1'], ['company', '=', company]] if date_range: - filters.append(['posting_date', 'between' [date_range[0], date_range[1]]]) + date_range = frappe.parse_json(date_range) + filters.append(['posting_date', 'between', [date_range[0], date_range[1]]]) return frappe.db.get_all('Purchase Invoice', fields = ['supplier as name', 'sum(outstanding_amount) as value'], filters = filters, diff --git a/erpnext/stock/desk_page/stock/stock.json b/erpnext/stock/desk_page/stock/stock.json index 1bf81f7f0e8..2fba5fa8044 100644 --- a/erpnext/stock/desk_page/stock/stock.json +++ b/erpnext/stock/desk_page/stock/stock.json @@ -33,7 +33,7 @@ { "hidden": 0, "label": "Key Reports", - "links": "[\n {\n \"dependencies\": [\n \"Item Price\"\n ],\n \"doctype\": \"Item Price\",\n \"is_query_report\": false,\n \"label\": \"Item-wise Price List Rate\",\n \"name\": \"Item-wise Price List Rate\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Stock Entry\"\n ],\n \"doctype\": \"Stock Entry\",\n \"is_query_report\": true,\n \"label\": \"Stock Analytics\",\n \"name\": \"Stock Analytics\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Delivery Note\"\n ],\n \"doctype\": \"Delivery Note\",\n \"is_query_report\": true,\n \"label\": \"Delivery Note Trends\",\n \"name\": \"Delivery Note Trends\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Purchase Receipt\"\n ],\n \"doctype\": \"Purchase Receipt\",\n \"is_query_report\": true,\n \"label\": \"Purchase Receipt Trends\",\n \"name\": \"Purchase Receipt Trends\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Order\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Sales Order Analysis\",\n \"name\": \"Sales Order Analysis\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Purchase Order\"\n ],\n \"doctype\": \"Purchase Order\",\n \"is_query_report\": true,\n \"label\": \"Purchase Order Analysis\",\n \"name\": \"Purchase Order Analysis\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Bin\"\n ],\n \"doctype\": \"Bin\",\n \"is_query_report\": true,\n \"label\": \"Item Shortage Report\",\n \"name\": \"Item Shortage Report\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Batch\"\n ],\n \"doctype\": \"Batch\",\n \"is_query_report\": true,\n \"label\": \"Batch-Wise Balance History\",\n \"name\": \"Batch-Wise Balance History\",\n \"type\": \"report\"\n }\n]" + "links": "[\n {\n \"dependencies\": [\n \"Item Price\"\n ],\n \"doctype\": \"Item Price\",\n \"is_query_report\": false,\n \"label\": \"Item-wise Price List Rate\",\n \"name\": \"Item-wise Price List Rate\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Stock Entry\"\n ],\n \"doctype\": \"Stock Entry\",\n \"is_query_report\": true,\n \"label\": \"Stock Analytics\",\n \"name\": \"Stock Analytics\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"doctype\": \"Item\",\n \"is_query_report\": true,\n \"label\": \"Stock Qty vs Serial No Count\",\n \"name\": \"Stock Qty vs Serial No Count\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Delivery Note\"\n ],\n \"doctype\": \"Delivery Note\",\n \"is_query_report\": true,\n \"label\": \"Delivery Note Trends\",\n \"name\": \"Delivery Note Trends\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Purchase Receipt\"\n ],\n \"doctype\": \"Purchase Receipt\",\n \"is_query_report\": true,\n \"label\": \"Purchase Receipt Trends\",\n \"name\": \"Purchase Receipt Trends\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Order\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Sales Order Analysis\",\n \"name\": \"Sales Order Analysis\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Purchase Order\"\n ],\n \"doctype\": \"Purchase Order\",\n \"is_query_report\": true,\n \"label\": \"Purchase Order Analysis\",\n \"name\": \"Purchase Order Analysis\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Bin\"\n ],\n \"doctype\": \"Bin\",\n \"is_query_report\": true,\n \"label\": \"Item Shortage Report\",\n \"name\": \"Item Shortage Report\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Batch\"\n ],\n \"doctype\": \"Batch\",\n \"is_query_report\": true,\n \"label\": \"Batch-Wise Balance History\",\n \"name\": \"Batch-Wise Balance History\",\n \"type\": \"report\"\n }\n]" }, { "hidden": 0, @@ -58,7 +58,7 @@ "idx": 0, "is_standard": 1, "label": "Stock", - "modified": "2020-05-30 17:32:11.062681", + "modified": "2020-08-11 17:29:32.626067", "modified_by": "Administrator", "module": "Stock", "name": "Stock", diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 30bcccdda65..a92d04ff8cd 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -513,7 +513,7 @@ class StockEntry(StockController): d.basic_amount = flt((raw_material_cost - scrap_material_cost), d.precision("basic_amount")) elif self.purpose == "Repack" and total_fg_qty and not d.set_basic_rate_manually: d.basic_rate = flt(raw_material_cost) / flt(total_fg_qty) - d.basic_amount = d.basic_rate * d.qty + d.basic_amount = d.basic_rate * flt(d.qty) def distribute_additional_costs(self): if self.purpose == "Material Issue": diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 43fbc004664..b81f8a086d5 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -258,6 +258,7 @@ class StockReconciliation(StockController): sl_entries.append(args) + qty_after_transaction = 0 for serial_no in serial_nos: args = self.get_sle_for_items(row, [serial_no]) @@ -271,11 +272,19 @@ class StockReconciliation(StockController): if previous_sle and row.warehouse != previous_sle.get("warehouse"): # If serial no exists in different warehouse + warehouse = previous_sle.get("warehouse", '') or row.warehouse + + if not qty_after_transaction: + qty_after_transaction = get_stock_balance(row.item_code, + warehouse, self.posting_date, self.posting_time) + + qty_after_transaction -= 1 + new_args = args.copy() new_args.update({ 'actual_qty': -1, - 'qty_after_transaction': cint(previous_sle.get('qty_after_transaction')) - 1, - 'warehouse': previous_sle.get("warehouse", '') or row.warehouse, + 'qty_after_transaction': qty_after_transaction, + 'warehouse': warehouse, 'valuation_rate': previous_sle.get("valuation_rate") }) diff --git a/erpnext/stock/report/stock_qty_vs_serial_no_count/__init__.py b/erpnext/stock/report/stock_qty_vs_serial_no_count/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.js b/erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.js new file mode 100644 index 00000000000..2a0fd4025cc --- /dev/null +++ b/erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.js @@ -0,0 +1,42 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Stock Qty vs Serial No Count"] = { + "filters": [ + { + "fieldname":"company", + "label": __("Company"), + "fieldtype": "Link", + "options": "Company", + "default": frappe.defaults.get_user_default("Company"), + "reqd": 1 + }, + { + "fieldname":"warehouse", + "label": __("Warehouse"), + "fieldtype": "Link", + "options": "Warehouse", + "get_query": function() { + const company = frappe.query_report.get_filter_value('company'); + return { + filters: { 'company': company } + } + }, + "reqd": 1 + }, + ], + + "formatter": function (value, row, column, data, default_formatter) { + value = default_formatter(value, row, column, data); + if (column.fieldname == "difference" && data) { + if (data.difference > 0) { + value = "" + value + ""; + } + else if (data.difference < 0) { + value = "" + value + ""; + } + } + return value; + } +}; diff --git a/erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.json b/erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.json new file mode 100644 index 00000000000..c7108b513e2 --- /dev/null +++ b/erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.json @@ -0,0 +1,27 @@ +{ + "add_total_row": 0, + "creation": "2020-07-23 19:31:32.395011", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 0, + "is_standard": "Yes", + "modified": "2020-07-23 19:32:02.168185", + "modified_by": "Administrator", + "module": "Stock", + "name": "Stock Qty vs Serial No Count", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Item", + "report_name": "Stock Qty vs Serial No Count", + "report_type": "Script Report", + "roles": [ + { + "role": "Stock Manager" + }, + { + "role": "Stock User" + } + ] +} \ No newline at end of file diff --git a/erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py b/erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py new file mode 100644 index 00000000000..55f041c95c8 --- /dev/null +++ b/erpnext/stock/report/stock_qty_vs_serial_no_count/stock_qty_vs_serial_no_count.py @@ -0,0 +1,80 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe import _ + +def execute(filters=None): + columns = get_columns() + data = get_data(filters.warehouse) + return columns, data + +def get_columns(): + columns = [ + { + "label": _("Item Code"), + "fieldname": "item_code", + "fieldtype": "Link", + "options": "Item", + "width": 200 + }, + { + "label": _("Item Name"), + "fieldname": "item_name", + "fieldtype": "Data", + "width": 200 + }, + { + "label": _("Serial No Count"), + "fieldname": "total", + "fieldtype": "Float", + "width": 150 + }, + { + "label": _("Stock Qty"), + "fieldname": "stock_qty", + "fieldtype": "Float", + "width": 150 + }, + { + "label": _("Difference"), + "fieldname": "difference", + "fieldtype": "Float", + "width": 150 + }, + ] + + return columns + +def get_data(warehouse): + serial_item_list = frappe.get_all("Item", filters={ + 'has_serial_no': True, + }, fields=['item_code', 'item_name']) + + status_list = ['Active', 'Expired'] + data = [] + for item in serial_item_list: + total_serial_no = frappe.db.count("Serial No", + filters={"item_code": item.item_code, "status": ("in", status_list), "warehouse": warehouse}) + + actual_qty = frappe.db.get_value('Bin', fieldname=['actual_qty'], + filters={"warehouse": warehouse, "item_code": item.item_code}) + + # frappe.db.get_value returns null if no record exist. + if not actual_qty: + actual_qty = 0 + + difference = total_serial_no - actual_qty + + row = { + "item_code": item.item_code, + "item_name": item.item_name, + "total": total_serial_no, + "stock_qty": actual_qty, + "difference": difference, + } + + data.append(row) + + return data \ No newline at end of file diff --git a/erpnext/templates/generators/item/item_add_to_cart.html b/erpnext/templates/generators/item/item_add_to_cart.html index 2a70d8dbe9e..40bc0c749b7 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 b8b0d98bdc2..73f9ec99b34 100644 --- a/erpnext/templates/generators/item/item_configure.html +++ b/erpnext/templates/generators/item/item_configure.html @@ -10,14 +10,11 @@ {{ _('Configure') }} {% endif %} - {% if cart_settings.show_contact_us_button | int %} - + {% if cart_settings.show_contact_us_button %} + {% include "templates/generators/item/item_inquiry.html" %} {% endif %} {% endif %} diff --git a/erpnext/templates/generators/item/item_inquiry.html b/erpnext/templates/generators/item/item_inquiry.html new file mode 100644 index 00000000000..83653b68218 --- /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/print_formats/includes/item_table_qty.html b/erpnext/templates/print_formats/includes/item_table_qty.html index 239859eea19..ecaaef42b55 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 c23c1f7096d..66d6cd38886 100644 --- a/erpnext/utilities/product.py +++ b/erpnext/utilities/product.py @@ -82,6 +82,7 @@ def get_price(item_code, price_list, customer_group, company, qty=1): pricing_rule = get_pricing_rule_for_item(frappe._dict({ "item_code": item_code, "qty": qty, + "stock_qty": qty, "transaction_type": "selling", "price_list": price_list, "customer_group": customer_group, diff --git a/yarn.lock b/yarn.lock index b19f566fd01..97a063597da 100644 --- a/yarn.lock +++ b/yarn.lock @@ -282,9 +282,9 @@ balanced-match@^1.0.0: integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= bl@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/bl/-/bl-3.0.0.tgz#3611ec00579fd18561754360b21e9f784500ff88" - integrity sha512-EUAyP5UHU5hxF8BPT0LKW8gjYLhq1DQIcneOX/pL/m2Alo+OYDQAJlHq+yseMP50Os2nHXOSic6Ss3vSQeyf4A== + version "3.0.1" + resolved "https://registry.yarnpkg.com/bl/-/bl-3.0.1.tgz#1cbb439299609e419b5a74d7fce2f8b37d8e5c6f" + integrity sha512-jrCW5ZhfQ/Vt07WX1Ngs+yn9BDqPL/gw28S7s9H6QK/gupnizNzJAss5akW20ISgOrbLTlXOOCTJeNUQqruAWQ== dependencies: readable-stream "^3.0.1" @@ -866,12 +866,12 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@^2.0.3, inherits@~2.0.3: +inherits@2, inherits@~2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= -inherits@2.0.4, inherits@~2.0.1: +inherits@2.0.4, inherits@^2.0.3, inherits@~2.0.1: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -1447,9 +1447,9 @@ readable-stream@2, readable-stream@~2.3.6: util-deprecate "~1.0.1" readable-stream@^3.0.1, readable-stream@^3.1.1: - version "3.5.0" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.5.0.tgz#465d70e6d1087f6162d079cd0b5db7fbebfd1606" - integrity sha512-gSz026xs2LfxBPudDuI41V1lka8cxg64E66SGe78zJlsUofOg/yqwezdIcdfwik6B4h8LFmWPA9ef9X3FiNFLA== + version "3.6.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" + integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== dependencies: inherits "^2.0.3" string_decoder "^1.1.1" @@ -1505,9 +1505,9 @@ safe-buffer@^5.0.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1: integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== safe-buffer@~5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519" - integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg== + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== "safer-buffer@>= 2.1.2 < 3": version "2.1.2"