diff --git a/.travis.yml b/.travis.yml index a70062fea3a..80d979f602a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -56,6 +56,6 @@ script: - bench run-tests - sleep 5 - bench reinstall --yes - - bench execute erpnext.setup.setup_wizard.utils.complete + - bench --verbose run-setup-wizard-ui-test - bench execute erpnext.setup.utils.enable_all_roles_and_domains - bench run-ui-tests --app erpnext diff --git a/erpnext/accounts/doctype/account/test_account.js b/erpnext/accounts/doctype/account/test_account.js index 7b23ef01dd6..039e33e011d 100644 --- a/erpnext/accounts/doctype/account/test_account.js +++ b/erpnext/accounts/doctype/account/test_account.js @@ -5,7 +5,9 @@ QUnit.test("test account", function(assert) { let done = assert.async(); frappe.run_serially([ () => frappe.set_route('Tree', 'Account'), + () => frappe.timeout(3), () => frappe.click_button('Expand All'), + () => frappe.timeout(1), () => frappe.click_link('Debtors'), () => frappe.click_button('Edit'), () => frappe.timeout(1), diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.json b/erpnext/accounts/doctype/journal_entry/journal_entry.json index 57e83b0b7af..19f4b569283 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.json +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.json @@ -1337,6 +1337,67 @@ "set_only_once": 0, "unique": 0 }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "subscription_section", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Subscription Section", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 1, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "subscription", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Subscription", + "length": 0, + "no_copy": 1, + "options": "Subscription", + "permlevel": 0, + "precision": "", + "print_hide": 1, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, { "allow_bulk_edit": 0, "allow_on_submit": 0, @@ -1382,7 +1443,7 @@ "istable": 0, "max_attachments": 0, "menu_index": 0, - "modified": "2017-06-13 14:29:09.794076", + "modified": "2017-08-31 11:21:09.442695", "modified_by": "Administrator", "module": "Accounts", "name": "Journal Entry", diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index 12e46c42d3c..dc37574ea6a 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -644,16 +644,9 @@ frappe.ui.form.on('Payment Entry', { if(frm.doc.party) { var party_amount = frm.doc.payment_type=="Receive" ? frm.doc.paid_amount : frm.doc.received_amount; - - var total_deductions = frappe.utils.sum($.map(frm.doc.deductions || [], - function(d) { return flt(d.amount) })); if(frm.doc.total_allocated_amount < party_amount) { - if(frm.doc.payment_type == "Receive") { - unallocated_amount = party_amount - (frm.doc.total_allocated_amount - total_deductions); - } else { - unallocated_amount = party_amount - (frm.doc.total_allocated_amount + total_deductions); - } + unallocated_amount = party_amount - frm.doc.total_allocated_amount; } } frm.set_value("unallocated_amount", unallocated_amount); @@ -672,11 +665,10 @@ frappe.ui.form.on('Payment Entry', { difference_amount = flt(frm.doc.base_paid_amount) - flt(frm.doc.base_received_amount); } - $.each(frm.doc.deductions || [], function(i, d) { - if(d.amount) difference_amount -= flt(d.amount); - }) + var total_deductions = frappe.utils.sum($.map(frm.doc.deductions || [], + function(d) { return flt(d.amount) })); - frm.set_value("difference_amount", difference_amount); + frm.set_value("difference_amount", difference_amount - total_deductions); frm.events.hide_unhide_fields(frm); }, diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.json b/erpnext/accounts/doctype/payment_entry/payment_entry.json index a59223d3a8c..abf4ac9b15b 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.json +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.json @@ -1659,6 +1659,67 @@ "set_only_once": 0, "unique": 0 }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "subscription_section", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Subscription Section", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 1, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "subscription", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Subscription", + "length": 0, + "no_copy": 1, + "options": "Subscription", + "permlevel": 0, + "precision": "", + "print_hide": 1, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, { "allow_bulk_edit": 0, "allow_on_submit": 0, @@ -1730,7 +1791,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2017-06-13 14:29:04.244537", + "modified": "2017-08-31 11:20:37.578469", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Entry", diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 9832c0527a0..908e58eeac1 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -281,13 +281,8 @@ class PaymentEntry(AccountsController): if self.party: party_amount = self.paid_amount if self.payment_type=="Receive" else self.received_amount - total_deductions = sum([flt(d.amount) for d in self.get("deductions")]) - if self.total_allocated_amount < party_amount: - if self.payment_type == "Receive": - self.unallocated_amount = party_amount - (self.total_allocated_amount - total_deductions) - else: - self.unallocated_amount = party_amount - (self.total_allocated_amount + total_deductions) + self.unallocated_amount = party_amount - self.total_allocated_amount def set_difference_amount(self): base_unallocated_amount = flt(self.unallocated_amount) * (flt(self.source_exchange_rate) @@ -302,11 +297,10 @@ class PaymentEntry(AccountsController): else: self.difference_amount = self.base_paid_amount - flt(self.base_received_amount) - for d in self.get("deductions"): - if d.amount: - self.difference_amount -= flt(d.amount) + total_deductions = sum([flt(d.amount) for d in self.get("deductions")]) - self.difference_amount = flt(self.difference_amount, self.precision("difference_amount")) + self.difference_amount = flt(self.difference_amount - total_deductions, + self.precision("difference_amount")) def clear_unallocated_reference_document_rows(self): self.set("references", self.get("references", {"allocated_amount": ["not in", [0, None, ""]]})) diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py index 0316ccafe24..60be20dd89a 100644 --- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py @@ -267,3 +267,65 @@ class TestPaymentEntry(unittest.TestCase): return frappe.db.sql("""select account, debit, credit, against_voucher from `tabGL Entry` where voucher_type='Payment Entry' and voucher_no=%s order by account asc""", voucher_no, as_dict=1) + + def test_payment_entry_write_off_difference(self): + si = create_sales_invoice() + pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Cash - _TC") + pe.reference_no = "1" + pe.reference_date = "2016-01-01" + pe.received_amount = pe.paid_amount = 110 + pe.insert() + + self.assertEqual(pe.unallocated_amount, 10) + + pe.received_amount = pe.paid_amount = 95 + pe.append("deductions", { + "account": "_Test Write Off - _TC", + "cost_center": "_Test Cost Center - _TC", + "amount": 5 + }) + pe.save() + + self.assertEqual(pe.unallocated_amount, 0) + self.assertEqual(pe.difference_amount, 0) + + pe.submit() + + expected_gle = dict((d[0], d) for d in [ + ["Debtors - _TC", 0, 100, si.name], + ["_Test Cash - _TC", 95, 0, None], + ["_Test Write Off - _TC", 5, 0, None] + ]) + + self.validate_gl_entries(pe.name, expected_gle) + + def test_payment_entry_exchange_gain_loss(self): + si = create_sales_invoice(customer="_Test Customer USD", debit_to="_Test Receivable USD - _TC", + currency="USD", conversion_rate=50) + pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank USD - _TC") + pe.reference_no = "1" + pe.reference_date = "2016-01-01" + pe.target_exchange_rate = 55 + + pe.append("deductions", { + "account": "_Test Exchange Gain/Loss - _TC", + "cost_center": "_Test Cost Center - _TC", + "amount": -500 + }) + pe.save() + + self.assertEqual(pe.unallocated_amount, 0) + self.assertEqual(pe.difference_amount, 0) + + pe.submit() + + expected_gle = dict((d[0], d) for d in [ + ["_Test Receivable USD - _TC", 0, 5000, si.name], + ["_Test Bank USD - _TC", 5500, 0, None], + ["_Test Exchange Gain/Loss - _TC", 0, 500, None], + ]) + + self.validate_gl_entries(pe.name, expected_gle) + + outstanding_amount = flt(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount")) + self.assertEqual(outstanding_amount, 0) diff --git a/erpnext/accounts/doctype/payment_entry/tests/test_payment_against_invoice.js b/erpnext/accounts/doctype/payment_entry/tests/test_payment_against_invoice.js new file mode 100644 index 00000000000..7dea76db800 --- /dev/null +++ b/erpnext/accounts/doctype/payment_entry/tests/test_payment_against_invoice.js @@ -0,0 +1,51 @@ +QUnit.module('Payment Entry'); + +QUnit.test("test payment entry", function(assert) { + assert.expect(6); + let done = assert.async(); + frappe.run_serially([ + () => { + return frappe.tests.make('Sales Invoice', [ + {customer: 'Test Customer 1'}, + {items: [ + [ + {'qty': 1}, + {'rate': 101}, + {'item_code': 'Test Product 1'}, + ] + ]} + ]); + }, + () => cur_frm.save(), + () => frappe.tests.click_button('Submit'), + () => frappe.tests.click_button('Yes'), + () => frappe.timeout(0.5), + () => frappe.tests.click_button('Close'), + () => frappe.timeout(0.5), + () => frappe.click_button('Make'), + () => frappe.click_link('Payment', 1), + () => frappe.timeout(2), + () => { + assert.equal(frappe.get_route()[1], 'Payment Entry', + 'made payment entry'); + assert.equal(cur_frm.doc.party, 'Test Customer 1', + 'customer set in payment entry'); + assert.equal(cur_frm.doc.paid_amount, 101, + 'paid amount set in payment entry'); + assert.equal(cur_frm.doc.references[0].allocated_amount, 101, + 'amount allocated against sales invoice'); + }, + () => cur_frm.set_value('paid_amount', 100), + () => { + cur_frm.doc.references[0].allocated_amount = 101; + }, + () => frappe.click_button('Write Off Difference Amount'), + () => { + assert.equal(cur_frm.doc.difference_amount, 0, + 'difference amount is zero'); + assert.equal(cur_frm.doc.deductions[0].amount, 1, + 'Write off amount = 1'); + }, + () => done() + ]); +}); diff --git a/erpnext/accounts/doctype/payment_entry/tests/test_payment_entry.js b/erpnext/accounts/doctype/payment_entry/tests/test_payment_entry.js index a4ef0ca4eb3..0c76343fa90 100644 --- a/erpnext/accounts/doctype/payment_entry/tests/test_payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/tests/test_payment_entry.js @@ -25,5 +25,4 @@ QUnit.test("test payment entry", function(assert) { () => frappe.timeout(0.3), () => done() ]); -}); - +}); \ No newline at end of file diff --git a/erpnext/accounts/doctype/payment_entry/tests/test_payment_entry_write_off.js b/erpnext/accounts/doctype/payment_entry/tests/test_payment_entry_write_off.js new file mode 100644 index 00000000000..133f1362988 --- /dev/null +++ b/erpnext/accounts/doctype/payment_entry/tests/test_payment_entry_write_off.js @@ -0,0 +1,67 @@ +QUnit.module('Payment Entry'); + +QUnit.test("test payment entry", function(assert) { + assert.expect(8); + let done = assert.async(); + frappe.run_serially([ + () => { + return frappe.tests.make('Sales Invoice', [ + {customer: 'Test Customer 1'}, + {company: '_Test Company'}, + {currency: 'INR'}, + {selling_price_list: '_Test Price List'}, + {items: [ + [ + {'qty': 1}, + {'item_code': 'Test Product 1'}, + ] + ]} + ]); + }, + () => frappe.timeout(1), + () => cur_frm.save(), + () => frappe.tests.click_button('Submit'), + () => frappe.tests.click_button('Yes'), + () => frappe.timeout(1.5), + () => frappe.click_button('Close'), + () => frappe.timeout(0.5), + () => frappe.click_button('Make'), + () => frappe.timeout(1), + () => frappe.click_link('Payment'), + () => frappe.timeout(2), + () => cur_frm.set_value("paid_to", "_Test Cash - _TC"), + () => frappe.timeout(0.5), + () => { + assert.equal(frappe.get_route()[1], 'Payment Entry', 'made payment entry'); + assert.equal(cur_frm.doc.party, 'Test Customer 1', 'customer set in payment entry'); + assert.equal(cur_frm.doc.paid_from, 'Debtors - _TC', 'customer account set in payment entry'); + assert.equal(cur_frm.doc.paid_amount, 100, 'paid amount set in payment entry'); + assert.equal(cur_frm.doc.references[0].allocated_amount, 100, + 'amount allocated against sales invoice'); + }, + () => cur_frm.set_value('paid_amount', 95), + () => frappe.timeout(1), + () => { + frappe.model.set_value("Payment Entry Reference", + cur_frm.doc.references[0].name, "allocated_amount", 100); + }, + () => frappe.timeout(.5), + () => { + assert.equal(cur_frm.doc.difference_amount, 5, 'difference amount is 5'); + }, + () => { + frappe.db.set_value("Company", "_Test Company", "write_off_account", "_Test Write Off - _TC"); + frappe.timeout(1); + frappe.db.set_value("Company", "_Test Company", + "exchange_gain_loss_account", "_Test Exchange Gain/Loss - _TC"); + }, + () => frappe.timeout(1), + () => frappe.click_button('Write Off Difference Amount'), + () => frappe.timeout(2), + () => { + assert.equal(cur_frm.doc.difference_amount, 0, 'difference amount is zero'); + assert.equal(cur_frm.doc.deductions[0].amount, 5, 'Write off amount = 5'); + }, + () => done() + ]); +}); \ No newline at end of file diff --git a/erpnext/accounts/doctype/pos_profile/pos_profile.js b/erpnext/accounts/doctype/pos_profile/pos_profile.js index 03d84e9ad6f..97bbc1227f1 100755 --- a/erpnext/accounts/doctype/pos_profile/pos_profile.js +++ b/erpnext/accounts/doctype/pos_profile/pos_profile.js @@ -8,10 +8,6 @@ frappe.ui.form.on("POS Profile", "onload", function(frm) { return { filters: { selling: 1 } }; }); - frm.set_query("print_format", function() { - return { filters: { doc_type: "Sales Invoice", print_format_type: "Js"} }; - }); - erpnext.queries.setup_queries(frm, "Warehouse", function() { return erpnext.queries.warehouse(frm.doc); }); @@ -27,6 +23,27 @@ frappe.ui.form.on("POS Profile", "onload", function(frm) { }); frappe.ui.form.on('POS Profile', { + setup: function(frm) { + frm.set_query("online_print_format", function() { + return { + filters: [ + ['Print Format', 'doc_type', '=', 'Sales Invoice'], + ['Print Format', 'print_format_type', '!=', 'Js'], + ] + }; + }); + + frm.set_query("print_format", function() { + return { filters: { doc_type: "Sales Invoice", print_format_type: "Js"} }; + }); + + frappe.db.get_value('POS Settings', {name: 'POS Settings'}, 'is_online', (r) => { + is_online = r && cint(r.is_online) + frm.toggle_display('offline_pos_section', !is_online); + frm.toggle_display('print_format_for_online', is_online); + }); + }, + refresh: function(frm) { if(frm.doc.company) { frm.trigger("toggle_display_account_head"); diff --git a/erpnext/accounts/doctype/pos_profile/pos_profile.json b/erpnext/accounts/doctype/pos_profile/pos_profile.json index 6991da2888a..187454ef332 100644 --- a/erpnext/accounts/doctype/pos_profile/pos_profile.json +++ b/erpnext/accounts/doctype/pos_profile/pos_profile.json @@ -631,8 +631,7 @@ "bold": 0, "collapsible": 0, "columns": 0, - "default": "Point of Sale", - "fieldname": "print_format", + "fieldname": "print_format_for_online", "fieldtype": "Link", "hidden": 0, "ignore_user_permissions": 0, @@ -641,7 +640,7 @@ "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, - "label": "Print Format", + "label": "Print Format for Online", "length": 0, "no_copy": 0, "options": "Print Format", @@ -822,7 +821,7 @@ "columns": 0, "fieldname": "apply_discount", "fieldtype": "Check", - "hidden": 0, + "hidden": 1, "ignore_user_permissions": 0, "ignore_xss_filter": 0, "in_filter": 0, @@ -836,7 +835,7 @@ "precision": "", "print_hide": 0, "print_hide_if_no_value": 0, - "read_only": 0, + "read_only": 1, "remember_last_selected_value": 0, "report_hide": 0, "reqd": 0, @@ -851,7 +850,7 @@ "collapsible": 0, "columns": 0, "default": "Grand Total", - "depends_on": "apply_discount", + "depends_on": "", "fieldname": "apply_discount_on", "fieldtype": "Select", "hidden": 0, @@ -883,7 +882,7 @@ "bold": 0, "collapsible": 0, "columns": 0, - "fieldname": "customer_details", + "fieldname": "offline_pos_section", "fieldtype": "Section Break", "hidden": 0, "ignore_user_permissions": 0, @@ -892,7 +891,7 @@ "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, - "label": "New Customer Details", + "label": "Offline POS Section", "length": 0, "no_copy": 0, "permlevel": 0, @@ -969,6 +968,38 @@ "set_only_once": 0, "unique": 0 }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "Point of Sale", + "fieldname": "print_format", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Print Format", + "length": 0, + "no_copy": 0, + "options": "Print Format", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, { "allow_bulk_edit": 0, "allow_on_submit": 0, @@ -1291,7 +1322,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2017-07-28 03:40:03.253088", + "modified": "2017-09-01 15:55:14.890452", "modified_by": "Administrator", "module": "Accounts", "name": "POS Profile", diff --git a/erpnext/docs/user/manual/en/setting-up/setup-wizard/__init__.py b/erpnext/accounts/doctype/pos_settings/__init__.py similarity index 100% rename from erpnext/docs/user/manual/en/setting-up/setup-wizard/__init__.py rename to erpnext/accounts/doctype/pos_settings/__init__.py diff --git a/erpnext/accounts/doctype/pos_settings/pos_settings.js b/erpnext/accounts/doctype/pos_settings/pos_settings.js new file mode 100644 index 00000000000..1a146185139 --- /dev/null +++ b/erpnext/accounts/doctype/pos_settings/pos_settings.js @@ -0,0 +1,8 @@ +// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('POS Settings', { + refresh: function() { + + } +}); diff --git a/erpnext/accounts/doctype/pos_settings/pos_settings.json b/erpnext/accounts/doctype/pos_settings/pos_settings.json new file mode 100644 index 00000000000..a04558da26c --- /dev/null +++ b/erpnext/accounts/doctype/pos_settings/pos_settings.json @@ -0,0 +1,94 @@ +{ + "allow_copy": 0, + "allow_guest_to_view": 0, + "allow_import": 0, + "allow_rename": 0, + "beta": 0, + "creation": "2017-08-28 16:46:41.732676", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "", + "editable_grid": 1, + "engine": "InnoDB", + "fields": [ + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "1", + "fieldname": "is_online", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Online", + "length": 0, + "no_copy": 0, + "options": "", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + } + ], + "has_web_view": 0, + "hide_heading": 0, + "hide_toolbar": 0, + "idx": 0, + "image_view": 0, + "in_create": 0, + "is_submittable": 0, + "issingle": 1, + "istable": 0, + "max_attachments": 0, + "modified": "2017-08-30 18:34:58.960276", + "modified_by": "Administrator", + "module": "Accounts", + "name": "POS Settings", + "name_case": "", + "owner": "Administrator", + "permissions": [ + { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "create": 0, + "delete": 0, + "email": 1, + "export": 0, + "if_owner": 0, + "import": 0, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 0, + "role": "System Manager", + "set_user_permissions": 0, + "share": 1, + "submit": 0, + "write": 1 + } + ], + "quick_entry": 1, + "read_only": 0, + "read_only_onload": 0, + "show_name_in_global_search": 0, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1, + "track_seen": 0 +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/pos_settings/pos_settings.py b/erpnext/accounts/doctype/pos_settings/pos_settings.py new file mode 100644 index 00000000000..736d36eea96 --- /dev/null +++ b/erpnext/accounts/doctype/pos_settings/pos_settings.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +from frappe.model.document import Document + +class POSSettings(Document): + pass \ No newline at end of file diff --git a/erpnext/accounts/doctype/pos_settings/test_pos_settings.js b/erpnext/accounts/doctype/pos_settings/test_pos_settings.js new file mode 100644 index 00000000000..639c94ed10d --- /dev/null +++ b/erpnext/accounts/doctype/pos_settings/test_pos_settings.js @@ -0,0 +1,23 @@ +/* eslint-disable */ +// rename this file from _test_[name] to test_[name] to activate +// and remove above this line + +QUnit.test("test: POS Settings", function (assert) { + let done = assert.async(); + + // number of asserts + assert.expect(1); + + frappe.run_serially([ + // insert a new POS Settings + () => frappe.tests.make('POS Settings', [ + // values to be set + {key: 'value'} + ]), + () => { + assert.equal(cur_frm.doc.key, 'value'); + }, + () => done() + ]); + +}); diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.json b/erpnext/accounts/doctype/pricing_rule/pricing_rule.json index 49af59b3880..8860b092e0b 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.json +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.json @@ -1,5 +1,6 @@ { "allow_copy": 0, + "allow_guest_to_view": 0, "allow_import": 1, "allow_rename": 1, "autoname": "field:title", @@ -12,6 +13,7 @@ "editable_grid": 0, "fields": [ { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -40,6 +42,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -69,6 +72,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -99,6 +103,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -129,6 +134,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -159,6 +165,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -189,6 +196,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -217,6 +225,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -247,6 +256,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -275,6 +285,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -303,6 +314,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -331,6 +343,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -359,6 +372,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -387,6 +401,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -417,6 +432,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -447,6 +463,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -477,6 +494,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -507,6 +525,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -537,6 +556,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -567,6 +587,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -597,6 +618,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -627,6 +649,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -655,6 +678,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -683,6 +707,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -711,6 +736,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -739,6 +765,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -767,6 +794,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -796,6 +824,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -824,6 +853,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -851,6 +881,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -880,6 +911,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -910,6 +942,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -941,6 +974,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -969,6 +1003,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -1000,6 +1035,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -1028,6 +1064,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -1058,6 +1095,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -1085,13 +1123,14 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, "columns": 0, "depends_on": "eval:doc.price_or_discount==\"Price\"", "fieldname": "price", - "fieldtype": "Float", + "fieldtype": "Currency", "hidden": 0, "ignore_user_permissions": 0, "ignore_xss_filter": 0, @@ -1114,6 +1153,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -1143,6 +1183,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -1173,6 +1214,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -1202,6 +1244,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -1230,18 +1273,18 @@ "unique": 0 } ], + "has_web_view": 0, "hide_heading": 0, "hide_toolbar": 0, "icon": "fa fa-gift", "idx": 1, "image_view": 0, "in_create": 0, - "in_dialog": 0, "is_submittable": 0, "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2017-02-17 16:21:28.446208", + "modified": "2017-08-31 16:34:41.614743", "modified_by": "Administrator", "module": "Accounts", "name": "Pricing Rule", diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index ac5f5dd3214..b9a7dae55f5 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -46,6 +46,12 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({ cur_frm.add_custom_button(__('Return / Debit Note'), this.make_debit_note, __("Make")); } + + if(!doc.subscription) { + cur_frm.add_custom_button(__('Subscription'), function() { + erpnext.utils.make_subscription(doc.doctype, doc.name) + }, __("Make")) + } } if(doc.docstatus===0) { @@ -343,6 +349,7 @@ frappe.ui.form.on("Purchase Invoice", { 'Payment Entry': 'Payment' } }, + onload: function(frm) { $.each(["warehouse", "rejected_warehouse"], function(i, field) { frm.set_query(field, "items", function() { @@ -370,5 +377,5 @@ frappe.ui.form.on("Purchase Invoice", { erpnext.buying.get_default_bom(frm); } frm.toggle_reqd("supplier_warehouse", frm.doc.is_subcontracted==="Yes"); - } -}) + }, +}) \ No newline at end of file diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index 8ea48f65aa8..748c24ddea9 100755 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -3348,6 +3348,67 @@ "set_only_once": 0, "unique": 0 }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "subscription_section", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Subscription Section", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "subscription", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Subscription", + "length": 0, + "no_copy": 1, + "options": "Subscription", + "permlevel": 0, + "precision": "", + "print_hide": 1, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, { "allow_bulk_edit": 0, "allow_on_submit": 0, @@ -3358,7 +3419,7 @@ "depends_on": "eval:doc.docstatus<2 && !doc.__islocal", "fieldname": "recurring_invoice", "fieldtype": "Section Break", - "hidden": 0, + "hidden": 1, "ignore_user_permissions": 0, "ignore_xss_filter": 0, "in_filter": 0, @@ -3797,7 +3858,7 @@ "istable": 0, "max_attachments": 0, "menu_index": 0, - "modified": "2017-07-19 13:53:48.673757", + "modified": "2017-08-31 11:22:47.074420", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice", diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 7ab790118a0..be388aac850 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -667,7 +667,7 @@ class PurchaseInvoice(BuyingController): if account_type != 'Fixed Asset': frappe.throw(_("Row {0}# Account must be of type 'Fixed Asset'").format(d.idx)) - def on_recurring(self, reference_doc): + def on_recurring(self, reference_doc, subscription_doc): self.due_date = None @frappe.whitelist() diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice_dashboard.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice_dashboard.py index 6141db5d3cd..062a2d2c64f 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice_dashboard.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice_dashboard.py @@ -8,7 +8,8 @@ def get_data(): 'Payment Entry': 'reference_name', 'Payment Request': 'reference_name', 'Landed Cost Voucher': 'receipt_document', - 'Purchase Invoice': 'return_against' + 'Purchase Invoice': 'return_against', + 'Subscription': 'reference_document' }, 'internal_links': { 'Purchase Order': ['items', 'purchase_order'], @@ -27,5 +28,9 @@ def get_data(): 'label': _('Returns'), 'items': ['Purchase Invoice'] }, + { + 'label': _('Subscription'), + 'items': ['Subscription'] + }, ] } \ No newline at end of file diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 3454a2e9b49..639620f910f 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -256,10 +256,6 @@ class TestPurchaseInvoice(unittest.TestCase): self.assertFalse(frappe.db.sql("""select name from `tabJournal Entry Account` where reference_type='Purchase Invoice' and reference_name=%s""", pi.name)) - def test_recurring_invoice(self): - from erpnext.controllers.tests.test_recurring_document import test_recurring_document - test_recurring_document(self, test_records) - def test_total_purchase_cost_for_project(self): existing_purchase_cost = frappe.db.sql("""select sum(base_net_amount) from `tabPurchase Invoice Item` where project = '_Test Project' and docstatus=1""") diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index ef233c67995..11d18253887 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -86,7 +86,11 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte this.make_payment_request, __("Make")); } - + if(!doc.subscription) { + cur_frm.add_custom_button(__('Subscription'), function() { + erpnext.utils.make_subscription(doc.doctype, doc.name) + }, __("Make")) + } } // Show buttons only when pos view is active diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index 260c05e3e42..2bb0044fbdc 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -4175,6 +4175,67 @@ "set_only_once": 0, "unique": 0 }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "subscription_section", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Subscription Section", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 1, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "subscription", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Subscription", + "length": 0, + "no_copy": 1, + "options": "Subscription", + "permlevel": 0, + "precision": "", + "print_hide": 1, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, { "allow_bulk_edit": 0, "allow_on_submit": 0, @@ -4185,7 +4246,7 @@ "depends_on": "eval:doc.docstatus<2 && !doc.__islocal", "fieldname": "recurring_invoice", "fieldtype": "Section Break", - "hidden": 0, + "hidden": 1, "ignore_user_permissions": 0, "ignore_xss_filter": 0, "in_filter": 0, @@ -4688,7 +4749,7 @@ "istable": 0, "max_attachments": 0, "menu_index": 0, - "modified": "2017-07-07 13:05:37.469682", + "modified": "2017-08-31 11:23:08.675028", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice", diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 780edd8bdf0..065fb94ed28 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -107,7 +107,7 @@ class SalesInvoice(SellingController): def on_submit(self): self.validate_pos_paid_amount() - if not self.recurring_id: + if not self.subscription: frappe.get_doc('Authorization Control').validate_approving_authority(self.doctype, self.company, self.base_grand_total, self) @@ -313,7 +313,7 @@ class SalesInvoice(SellingController): for fieldname in ('territory', 'naming_series', 'currency', 'taxes_and_charges', 'letter_head', 'tc_name', 'selling_price_list', 'company', 'select_print_heading', 'cash_bank_account', - 'write_off_account', 'write_off_cost_center'): + 'write_off_account', 'write_off_cost_center', 'apply_discount_on'): if (not for_validate) or (for_validate and not self.get(fieldname)): self.set(fieldname, pos.get(fieldname)) @@ -799,7 +799,7 @@ class SalesInvoice(SellingController): for dn in set(updated_delivery_notes): frappe.get_doc("Delivery Note", dn).update_billing_percentage(update_modified=update_modified) - def on_recurring(self, reference_doc): + def on_recurring(self, reference_doc, subscription_doc): for fieldname in ("c_form_applicable", "c_form_no", "write_off_amount"): self.set(fieldname, reference_doc.get(fieldname)) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py index bc9d76646b9..efd18b52150 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py @@ -8,7 +8,8 @@ def get_data(): 'Journal Entry': 'reference_name', 'Payment Entry': 'reference_name', 'Payment Request': 'reference_name', - 'Sales Invoice': 'return_against' + 'Sales Invoice': 'return_against', + 'Subscription': 'reference_document', }, 'internal_links': { 'Sales Order': ['items', 'sales_order'] @@ -26,5 +27,9 @@ def get_data(): 'label': _('Returns'), 'items': ['Sales Invoice'] }, + { + 'label': _('Subscription'), + 'items': ['Subscription'] + }, ] } \ No newline at end of file diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index e0a453c5c91..db295636319 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -809,10 +809,6 @@ class TestSalesInvoice(unittest.TestCase): self.assertTrue(not frappe.db.sql("""select name from `tabJournal Entry Account` where reference_name=%s""", si.name)) - def test_recurring_invoice(self): - from erpnext.controllers.tests.test_recurring_document import test_recurring_document - test_recurring_document(self, test_records) - def test_serialized(self): from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos @@ -1167,8 +1163,15 @@ class TestSalesInvoice(unittest.TestCase): self.assertEqual(flt(si.outstanding_amount), flt(si.grand_total + si.total_advance, si.precision("outstanding_amount"))) def test_multiple_uom_in_selling(self): - si = frappe.copy_doc(test_records[1]) + frappe.db.sql("""delete from `tabItem Price` + where price_list='_Test Price List' and item_code='_Test Item'""") + item_price = frappe.new_doc("Item Price") + item_price.price_list = "_Test Price List" + item_price.item_code = "_Test Item" + item_price.price_list_rate = 100 + item_price.insert() + si = frappe.copy_doc(test_records[1]) si.items[0].uom = "_Test UOM 1" si.items[0].conversion_factor = None si.items[0].price_list_rate = None diff --git a/erpnext/accounts/page/pos/pos.js b/erpnext/accounts/page/pos/pos.js index 33b41e9ee44..2f425248a16 100644 --- a/erpnext/accounts/page/pos/pos.js +++ b/erpnext/accounts/page/pos/pos.js @@ -8,7 +8,16 @@ frappe.pages['pos'].on_page_load = function (wrapper) { single_column: true }); - wrapper.pos = new erpnext.pos.PointOfSale(wrapper) + frappe.db.get_value('POS Settings', {name: 'POS Settings'}, 'is_online', (r) => { + if (r && r.is_online && !cint(r.is_online)) { + // offline + wrapper.pos = new erpnext.pos.PointOfSale(wrapper); + cur_pos = wrapper.pos; + } else { + // online + frappe.set_route('point-of-sale'); + } + }); } frappe.pages['pos'].refresh = function (wrapper) { diff --git a/erpnext/accounts/page/pos/test_pos.js b/erpnext/accounts/page/pos/test_pos.js index bc5edc9f2a4..8913a9e1cc0 100644 --- a/erpnext/accounts/page/pos/test_pos.js +++ b/erpnext/accounts/page/pos/test_pos.js @@ -1,16 +1,15 @@ -QUnit.test("test:POS Profile", function(assert) { - assert.expect(1); +QUnit.test("test:Sales Invoice", function(assert) { + assert.expect(3); let done = assert.async(); frappe.run_serially([ () => { return frappe.tests.make("POS Profile", [ {naming_series: "SINV"}, - {company: "Test Company"}, {country: "India"}, {currency: "INR"}, - {write_off_account: "Write Off - TC"}, - {write_off_cost_center: "Main - TC"}, + {write_off_account: "Write Off - FT"}, + {write_off_cost_center: "Main - FT"}, {payments: [ [ {"default": 1}, @@ -24,19 +23,10 @@ QUnit.test("test:POS Profile", function(assert) { () => { assert.equal(cur_frm.doc.payments[0].default, 1, "Default mode of payment tested"); }, - () => done() - ]); -}); - -QUnit.test("test:Sales Invoice", function(assert) { - assert.expect(2); - let done = assert.async(); - - frappe.run_serially([ + () => frappe.timeout(1), () => { return frappe.tests.make("Sales Invoice", [ {customer: "Test Customer 2"}, - {company: "Test Company"}, {is_pos: 1}, {posting_date: frappe.datetime.get_today()}, {due_date: frappe.datetime.get_today()}, @@ -44,7 +34,7 @@ QUnit.test("test:Sales Invoice", function(assert) { [ {"item_code": "Test Product 1"}, {"qty": 5}, - {"warehouse":'Stores - TC'} + {"warehouse":'Stores - FT'} ]] } ]); diff --git a/erpnext/accounts/print_format/point_of_sale/point_of_sale.json b/erpnext/accounts/print_format/point_of_sale/point_of_sale.json index 28c853cc48d..4e69cad06bc 100644 --- a/erpnext/accounts/print_format/point_of_sale/point_of_sale.json +++ b/erpnext/accounts/print_format/point_of_sale/point_of_sale.json @@ -10,7 +10,7 @@ "html": "\n\n

\n\t{{ company }}
\n\t{{ __(\"POS No : \") }} {{ offline_pos_name }}
\n

\n

\n\t{{ __(\"Customer\") }}: {{ customer }}
\n

\n\n

\n\t{{ __(\"Date\") }}: {{ dateutil.global_date_format(posting_date) }}
\n

\n\n
\n\n\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\n\t\n\t\t{% for item in items %}\n\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t{% endfor %}\n\t\n
{{ __(\"Item\") }}{{ __(\"Qty\") }}{{ __(\"Amount\") }}
\n\t\t\t\t{{ item.item_name }}\n\t\t\t{{ format_number(item.qty, null,precision(\"difference\")) }}
@ {{ format_currency(item.rate, currency) }}
{{ format_currency(item.amount, currency) }}
\n\n\n\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t{% for row in taxes %}\n\t\t{% if not row.included_in_print_rate %}\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t{% endif %}\n\t\t{% endfor %}\n\t\t{% if discount_amount %}\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t{% endif %}\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\n
\n\t\t\t\t{{ __(\"Net Total\") }}\n\t\t\t\n\t\t\t\t{{ format_currency(total, currency) }}\n\t\t\t
\n\t\t\t\t{{ row.description }}\n\t\t\t\n\t\t\t\t{{ format_currency(row.tax_amount, currency) }}\n\t\t\t
\n\t\t\t\t{{ __(\"Discount\") }}\n\t\t\t\n\t\t\t\t{{ format_currency(discount_amount, currency) }}\n\t\t\t
\n\t\t\t\t{{ __(\"Grand Total\") }}\n\t\t\t\n\t\t\t\t{{ format_currency(grand_total, currency) }}\n\t\t\t
\n\t\t\t\t{{ __(\"Paid Amount\") }}\n\t\t\t\n\t\t\t\t{{ format_currency(paid_amount, currency) }}\n\t\t\t
\n\n\n
\n

{{ terms }}

\n

{{ __(\"Thank you, please visit again.\") }}

", "idx": 0, "line_breaks": 0, - "modified": "2017-05-19 14:36:04.740728", + "modified": "2017-09-01 14:27:04.871233", "modified_by": "Administrator", "module": "Accounts", "name": "Point of Sale", diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index c533e6bbcdd..a51246bcb86 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -13,6 +13,7 @@ frappe.ui.form.on("Purchase Order", { 'Stock Entry': 'Material to Supplier' } }, + onload: function(frm) { erpnext.queries.setup_queries(frm, "Warehouse", function() { return erpnext.queries.warehouse(frm.doc); @@ -20,8 +21,7 @@ frappe.ui.form.on("Purchase Order", { frm.set_indicator_formatter('item_code', function(doc) { return (doc.qty<=doc.received_qty) ? "green" : "orange" }) - - } + }, }); erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend({ @@ -86,8 +86,13 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend( if(flt(doc.per_billed)==0 && doc.status != "Delivered") { cur_frm.add_custom_button(__('Payment'), cur_frm.cscript.make_payment_entry, __("Make")); } - cur_frm.page.set_inner_btn_group_as_primary(__("Make")); + if(!doc.subscription) { + cur_frm.add_custom_button(__('Subscription'), function() { + erpnext.utils.make_subscription(doc.doctype, doc.name) + }, __("Make")) + } + cur_frm.page.set_inner_btn_group_as_primary(__("Make")); } }, diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json index 09c987f0fd8..c4096cc7724 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.json +++ b/erpnext/buying/doctype/purchase_order/purchase_order.json @@ -2856,6 +2856,67 @@ "set_only_once": 0, "unique": 0 }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "subscription_section", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Subscription Section", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "subscription", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Subscription", + "length": 0, + "no_copy": 1, + "options": "Subscription", + "permlevel": 0, + "precision": "", + "print_hide": 1, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, { "allow_bulk_edit": 0, "allow_on_submit": 0, @@ -2866,7 +2927,7 @@ "depends_on": "eval:doc.docstatus<2 && !doc.__islocal", "fieldname": "recurring_order", "fieldtype": "Section Break", - "hidden": 0, + "hidden": 1, "ignore_user_permissions": 0, "ignore_xss_filter": 0, "in_filter": 0, @@ -3335,7 +3396,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2017-07-19 14:03:51.838328", + "modified": "2017-08-31 11:22:30.190589", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order", diff --git a/erpnext/buying/doctype/purchase_order/purchase_order_dashboard.py b/erpnext/buying/doctype/purchase_order/purchase_order_dashboard.py index df10a541df2..d57b0e2568f 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order_dashboard.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order_dashboard.py @@ -5,7 +5,8 @@ def get_data(): 'fieldname': 'purchase_order', 'non_standard_fieldnames': { 'Journal Entry': 'reference_name', - 'Payment Entry': 'reference_name' + 'Payment Entry': 'reference_name', + 'Subscription': 'reference_document' }, 'internal_links': { 'Material Request': ['items', 'material_request'], @@ -23,11 +24,11 @@ def get_data(): }, { 'label': _('Reference'), - 'items': ['Material Request', 'Supplier Quotation', 'Project'] + 'items': ['Material Request', 'Supplier Quotation', 'Project', 'Subscription'] }, { 'label': _('Sub-contracting'), 'items': ['Stock Entry'] - } + }, ] } diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.js b/erpnext/buying/doctype/purchase_order/test_purchase_order.js new file mode 100644 index 00000000000..e9db270b4fd --- /dev/null +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.js @@ -0,0 +1,23 @@ +/* eslint-disable */ +// rename this file from _test_[name] to test_[name] to activate +// and remove above this line + +QUnit.test("test: Purchase Order", function (assert) { + let done = assert.async(); + + // number of asserts + assert.expect(1); + + frappe.run_serially('Purchase Order', [ + // insert a new Purchase Order + () => frappe.tests.make([ + // values to be set + {key: 'value'} + ]), + () => { + assert.equal(cur_frm.doc.key, 'value'); + }, + () => done() + ]); + +}); diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.js b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.js index 3767248e369..3899bbab114 100644 --- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.js +++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.js @@ -22,7 +22,9 @@ erpnext.buying.SupplierQuotationController = erpnext.buying.BuyingController.ext cur_frm.page.set_inner_btn_group_as_primary(__("Make")); cur_frm.add_custom_button(__("Quotation"), this.make_quotation, __("Make")); - + cur_frm.add_custom_button(__('Subscription'), function() { + erpnext.utils.make_subscription(me.frm.doc.doctype, me.frm.doc.name) + }, __("Make")) } else if (this.frm.doc.docstatus===0) { diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json index ea3ae74dfe7..eed0c15cadf 100644 --- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json +++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json @@ -2051,6 +2051,67 @@ "set_only_once": 0, "unique": 0 }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "subscription_section", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Subscription Section", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "subscription", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Subscription", + "length": 0, + "no_copy": 1, + "options": "Subscription", + "permlevel": 0, + "precision": "", + "print_hide": 1, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, { "allow_bulk_edit": 0, "allow_on_submit": 0, @@ -2247,7 +2308,7 @@ "istable": 0, "max_attachments": 0, "menu_index": 0, - "modified": "2017-07-19 13:51:18.929697", + "modified": "2017-08-31 11:23:25.268924", "modified_by": "Administrator", "module": "Buying", "name": "Supplier Quotation", diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation_dashboard.py b/erpnext/buying/doctype/supplier_quotation/supplier_quotation_dashboard.py index df69063aae6..4321f27f2af 100644 --- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation_dashboard.py +++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation_dashboard.py @@ -3,6 +3,9 @@ from frappe import _ def get_data(): return { 'fieldname': 'supplier_quotation', + 'non_standard_fieldnames': { + 'Subscription': 'reference_document' + }, 'internal_links': { 'Material Request': ['items', 'material_request'], 'Request for Quotation': ['items', 'request_for_quotation'], @@ -17,6 +20,10 @@ def get_data(): 'label': _('Reference'), 'items': ['Material Request', 'Request for Quotation', 'Project'] }, + { + 'label': _('Subscription'), + 'items': ['Subscription'] + }, ] } diff --git a/erpnext/buying/doctype/supplier_quotation/test_supplier_quotation.js b/erpnext/buying/doctype/supplier_quotation/test_supplier_quotation.js new file mode 100644 index 00000000000..7097a6dcb2b --- /dev/null +++ b/erpnext/buying/doctype/supplier_quotation/test_supplier_quotation.js @@ -0,0 +1,23 @@ +/* eslint-disable */ +// rename this file from _test_[name] to test_[name] to activate +// and remove above this line + +QUnit.test("test: Supplier Quotation", function (assert) { + let done = assert.async(); + + // number of asserts + assert.expect(1); + + frappe.run_serially('Supplier Quotation', [ + // insert a new Supplier Quotation + () => frappe.tests.make([ + // values to be set + {key: 'value'} + ]), + () => { + assert.equal(cur_frm.doc.key, 'value'); + }, + () => done() + ]); + +}); diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index b24e047bcc0..d04143d77d6 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -8,7 +8,6 @@ from frappe.utils import today, flt, cint, fmt_money, formatdate, getdate from erpnext.setup.utils import get_exchange_rate from erpnext.accounts.utils import get_fiscal_years, validate_fiscal_year, get_account_currency from erpnext.utilities.transaction_base import TransactionBase -from erpnext.controllers.recurring_document import convert_to_recurring, validate_recurring_document from erpnext.controllers.sales_and_purchase_return import validate_return from erpnext.accounts.party import get_party_account_currency, validate_party_frozen_disabled from erpnext.exceptions import InvalidCurrency @@ -53,13 +52,6 @@ class AccountsController(TransactionBase): self.validate_party() self.validate_currency() - if self.meta.get_field("is_recurring"): - if self.amended_from and self.recurring_id == self.amended_from: - self.recurring_id = None - if not self.get("__islocal"): - validate_recurring_document(self) - convert_to_recurring(self, self.get("posting_date") or self.get("transaction_date")) - if self.doctype == 'Purchase Invoice': self.validate_paid_amount() @@ -84,11 +76,6 @@ class AccountsController(TransactionBase): else: frappe.db.set(self,'paid_amount',0) - def on_update_after_submit(self): - if self.meta.get_field("is_recurring"): - validate_recurring_document(self) - convert_to_recurring(self, self.get("posting_date") or self.get("transaction_date")) - def set_missing_values(self, for_validate=False): if frappe.flags.in_test: for fieldname in ["posting_date","transaction_date"]: diff --git a/erpnext/controllers/recurring_document.py b/erpnext/controllers/recurring_document.py deleted file mode 100644 index 713e9bac2f1..00000000000 --- a/erpnext/controllers/recurring_document.py +++ /dev/null @@ -1,230 +0,0 @@ -from __future__ import unicode_literals -import frappe -import calendar -import frappe.utils -import frappe.defaults - -from frappe.utils import cint, cstr, getdate, nowdate, \ - get_first_day, get_last_day, split_emails - -from frappe import _, msgprint, throw - -month_map = {'Monthly': 1, 'Quarterly': 3, 'Half-yearly': 6, 'Yearly': 12} -date_field_map = { - "Sales Order": "transaction_date", - "Sales Invoice": "posting_date", - "Purchase Order": "transaction_date", - "Purchase Invoice": "posting_date" -} - -def create_recurring_documents(): - manage_recurring_documents("Sales Order") - manage_recurring_documents("Sales Invoice") - manage_recurring_documents("Purchase Order") - manage_recurring_documents("Purchase Invoice") - -def manage_recurring_documents(doctype, next_date=None, commit=True): - """ - Create recurring documents on specific date by copying the original one - and notify the concerned people - """ - next_date = next_date or nowdate() - - date_field = date_field_map[doctype] - - condition = " and ifnull(status, '') != 'Closed'" if doctype in ("Sales Order", "Purchase Order") else "" - - recurring_documents = frappe.db.sql("""select name, recurring_id - from `tab{0}` where is_recurring=1 - and (docstatus=1 or docstatus=0) and next_date=%s - and next_date <= ifnull(end_date, '2199-12-31') {1}""".format(doctype, condition), next_date) - - exception_list = [] - for ref_document, recurring_id in recurring_documents: - if not frappe.db.sql("""select name from `tab%s` - where %s=%s and recurring_id=%s and (docstatus=1 or docstatus=0)""" - % (doctype, date_field, '%s', '%s'), (next_date, recurring_id)): - try: - reference_doc = frappe.get_doc(doctype, ref_document) - new_doc = make_new_document(reference_doc, date_field, next_date) - if reference_doc.notify_by_email: - send_notification(new_doc) - if commit: - frappe.db.commit() - except: - if commit: - frappe.db.rollback() - - frappe.db.begin() - frappe.db.sql("update `tab%s` \ - set is_recurring = 0 where name = %s" % (doctype, '%s'), - (ref_document)) - notify_errors(ref_document, doctype, reference_doc.get("customer") or reference_doc.get("supplier"), - reference_doc.owner) - frappe.db.commit() - - exception_list.append(frappe.get_traceback()) - finally: - if commit: - frappe.db.begin() - - if exception_list: - exception_message = "\n\n".join([cstr(d) for d in exception_list]) - frappe.throw(exception_message) - -def make_new_document(reference_doc, date_field, posting_date): - new_document = frappe.copy_doc(reference_doc, ignore_no_copy=False) - mcount = month_map[reference_doc.recurring_type] - - from_date = get_next_date(reference_doc.from_date, mcount) - - # get last day of the month to maintain period if the from date is first day of its own month - # and to date is the last day of its own month - if (cstr(get_first_day(reference_doc.from_date)) == cstr(reference_doc.from_date)) and \ - (cstr(get_last_day(reference_doc.to_date)) == cstr(reference_doc.to_date)): - to_date = get_last_day(get_next_date(reference_doc.to_date, mcount)) - else: - to_date = get_next_date(reference_doc.to_date, mcount) - - new_document.update({ - date_field: posting_date, - "from_date": from_date, - "to_date": to_date, - "next_date": get_next_date(reference_doc.next_date, mcount,cint(reference_doc.repeat_on_day_of_month)) - }) - - if new_document.meta.get_field('set_posting_time'): - new_document.set('set_posting_time', 1) - - # copy document fields - for fieldname in ("owner", "recurring_type", "repeat_on_day_of_month", - "recurring_id", "notification_email_address", "is_recurring", "end_date", - "title", "naming_series", "select_print_heading", "ignore_pricing_rule", - "posting_time", "remarks", 'submit_on_creation'): - if new_document.meta.get_field(fieldname): - new_document.set(fieldname, reference_doc.get(fieldname)) - - # copy item fields - for i, item in enumerate(new_document.items): - for fieldname in ("page_break",): - item.set(fieldname, reference_doc.items[i].get(fieldname)) - - new_document.run_method("on_recurring", reference_doc=reference_doc) - - if reference_doc.submit_on_creation: - new_document.insert() - new_document.submit() - else: - new_document.docstatus=0 - new_document.insert() - - return new_document - -def get_next_date(dt, mcount, day=None): - dt = getdate(dt) - - from dateutil.relativedelta import relativedelta - dt += relativedelta(months=mcount, day=day) - - return dt - -def send_notification(new_rv): - """Notify concerned persons about recurring document generation""" - - frappe.sendmail(new_rv.notification_email_address, - subject= _("New {0}: #{1}").format(new_rv.doctype, new_rv.name), - message = _("Please find attached {0} #{1}").format(new_rv.doctype, new_rv.name), - attachments = [frappe.attach_print(new_rv.doctype, new_rv.name, file_name=new_rv.name, print_format=new_rv.recurring_print_format)]) - -def notify_errors(doc, doctype, party, owner): - from frappe.utils.user import get_system_managers - recipients = get_system_managers(only_name=True) - - frappe.sendmail(recipients + [frappe.db.get_value("User", owner, "email")], - subject="[Urgent] Error while creating recurring %s for %s" % (doctype, doc), - message = frappe.get_template("templates/emails/recurring_document_failed.html").render({ - "type": doctype, - "name": doc, - "party": party - })) - - assign_task_to_owner(doc, doctype, "Recurring Invoice Failed", recipients) - -def assign_task_to_owner(doc, doctype, msg, users): - for d in users: - from frappe.desk.form import assign_to - args = { - 'assign_to' : d, - 'doctype' : doctype, - 'name' : doc, - 'description' : msg, - 'priority' : 'High' - } - assign_to.add(args) - -def validate_recurring_document(doc): - if doc.is_recurring: - validate_notification_email_id(doc) - if not doc.recurring_type: - frappe.throw(_("Please select {0}").format(doc.meta.get_label("recurring_type"))) - - elif not (doc.from_date and doc.to_date): - frappe.throw(_("Period From and Period To dates mandatory for recurring {0}").format(doc.doctype)) - -def validate_recurring_next_date(doc): - posting_date = doc.get("posting_date") or doc.get("transaction_date") - if getdate(posting_date) > getdate(doc.next_date): - frappe.throw(_("Next Date must be greater than Posting Date")) - - next_date = getdate(doc.next_date) - if next_date.day != doc.repeat_on_day_of_month: - - # if the repeat day is the last day of the month (31) - # and the current month does not have as many days, - # then the last day of the current month is a valid date - lastday = calendar.monthrange(next_date.year, next_date.month)[1] - if doc.repeat_on_day_of_month < lastday: - - # the specified day of the month is not same as the day specified - # or the last day of the month - frappe.throw(_("Next Date's day and Repeat on Day of Month must be equal")) - -def convert_to_recurring(doc, posting_date): - if doc.is_recurring: - if not doc.recurring_id: - doc.db_set("recurring_id", doc.name) - - set_next_date(doc, posting_date) - - if doc.next_date: - validate_recurring_next_date(doc) - - elif doc.recurring_id: - doc.db_set("recurring_id", None) - -def validate_notification_email_id(doc): - if doc.notify_by_email: - if doc.notification_email_address: - email_list = split_emails(doc.notification_email_address.replace("\n", "")) - - from frappe.utils import validate_email_add - for email in email_list: - if not validate_email_add(email): - throw(_("{0} is an invalid email address in 'Notification \ - Email Address'").format(email)) - - else: - frappe.throw(_("'Notification Email Addresses' not specified for recurring %s") \ - % doc.doctype) - -def set_next_date(doc, posting_date): - """ Set next date on which recurring document will be created""" - if not doc.repeat_on_day_of_month: - msgprint(_("Please enter 'Repeat on Day of Month' field value"), raise_exception=1) - - next_date = get_next_date(posting_date, month_map[doc.recurring_type], - cint(doc.repeat_on_day_of_month)) - - doc.db_set('next_date', next_date) - - msgprint(_("Next Recurring {0} will be created on {1}").format(doc.doctype, next_date)) diff --git a/erpnext/controllers/tests/test_recurring_document.py b/erpnext/controllers/tests/test_recurring_document.py deleted file mode 100644 index d47c5c77013..00000000000 --- a/erpnext/controllers/tests/test_recurring_document.py +++ /dev/null @@ -1,149 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# License: GNU General Public License v3. See license.txt -from __future__ import unicode_literals - -import frappe -import frappe.permissions -from erpnext.controllers.recurring_document import date_field_map -from frappe.utils import get_first_day, get_last_day, add_to_date, nowdate, getdate, add_days - -def test_recurring_document(obj, test_records): - frappe.db.set_value("Print Settings", "Print Settings", "send_print_as_pdf", 1) - today = nowdate() - base_doc = frappe.copy_doc(test_records[0]) - - base_doc.update({ - "is_recurring": 1, - "submit_on_create": 1, - "recurring_type": "Monthly", - "notification_email_address": "test@example.com, test1@example.com, test2@example.com", - "repeat_on_day_of_month": getdate(today).day, - "due_date": None, - "from_date": get_first_day(today), - "to_date": get_last_day(today) - }) - - date_field = date_field_map[base_doc.doctype] - base_doc.set(date_field, today) - - if base_doc.doctype == "Sales Order": - base_doc.set("delivery_date", add_days(today, 15)) - - # monthly - doc1 = frappe.copy_doc(base_doc) - doc1.insert() - doc1.submit() - _test_recurring_document(obj, doc1, date_field, True) - - # monthly without a first and last day period - if getdate(today).day != 1: - doc2 = frappe.copy_doc(base_doc) - doc2.update({ - "from_date": today, - "to_date": add_to_date(today, days=30) - }) - doc2.insert() - doc2.submit() - _test_recurring_document(obj, doc2, date_field, False) - - # quarterly - doc3 = frappe.copy_doc(base_doc) - doc3.update({ - "recurring_type": "Quarterly", - "from_date": get_first_day(today), - "to_date": get_last_day(add_to_date(today, months=3)) - }) - doc3.insert() - doc3.submit() - _test_recurring_document(obj, doc3, date_field, True) - - # quarterly without a first and last day period - doc4 = frappe.copy_doc(base_doc) - doc4.update({ - "recurring_type": "Quarterly", - "from_date": today, - "to_date": add_to_date(today, months=3) - }) - doc4.insert() - doc4.submit() - _test_recurring_document(obj, doc4, date_field, False) - - # yearly - doc5 = frappe.copy_doc(base_doc) - doc5.update({ - "recurring_type": "Yearly", - "from_date": get_first_day(today), - "to_date": get_last_day(add_to_date(today, years=1)) - }) - doc5.insert() - doc5.submit() - _test_recurring_document(obj, doc5, date_field, True) - - # yearly without a first and last day period - doc6 = frappe.copy_doc(base_doc) - doc6.update({ - "recurring_type": "Yearly", - "from_date": today, - "to_date": add_to_date(today, years=1) - }) - doc6.insert() - doc6.submit() - _test_recurring_document(obj, doc6, date_field, False) - - # change date field but keep recurring day to be today - doc7 = frappe.copy_doc(base_doc) - doc7.update({ - date_field: today, - }) - doc7.insert() - doc7.submit() - - # setting so that _test function works - # doc7.set(date_field, today) - _test_recurring_document(obj, doc7, date_field, True) - -def _test_recurring_document(obj, base_doc, date_field, first_and_last_day): - from frappe.utils import add_months, get_last_day - from erpnext.controllers.recurring_document import manage_recurring_documents, \ - get_next_date - - no_of_months = ({"Monthly": 1, "Quarterly": 3, "Yearly": 12})[base_doc.recurring_type] - - def _test(i): - obj.assertEquals(i+1, frappe.db.sql("""select count(*) from `tab%s` - where recurring_id=%s and (docstatus=1 or docstatus=0)""" % (base_doc.doctype, '%s'), - (base_doc.recurring_id))[0][0]) - - next_date = get_next_date(base_doc.get(date_field), no_of_months, - base_doc.repeat_on_day_of_month) - - manage_recurring_documents(base_doc.doctype, next_date=next_date, commit=False) - - recurred_documents = frappe.db.sql("""select name from `tab%s` - where recurring_id=%s and (docstatus=1 or docstatus=0) order by name desc""" - % (base_doc.doctype, '%s'), (base_doc.recurring_id)) - - obj.assertEquals(i+2, len(recurred_documents)) - - new_doc = frappe.get_doc(base_doc.doctype, recurred_documents[0][0]) - - for fieldname in ["is_recurring", "recurring_type", - "repeat_on_day_of_month", "notification_email_address"]: - obj.assertEquals(base_doc.get(fieldname), - new_doc.get(fieldname)) - - obj.assertEquals(new_doc.get(date_field), getdate(next_date)) - - obj.assertEquals(new_doc.from_date, getdate(add_months(base_doc.from_date, no_of_months))) - - if first_and_last_day: - obj.assertEquals(new_doc.to_date, getdate(get_last_day(add_months(base_doc.to_date, no_of_months)))) - else: - obj.assertEquals(new_doc.to_date, getdate(add_months(base_doc.to_date, no_of_months))) - - return new_doc - - # if yearly, test 1 repetition, else test 5 repetitions - count = 1 if (no_of_months == 12) else 5 - for i in xrange(count): - base_doc = _test(i) diff --git a/erpnext/docs/assets/img/articles/brand-logo.gif b/erpnext/docs/assets/img/articles/brand-logo.gif new file mode 100644 index 00000000000..99a10d3b34b Binary files /dev/null and b/erpnext/docs/assets/img/articles/brand-logo.gif differ diff --git a/erpnext/docs/assets/img/articles/download-backup-1.png b/erpnext/docs/assets/img/articles/download-backup-1.png new file mode 100644 index 00000000000..93597f453f9 Binary files /dev/null and b/erpnext/docs/assets/img/articles/download-backup-1.png differ diff --git a/erpnext/docs/assets/img/articles/download-backup-2.gif b/erpnext/docs/assets/img/articles/download-backup-2.gif new file mode 100644 index 00000000000..4109dff0187 Binary files /dev/null and b/erpnext/docs/assets/img/articles/download-backup-2.gif differ diff --git a/erpnext/docs/assets/img/articles/email-error.png b/erpnext/docs/assets/img/articles/email-error.png new file mode 100644 index 00000000000..e8c5d73dc35 Binary files /dev/null and b/erpnext/docs/assets/img/articles/email-error.png differ diff --git a/erpnext/docs/assets/img/articles/fetching-1.png b/erpnext/docs/assets/img/articles/fetching-1.png new file mode 100644 index 00000000000..fcf3fcbac24 Binary files /dev/null and b/erpnext/docs/assets/img/articles/fetching-1.png differ diff --git a/erpnext/docs/assets/img/articles/fetching-2.png b/erpnext/docs/assets/img/articles/fetching-2.png new file mode 100644 index 00000000000..c5594d56f4f Binary files /dev/null and b/erpnext/docs/assets/img/articles/fetching-2.png differ diff --git a/erpnext/docs/assets/img/articles/fetching-3.gif b/erpnext/docs/assets/img/articles/fetching-3.gif new file mode 100644 index 00000000000..5deea3e5f8c Binary files /dev/null and b/erpnext/docs/assets/img/articles/fetching-3.gif differ diff --git a/erpnext/docs/assets/img/articles/item-valuation-1.png b/erpnext/docs/assets/img/articles/item-valuation-1.png new file mode 100644 index 00000000000..16bde1d1202 Binary files /dev/null and b/erpnext/docs/assets/img/articles/item-valuation-1.png differ diff --git a/erpnext/docs/assets/img/articles/item-valuation-2.png b/erpnext/docs/assets/img/articles/item-valuation-2.png new file mode 100644 index 00000000000..cc8b8442f3f Binary files /dev/null and b/erpnext/docs/assets/img/articles/item-valuation-2.png differ diff --git a/erpnext/docs/assets/img/articles/purchase-return.gif b/erpnext/docs/assets/img/articles/purchase-return.gif new file mode 100644 index 00000000000..de5f18cf337 Binary files /dev/null and b/erpnext/docs/assets/img/articles/purchase-return.gif differ diff --git a/erpnext/docs/assets/img/articles/report-header-1.png b/erpnext/docs/assets/img/articles/report-header-1.png new file mode 100644 index 00000000000..848c3018bbf Binary files /dev/null and b/erpnext/docs/assets/img/articles/report-header-1.png differ diff --git a/erpnext/docs/assets/img/articles/report-header-2.png b/erpnext/docs/assets/img/articles/report-header-2.png new file mode 100644 index 00000000000..47d86958be6 Binary files /dev/null and b/erpnext/docs/assets/img/articles/report-header-2.png differ diff --git a/erpnext/docs/assets/img/project/timesheet/make_invoice_from_timesheet.gif b/erpnext/docs/assets/img/project/timesheet/make_invoice_from_timesheet.gif new file mode 100644 index 00000000000..90cc6d6e869 Binary files /dev/null and b/erpnext/docs/assets/img/project/timesheet/make_invoice_from_timesheet.gif differ diff --git a/erpnext/docs/assets/img/schools/admission/program-enrollment-tool.gif b/erpnext/docs/assets/img/schools/admission/program-enrollment-tool.gif new file mode 100644 index 00000000000..c25b1799674 Binary files /dev/null and b/erpnext/docs/assets/img/schools/admission/program-enrollment-tool.gif differ diff --git a/erpnext/docs/assets/img/schools/admission/program-enrollment-tool01.gif b/erpnext/docs/assets/img/schools/admission/program-enrollment-tool01.gif new file mode 100644 index 00000000000..8b1f6314be0 Binary files /dev/null and b/erpnext/docs/assets/img/schools/admission/program-enrollment-tool01.gif differ diff --git a/erpnext/docs/assets/img/schools/admission/program-enrollment.gif b/erpnext/docs/assets/img/schools/admission/program-enrollment.gif new file mode 100644 index 00000000000..616ab17df90 Binary files /dev/null and b/erpnext/docs/assets/img/schools/admission/program-enrollment.gif differ diff --git a/erpnext/docs/assets/img/schools/admission/program-enrollment.png b/erpnext/docs/assets/img/schools/admission/program-enrollment.png deleted file mode 100644 index df96f3c320d..00000000000 Binary files a/erpnext/docs/assets/img/schools/admission/program-enrollment.png and /dev/null differ diff --git a/erpnext/docs/assets/img/schools/admission/student-applicant-enroll.png b/erpnext/docs/assets/img/schools/admission/student-applicant-enroll.png index fb92a679dff..7cf5e75be4d 100644 Binary files a/erpnext/docs/assets/img/schools/admission/student-applicant-enroll.png and b/erpnext/docs/assets/img/schools/admission/student-applicant-enroll.png differ diff --git a/erpnext/docs/assets/img/schools/admission/student-application-actions.png b/erpnext/docs/assets/img/schools/admission/student-application-actions.png new file mode 100644 index 00000000000..e3a4c15ec19 Binary files /dev/null and b/erpnext/docs/assets/img/schools/admission/student-application-actions.png differ diff --git a/erpnext/docs/assets/img/schools/schedule/student-attendance.gif b/erpnext/docs/assets/img/schools/schedule/student-attendance.gif new file mode 100644 index 00000000000..82549d46922 Binary files /dev/null and b/erpnext/docs/assets/img/schools/schedule/student-attendance.gif differ diff --git a/erpnext/docs/assets/img/schools/schedule/student-leave-application.gif b/erpnext/docs/assets/img/schools/schedule/student-leave-application.gif new file mode 100644 index 00000000000..3b09fac27b2 Binary files /dev/null and b/erpnext/docs/assets/img/schools/schedule/student-leave-application.gif differ diff --git a/erpnext/docs/assets/img/schools/setup/Course-schedule-error.png b/erpnext/docs/assets/img/schools/setup/Course-schedule-error.png new file mode 100644 index 00000000000..d9913d857a4 Binary files /dev/null and b/erpnext/docs/assets/img/schools/setup/Course-schedule-error.png differ diff --git a/erpnext/docs/assets/img/schools/setup/Room-Assesment-plan.png b/erpnext/docs/assets/img/schools/setup/Room-Assesment-plan.png new file mode 100644 index 00000000000..4b23cd7ea38 Binary files /dev/null and b/erpnext/docs/assets/img/schools/setup/Room-Assesment-plan.png differ diff --git a/erpnext/docs/assets/img/schools/setup/course-fee-program.png b/erpnext/docs/assets/img/schools/setup/course-fee-program.png new file mode 100644 index 00000000000..c8527d04e0d Binary files /dev/null and b/erpnext/docs/assets/img/schools/setup/course-fee-program.png differ diff --git a/erpnext/docs/assets/img/schools/setup/setup-section.png b/erpnext/docs/assets/img/schools/setup/setup-section.png deleted file mode 100644 index c02bbc4733d..00000000000 Binary files a/erpnext/docs/assets/img/schools/setup/setup-section.png and /dev/null differ diff --git a/erpnext/docs/assets/img/schools/setup/student-attendance-tool.gif b/erpnext/docs/assets/img/schools/setup/student-attendance-tool.gif new file mode 100644 index 00000000000..c8c1c05e809 Binary files /dev/null and b/erpnext/docs/assets/img/schools/setup/student-attendance-tool.gif differ diff --git a/erpnext/docs/assets/img/schools/setup/student-group-instructor.png b/erpnext/docs/assets/img/schools/setup/student-group-instructor.png new file mode 100644 index 00000000000..d9457a6d9c4 Binary files /dev/null and b/erpnext/docs/assets/img/schools/setup/student-group-instructor.png differ diff --git a/erpnext/docs/assets/img/schools/student/guardian.png b/erpnext/docs/assets/img/schools/student/guardian.png new file mode 100644 index 00000000000..01045be4901 Binary files /dev/null and b/erpnext/docs/assets/img/schools/student/guardian.png differ diff --git a/erpnext/docs/assets/img/schools/student/schools-settings.png b/erpnext/docs/assets/img/schools/student/schools-settings.png new file mode 100644 index 00000000000..8a71ac92edb Binary files /dev/null and b/erpnext/docs/assets/img/schools/student/schools-settings.png differ diff --git a/erpnext/docs/assets/img/schools/student/student group.gif b/erpnext/docs/assets/img/schools/student/student group.gif new file mode 100644 index 00000000000..13b1cf43420 Binary files /dev/null and b/erpnext/docs/assets/img/schools/student/student group.gif differ diff --git a/erpnext/docs/assets/img/schools/student/student-admission.gif b/erpnext/docs/assets/img/schools/student/student-admission.gif new file mode 100644 index 00000000000..a9ce8dc9b50 Binary files /dev/null and b/erpnext/docs/assets/img/schools/student/student-admission.gif differ diff --git a/erpnext/docs/assets/img/schools/student/student-batch-validation.gif b/erpnext/docs/assets/img/schools/student/student-batch-validation.gif new file mode 100644 index 00000000000..dd9f0e821d4 Binary files /dev/null and b/erpnext/docs/assets/img/schools/student/student-batch-validation.gif differ diff --git a/erpnext/docs/assets/img/schools/student/student-batch.gif b/erpnext/docs/assets/img/schools/student/student-batch.gif new file mode 100644 index 00000000000..43bae7734d7 Binary files /dev/null and b/erpnext/docs/assets/img/schools/student/student-batch.gif differ diff --git a/erpnext/docs/assets/img/schools/student/student-batch.png b/erpnext/docs/assets/img/schools/student/student-batch.png deleted file mode 100644 index 75a33140cc7..00000000000 Binary files a/erpnext/docs/assets/img/schools/student/student-batch.png and /dev/null differ diff --git a/erpnext/docs/assets/img/schools/student/student-course-validation.gif b/erpnext/docs/assets/img/schools/student/student-course-validation.gif new file mode 100644 index 00000000000..6ab9d00472b Binary files /dev/null and b/erpnext/docs/assets/img/schools/student/student-course-validation.gif differ diff --git a/erpnext/docs/assets/img/schools/student/student-group-attendance.gif b/erpnext/docs/assets/img/schools/student/student-group-attendance.gif new file mode 100644 index 00000000000..da9aa1ac269 Binary files /dev/null and b/erpnext/docs/assets/img/schools/student/student-group-attendance.gif differ diff --git a/erpnext/docs/assets/img/schools/student/student-group-creation-tool.gif b/erpnext/docs/assets/img/schools/student/student-group-creation-tool.gif new file mode 100644 index 00000000000..1fe1a6f0201 Binary files /dev/null and b/erpnext/docs/assets/img/schools/student/student-group-creation-tool.gif differ diff --git a/erpnext/docs/assets/img/schools/student/student-group-creation-tool.png b/erpnext/docs/assets/img/schools/student/student-group-creation-tool.png deleted file mode 100644 index 32e6498f8b2..00000000000 Binary files a/erpnext/docs/assets/img/schools/student/student-group-creation-tool.png and /dev/null differ diff --git a/erpnext/docs/assets/img/schools/student/student-group.png b/erpnext/docs/assets/img/schools/student/student-group.png deleted file mode 100644 index c562b001328..00000000000 Binary files a/erpnext/docs/assets/img/schools/student/student-group.png and /dev/null differ diff --git a/erpnext/docs/assets/img/subscription/__init__.py b/erpnext/docs/assets/img/subscription/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/docs/assets/img/subscription/subscription.gif b/erpnext/docs/assets/img/subscription/subscription.gif new file mode 100644 index 00000000000..68488053d67 Binary files /dev/null and b/erpnext/docs/assets/img/subscription/subscription.gif differ diff --git a/erpnext/docs/assets/img/subscription/subscription.png b/erpnext/docs/assets/img/subscription/subscription.png new file mode 100644 index 00000000000..8b2cdc30294 Binary files /dev/null and b/erpnext/docs/assets/img/subscription/subscription.png differ diff --git a/erpnext/docs/user/manual/en/accounts/articles/freeze-account.md b/erpnext/docs/user/manual/en/accounts/articles/freeze-account.md new file mode 100644 index 00000000000..3582c281248 --- /dev/null +++ b/erpnext/docs/user/manual/en/accounts/articles/freeze-account.md @@ -0,0 +1,13 @@ +# Freeze an Account + +Once an Account is Frozen, you won't be able to use it any accounting transaction. Since this is a critical action, you need to explicitly define a Role who can set an Account as Frozen. You can define this Role in the Account Settings. + +`Accounts > Account Settings` + +To freeze an Account, go to Chart of Accounts, and edit an Account. + +Download Backup + +If User has Role define in the  Account Setting assigned, then he/she will be able to set an Account as Frozen. + +Download Backup \ No newline at end of file diff --git a/erpnext/docs/user/manual/en/accounts/articles/index.txt b/erpnext/docs/user/manual/en/accounts/articles/index.txt index 8b7768a2d18..af8572c2332 100644 --- a/erpnext/docs/user/manual/en/accounts/articles/index.txt +++ b/erpnext/docs/user/manual/en/accounts/articles/index.txt @@ -13,4 +13,5 @@ what-is-the-differences-of-total-and-valuation-in-tax-and-charges withdrawing-salary-from-owners-equity-account adjust-withhold-amount-payment-entry common-receivable-account.md -types-in-tax-template \ No newline at end of file +types-in-tax-template +freeze-account \ No newline at end of file diff --git a/erpnext/docs/user/manual/en/customize-erpnext/articles/fetching-data-from-a-document.md b/erpnext/docs/user/manual/en/customize-erpnext/articles/fetching-data-from-a-document.md new file mode 100644 index 00000000000..04b20ac4b9f --- /dev/null +++ b/erpnext/docs/user/manual/en/customize-erpnext/articles/fetching-data-from-a-document.md @@ -0,0 +1,19 @@ +# Fetching Data from one Document to Another + +**Question:** We track Customer's PO No and PO Date field in the Sales Order. To have these values fetched into Sales Invoice as well, we have inserted Custom Field in the Sales Invoice. However, when we create Sales Invoice from the Sales Order, Customer's PO details are not being fetched. + +**Answer:** When data is fetched from one transaction to the another transaction, then the mapping of data is done based on the field names. If two transactions have fields with the exact same name, then it's values are mapped. + +For example, if you want Customer's PO No. and PO Date to be fetched from Sales Order to Sales Invoice, then you should ensure that Custom Fields added in the Sales Invoice has an exact same field name as in the Sales Order. + +Sales Order (standard fields) + +Standard fields in Sales Order + +Sales Invoice (custom fields) + +Custom Field in Sales Invoice + +Since names for the Customer's PO related fields are same in the Sales Order and Sales Invoice, when creating Sales Invoice from the Sales Order, values in these fields are auto-fetched. + +Values fetching from Sales Order to Sales Invoice \ No newline at end of file diff --git a/erpnext/docs/user/manual/en/customize-erpnext/articles/index.txt b/erpnext/docs/user/manual/en/customize-erpnext/articles/index.txt index 1740ef0e943..56e7040b47a 100644 --- a/erpnext/docs/user/manual/en/customize-erpnext/articles/index.txt +++ b/erpnext/docs/user/manual/en/customize-erpnext/articles/index.txt @@ -15,4 +15,5 @@ set-language set-precision user-restriction maximum-numbers-of-fields-in-a-form -child-table \ No newline at end of file +child-table +fetching-data-from-a-document \ No newline at end of file diff --git a/erpnext/docs/user/manual/en/human-resources/training.md b/erpnext/docs/user/manual/en/human-resources/training.md index 39b9790246d..2aa06791f05 100644 --- a/erpnext/docs/user/manual/en/human-resources/training.md +++ b/erpnext/docs/user/manual/en/human-resources/training.md @@ -9,22 +9,23 @@ Schedule seminars, workshops, conferences etc using Training Event. You can also ### Inviting Employees for Event You can invite your employees to attend the event. You can do so by selecting the employees to be invited in the employee table. + By default the status of the employee will be 'Open'. -The system shall notify the employee with status 'Open' by sending a email to the office email address of the employee as mentioned in the employee master if you have selected 'Send Email' checkbox. -The status is changed to 'Invited' when an invitation email is sent to the employee by the system. -When an Employee confirms his/her presence for the event you can change the status to 'Confirmed'. Employee +When you submit the Training Event, a notifcation will be sent to the employee notifying that the Training has been scheduled. This is sent via Email Alert "Training Scheduled". You can modifiy this Email Alert to customize the message. + ### Training Result -After compleation of the training Employee Wise training results can be stored based on the Feedback received from the Trainer. +After compleation of the training Employee-wise training results can be stored based on the Feedback received from the Trainer. Employee +When the Training Result is submitted, all the employees will receive an email notifying them that they must share their feedback via "Training Feedback". This is also managed via an Email Alert, so you can customize this alert too. -### Trainig Feedback +### Training Feedback -Collect feedback regarding the event from your Employees using Training Feedback. +Employees can then share their feedback via Training Feedback. Employee diff --git a/erpnext/docs/user/manual/en/index.txt b/erpnext/docs/user/manual/en/index.txt index fff4da7d8ef..712ab8eabd3 100644 --- a/erpnext/docs/user/manual/en/index.txt +++ b/erpnext/docs/user/manual/en/index.txt @@ -9,6 +9,7 @@ manufacturing projects support human-resources +subscription customer-portal website using-erpnext diff --git a/erpnext/docs/user/manual/en/projects/timesheet/sales-invoice-from-timesheet.md b/erpnext/docs/user/manual/en/projects/timesheet/sales-invoice-from-timesheet.md index 9be2bad0e21..4dbdcbb6dc3 100644 --- a/erpnext/docs/user/manual/en/projects/timesheet/sales-invoice-from-timesheet.md +++ b/erpnext/docs/user/manual/en/projects/timesheet/sales-invoice-from-timesheet.md @@ -48,6 +48,8 @@ In the Timesheet, if "Is Billable" is checked, you will find option to create Sa Sales Invoice +Sales Invoice timesheet + ####Sales Invoice Sales Invoice has dedicated table for the Timesheet table where Timesheet details will be updated. You can select more Timesheets in this table. diff --git a/erpnext/docs/user/manual/en/schools/Assessment/assessment_criteria_group.md b/erpnext/docs/user/manual/en/schools/Assessment/assessment_criteria_group.md new file mode 100644 index 00000000000..4287ca86179 --- /dev/null +++ b/erpnext/docs/user/manual/en/schools/Assessment/assessment_criteria_group.md @@ -0,0 +1 @@ +# \ No newline at end of file diff --git a/erpnext/docs/user/manual/en/schools/Attendance/__init__.py b/erpnext/docs/user/manual/en/schools/Attendance/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/docs/user/manual/en/schools/Attendance/index.md b/erpnext/docs/user/manual/en/schools/Attendance/index.md new file mode 100644 index 00000000000..3153a590227 --- /dev/null +++ b/erpnext/docs/user/manual/en/schools/Attendance/index.md @@ -0,0 +1,6 @@ +#Attendance + + +### Topics + +{index} \ No newline at end of file diff --git a/erpnext/docs/user/manual/en/schools/Attendance/index.txt b/erpnext/docs/user/manual/en/schools/Attendance/index.txt new file mode 100644 index 00000000000..8cd02627a20 --- /dev/null +++ b/erpnext/docs/user/manual/en/schools/Attendance/index.txt @@ -0,0 +1,3 @@ +student-attendance +student-leave-application +student-attendance-tool \ No newline at end of file diff --git a/erpnext/docs/user/manual/en/schools/Attendance/student-attendance-tool.md b/erpnext/docs/user/manual/en/schools/Attendance/student-attendance-tool.md new file mode 100644 index 00000000000..5c4ce3072a3 --- /dev/null +++ b/erpnext/docs/user/manual/en/schools/Attendance/student-attendance-tool.md @@ -0,0 +1,15 @@ +# Student Attendance Tool + +The Student Attendance tool allow you to bulk update the attendance for students based on **Student Group and Course Schedule**. + +To mark the **Attedance* based on Student Group select the group based on + +**1. Batch + 2. Course + 3. Activity ** + +Student detials will be autofetched and you can mark the attendance of the given date. + +Student Attendance + +{next} \ No newline at end of file diff --git a/erpnext/docs/user/manual/en/schools/Attendance/student-attendance.md b/erpnext/docs/user/manual/en/schools/Attendance/student-attendance.md new file mode 100644 index 00000000000..1b917d0ae26 --- /dev/null +++ b/erpnext/docs/user/manual/en/schools/Attendance/student-attendance.md @@ -0,0 +1,15 @@ +# Student Attendance + +Attendance doctype allows you to track and manage attendance of a student in all the days at any time. The Attendance module is designed to help teachers easily mark student attendance during class. + +Attendance Records can be created against Student on daily basis. + +To create Attendance record : + +Select the **Student, Course Schedule and Student Group** for which attendance is to be marked for the given date. Set the Status to Present/Absent and save. + +Student Attendance + +**Student Attendance tool** can be used for bulk updation of the attendance based on **Batch, Course or Activity**. + +{next} \ No newline at end of file diff --git a/erpnext/docs/user/manual/en/schools/Attendance/student-leave-application.md b/erpnext/docs/user/manual/en/schools/Attendance/student-leave-application.md new file mode 100644 index 00000000000..5620bdadd73 --- /dev/null +++ b/erpnext/docs/user/manual/en/schools/Attendance/student-leave-application.md @@ -0,0 +1,13 @@ +#Student Leave Application + +ERPNext allows you to record the leave application for a student. + +To create a Student Leave application record, enter the Student and the date for the leave is applied and save. + +Student Attendance + +Incase the student is not attending the school in order to participate or represent school in any event, he/she can be mark as present from the Leave Application itself. + +Once a Leave Application is recorded for a student it will not be recorded in the absent student report as he has applied for a leave. + +{next} \ No newline at end of file diff --git a/erpnext/docs/user/manual/en/schools/admission/index.md b/erpnext/docs/user/manual/en/schools/admission/index.md index 87cdfe5c899..b602271bbbd 100644 --- a/erpnext/docs/user/manual/en/schools/admission/index.md +++ b/erpnext/docs/user/manual/en/schools/admission/index.md @@ -1,6 +1,6 @@ # Admission -This section contains student admission related documents. +The Admission section allow you to create all records starting from Student application till the program enrollment. Below is the list of documents for Student addmission. ### Topics diff --git a/erpnext/docs/user/manual/en/schools/admission/index.txt b/erpnext/docs/user/manual/en/schools/admission/index.txt index ec9e768116a..680d7790897 100644 --- a/erpnext/docs/user/manual/en/schools/admission/index.txt +++ b/erpnext/docs/user/manual/en/schools/admission/index.txt @@ -1,2 +1,4 @@ +student-admission student-applicant -program-enrollment \ No newline at end of file +program-enrollment +program-enrollment-Tool diff --git a/erpnext/docs/user/manual/en/schools/admission/program-enrollment-tool.md b/erpnext/docs/user/manual/en/schools/admission/program-enrollment-tool.md new file mode 100644 index 00000000000..2a2fa1e58e7 --- /dev/null +++ b/erpnext/docs/user/manual/en/schools/admission/program-enrollment-tool.md @@ -0,0 +1,16 @@ +# Program Enrollment Tool + +The Program Enrollment tool allows the bulk enrollment of the **Student Applicants** in a Program. + + +You can create the the Program Enrollment for : + +1. **Student Applicants** >> List of Student Applicants will be fetched for the selected **Program** and **Academic year**. + +Student Applicant Enrollment + +2. **Program Enrollment** >> You can bulk update the **Program** for the students from one academic year to another in the same **Program** or a new **Program**. + +Student Applicant Enrollment + +{next} \ No newline at end of file diff --git a/erpnext/docs/user/manual/en/schools/admission/program-enrollment.md b/erpnext/docs/user/manual/en/schools/admission/program-enrollment.md index 2dcc6cb2cec..cc1308cf6c1 100644 --- a/erpnext/docs/user/manual/en/schools/admission/program-enrollment.md +++ b/erpnext/docs/user/manual/en/schools/admission/program-enrollment.md @@ -1,7 +1,12 @@ # Program Enrollment -This form allows you to enroll a student to a program. A student can be enrolled to multiple programs. +Program Enrollment describes an educational model where students must complete a defined set of courses towards their academic objective in a specified sequence. Enrollment is a program driven process in which the student select the Program to enrol for in a Academic Year. -Student Applicant Enrollment +Once a student have applied for the **Program** and the application is approved, the program enrollment is done for that student. + +Student Applicant Enrollment + +- A student can be enrolled in multiple Course for a program in a given academeic year. +- Based on the Fee structure selected at the time of enrollment Fee detials are created of the student. {next} diff --git a/erpnext/docs/user/manual/en/schools/admission/student-applicant.md b/erpnext/docs/user/manual/en/schools/admission/student-applicant.md index e67c1dc79e6..22d370939d1 100644 --- a/erpnext/docs/user/manual/en/schools/admission/student-applicant.md +++ b/erpnext/docs/user/manual/en/schools/admission/student-applicant.md @@ -18,7 +18,9 @@ You can Approve or Reject a student applicant. By accepting a student applicant and will not allow you to change the application status unless the student record is deleted. ### Student Enrollment +Once the form is submitted you can either approve or reject the application form. +Student Applicant Enrollment Once you approve a Student Applicant you can enroll them to a program. When you click the 'Enroll' buttom, the system shall create a student against that applicant and redirect you to the [Program Enrollment form](/docs/user/manual/en/schools/student/program-enrollment.html). diff --git a/erpnext/docs/user/manual/en/schools/admission/student_admission.md b/erpnext/docs/user/manual/en/schools/admission/student_admission.md new file mode 100644 index 00000000000..eeaa977912b --- /dev/null +++ b/erpnext/docs/user/manual/en/schools/admission/student_admission.md @@ -0,0 +1,13 @@ +# Student Admission + +The admission process begins with filling the admission form. The Student Admission record enables to intitate your admission process for a given **Academic year**. ERPNext admission module allow you to create an admission record which can be then published on the ERPNext generate website. + +To create a Student Admission record go to : + +**Schools** >> **Admissions** >> **Student Admission** >> + + +Student Applicant + + +Once a admission record is created it can be published on the website and the student can apply from the web portal itself. \ No newline at end of file diff --git a/erpnext/docs/user/manual/en/schools/index.md b/erpnext/docs/user/manual/en/schools/index.md index 8c766d6db49..d317d39bfaa 100644 --- a/erpnext/docs/user/manual/en/schools/index.md +++ b/erpnext/docs/user/manual/en/schools/index.md @@ -1,7 +1,7 @@ # Schools -The School Modules is designed to meet requirements of Schools, Colleges & Educational Institutes. +The School Modules in ERPNext is designed to meet requirements of Schools, Colleges & Educational Institutes. This is a centralized system, which maintains and updates all the activities related to an Institution. This will ease the process of each and every aspect of a School, be it Students, Admission, Examination and Fee. Fees Section diff --git a/erpnext/docs/user/manual/en/schools/index.txt b/erpnext/docs/user/manual/en/schools/index.txt index b485fdcfea5..159089c6f38 100644 --- a/erpnext/docs/user/manual/en/schools/index.txt +++ b/erpnext/docs/user/manual/en/schools/index.txt @@ -1,6 +1,7 @@ student admission +Attendance schedule fees setup -assessment \ No newline at end of file +Assessment \ No newline at end of file diff --git a/erpnext/docs/user/manual/en/schools/schedule/student-attendance.md b/erpnext/docs/user/manual/en/schools/schedule/student-attendance.md deleted file mode 100644 index 84e1ea8669d..00000000000 --- a/erpnext/docs/user/manual/en/schools/schedule/student-attendance.md +++ /dev/null @@ -1,7 +0,0 @@ -# Student Attendance - -Maintains attendance record of the student. Attendance Records can be created against Course Schedules. - -Student Attendance - -{next} \ No newline at end of file diff --git a/erpnext/docs/user/manual/en/schools/setup/academic-term.md b/erpnext/docs/user/manual/en/schools/setup/academic-term.md index 6306444b616..b52bea71086 100644 --- a/erpnext/docs/user/manual/en/schools/setup/academic-term.md +++ b/erpnext/docs/user/manual/en/schools/setup/academic-term.md @@ -1,5 +1,9 @@ # Academic Term +An academic term (or simply "term") is a portion of an academic year, the time during which an educational institution holds classes. The schedules adopted vary widely. The academic term can be a quater, trimester or a semester. + +The **Academic term** form in ERPNext enables you to create academic terms within in a year. Based on the term schedule enter the start and end date for the schedule and generate the term for a Academic year. + Academic Term diff --git a/erpnext/docs/user/manual/en/schools/setup/academic-year.md b/erpnext/docs/user/manual/en/schools/setup/academic-year.md index 3913eb634b2..4fc5f929a56 100644 --- a/erpnext/docs/user/manual/en/schools/setup/academic-year.md +++ b/erpnext/docs/user/manual/en/schools/setup/academic-year.md @@ -1,5 +1,11 @@ # Academic Year +An academic year is a period of time which schools, colleges and universities use to measure a quantity of study. + +The **Academic year** form have the Start and End date for the Academic year. + Academic Year -{next} \ No newline at end of file +**Student group** link is given to view or add the respective groups to the Academic year. + +{next} \ No newline at end of file diff --git a/erpnext/docs/user/manual/en/schools/setup/course.md b/erpnext/docs/user/manual/en/schools/setup/course.md index 140131dccd2..dc10ed54b17 100644 --- a/erpnext/docs/user/manual/en/schools/setup/course.md +++ b/erpnext/docs/user/manual/en/schools/setup/course.md @@ -1,5 +1,15 @@ # Course + A course is a unit of teaching that typically lasts one academic term, is led by one or more instructors (teachers or professors), and has a fixed number of students. Students may receive a grade and academic credit after completion of the course. + +To create a **Course** enter the Course name and Code. Code for the course should be unique for every course. You can also link the department under which the course is conducted. + Course +Once a **Course** is created, a course schedule can defined for the same. + +Course + +The Course form is further linked to **Program, Student Group and Assessment Plan** doctypes. The links allow to view/create the related documents for a **Course**. + {next} \ No newline at end of file diff --git a/erpnext/docs/user/manual/en/schools/setup/index.md b/erpnext/docs/user/manual/en/schools/setup/index.md index 9de753614c8..4e0915277b7 100644 --- a/erpnext/docs/user/manual/en/schools/setup/index.md +++ b/erpnext/docs/user/manual/en/schools/setup/index.md @@ -1,5 +1,7 @@ # Setup +The Setup section of Schools module provides facility to make some basic configuration. Below are doctypes for basic configuration. + Setup Section ### Topics diff --git a/erpnext/docs/user/manual/en/schools/setup/index.txt b/erpnext/docs/user/manual/en/schools/setup/index.txt index fb9ba05d090..8fb9bb2def9 100644 --- a/erpnext/docs/user/manual/en/schools/setup/index.txt +++ b/erpnext/docs/user/manual/en/schools/setup/index.txt @@ -2,5 +2,8 @@ course program instructor room +student-category +student-batch-name academic-term -academic-year \ No newline at end of file +academic-year +school-settings \ No newline at end of file diff --git a/erpnext/docs/user/manual/en/schools/setup/instructor.md b/erpnext/docs/user/manual/en/schools/setup/instructor.md index 1a4d35161c2..6150f025fe7 100644 --- a/erpnext/docs/user/manual/en/schools/setup/instructor.md +++ b/erpnext/docs/user/manual/en/schools/setup/instructor.md @@ -1,5 +1,18 @@ # Instructor +An instructoe is a teacher, or professor, of a specialised subject that involves skill. + +You can create an Instructor and link it to the Employee master and a Departmemt. + Instructor +An **Instructor** is further linked to a **Course Schedule**, where you can define the schedule for a **Course** for a give date and **Room no**. + +Instructor +It is also linked to **Student group** where an **Instructor** is assigned to the Student group. + +Instructor + +An **Instructor** is also linked to an **Assesment Plan** for a Student group. The Instructor can be an Examiner or the supervisor for the assesment. + {next} \ No newline at end of file diff --git a/erpnext/docs/user/manual/en/schools/setup/program.md b/erpnext/docs/user/manual/en/schools/setup/program.md index 64513e20b0f..be10166e7be 100644 --- a/erpnext/docs/user/manual/en/schools/setup/program.md +++ b/erpnext/docs/user/manual/en/schools/setup/program.md @@ -1,5 +1,19 @@ # Program +An educational program is a program written by the institutions which determines the learning progress of each subject in all the stages of formal education. + +To create a Program go to : + +###Schools >> Setup >> Program >> New Program + +Enter a unique code for every **Program**. You can also link the **Program** to the department under which it is conducted. + Program +Add the relevant Course and the Fee details for a program. + +Program + +The Program Doctype is further linked to the **Student applicant**, **Program enrollment, Student group, Fee structre and Fee**. The links allow to view or create the related document for a Program. + {next} \ No newline at end of file diff --git a/erpnext/docs/user/manual/en/schools/setup/room.md b/erpnext/docs/user/manual/en/schools/setup/room.md index bb265e3ea21..43ddde92245 100644 --- a/erpnext/docs/user/manual/en/schools/setup/room.md +++ b/erpnext/docs/user/manual/en/schools/setup/room.md @@ -1,6 +1,17 @@ # Room +A classroom is a space (room or lab) where you want to schedule courses or examinations. A room in an educational institute can be a Class room, a laboratory or a Examination hall. + +The Room doctype allows you to record the room number and the seating capacity for a classroom. Once a room is created Course schedule link is provided in the Room doctype to view or add the course schedule for the classroom. Room +The course schedule validate the availability of the Room number and an alert message is shown if there is an overlap for the Room number for a given time slot. + +Room + +The Room number is further linked to the Assesment plan. It validates the availability of examination room for the assessment to be held for a given date and time. + +Room + {next} \ No newline at end of file diff --git a/erpnext/docs/user/manual/en/schools/setup/school-settings.md b/erpnext/docs/user/manual/en/schools/setup/school-settings.md new file mode 100644 index 00000000000..ce9e9144b7a --- /dev/null +++ b/erpnext/docs/user/manual/en/schools/setup/school-settings.md @@ -0,0 +1,15 @@ +#School Settings + +The Schools settings page allow you to setup basic settings like **Academic Year and Term** for the Schools setup. + +Student + +The checkbox to Validate Batch for Students in Student Group enables the Student Batch validation for every Student from the Program Enrollment for the **Batch** based on **Student Group** + +Student + +You can enable the validation of Course for every Student from the enrolled Courses in Program Enrollment,for Course based Student Group by checking the settings for **Validate Enrolled Course for Students in Student Group** + +Student + +{next} \ No newline at end of file diff --git a/erpnext/docs/user/manual/en/schools/setup/student-batch-name.md b/erpnext/docs/user/manual/en/schools/setup/student-batch-name.md new file mode 100644 index 00000000000..056f9108636 --- /dev/null +++ b/erpnext/docs/user/manual/en/schools/setup/student-batch-name.md @@ -0,0 +1,10 @@ +# Student Batch + +Student batch is a collection of students from Student Groups. **Student batch** allows you to create **Student Group** based on a batch. When a student is enrolled for a **Program**, the Student batch is selected to enroll the student for the given Program and batch + +Student + +You can also get a **Student Batch-Wise Attendance** report to view the number of student present from the Batch. + + +{next} diff --git a/erpnext/docs/user/manual/en/schools/setup/student-category.md b/erpnext/docs/user/manual/en/schools/setup/student-category.md new file mode 100644 index 00000000000..0d769279727 --- /dev/null +++ b/erpnext/docs/user/manual/en/schools/setup/student-category.md @@ -0,0 +1,12 @@ +# Student Category + +Student Category doctype allow you to classify student based various categories. In Institutions, there may be fee concession for some categories such as Handicapped students, foreign, nationals, reserved category by the government etc. + +To create Student category go to Setup >> Student Category >> New. + +We can create new student category by adding a name and save it + +Student + + +{next} \ No newline at end of file diff --git a/erpnext/docs/user/manual/en/schools/student/guardian.md b/erpnext/docs/user/manual/en/schools/student/guardian.md new file mode 100644 index 00000000000..7bbdbc22c5c --- /dev/null +++ b/erpnext/docs/user/manual/en/schools/student/guardian.md @@ -0,0 +1,9 @@ +#Guardian + +The Guardian doctype allows you to record the guardian details for a **Student**. + +Student + +The email id added in the **Guardian** detail can be linked to a email group for sending newsletter or announcements. + +{next} \ No newline at end of file diff --git a/erpnext/docs/user/manual/en/schools/student/index.txt b/erpnext/docs/user/manual/en/schools/student/index.txt index 9b31be4dc98..89704b1fc79 100644 --- a/erpnext/docs/user/manual/en/schools/student/index.txt +++ b/erpnext/docs/user/manual/en/schools/student/index.txt @@ -1,5 +1,6 @@ student +guardian student-log student-batch student-group -student-group-creation-tool +student-group-creation-tool \ No newline at end of file diff --git a/erpnext/docs/user/manual/en/schools/student/student-batch.md b/erpnext/docs/user/manual/en/schools/student/student-batch.md index 4987c0313f2..056f9108636 100644 --- a/erpnext/docs/user/manual/en/schools/student/student-batch.md +++ b/erpnext/docs/user/manual/en/schools/student/student-batch.md @@ -1,7 +1,10 @@ # Student Batch -Student batch is a collection of students from Student Groups. +Student batch is a collection of students from Student Groups. **Student batch** allows you to create **Student Group** based on a batch. When a student is enrolled for a **Program**, the Student batch is selected to enroll the student for the given Program and batch + +Student + +You can also get a **Student Batch-Wise Attendance** report to view the number of student present from the Batch. -Student {next} diff --git a/erpnext/docs/user/manual/en/schools/student/student-group-creation-tool.md b/erpnext/docs/user/manual/en/schools/student/student-group-creation-tool.md index 130c94491eb..1cd9b1e796f 100644 --- a/erpnext/docs/user/manual/en/schools/student/student-group-creation-tool.md +++ b/erpnext/docs/user/manual/en/schools/student/student-group-creation-tool.md @@ -1,8 +1,17 @@ # Student Group Creation Tool -This tool allows you to create student groups in bulk. You can specify multiple parameters to create them. +The Student group creation tool allows you to create student groups in bulk. +To create Student group using this tool go to -Student Group Creation Tool +##Schools >>Student >> Student Group creation tool + +Select the **Academic Term** and the **Program** for which a student group is to be created. + +Student Group Creation Tool + +By default the student group is created based on the **Course** only. The check box for "Separate course based Group for every Batch" allows you to create batchwise Student groups for a course. + +You can leave it unchecked if you don't want to consider batch while making course based groups. {next} \ No newline at end of file diff --git a/erpnext/docs/user/manual/en/schools/student/student-group.md b/erpnext/docs/user/manual/en/schools/student/student-group.md index 191e917a8bf..cf3f82c5a2d 100644 --- a/erpnext/docs/user/manual/en/schools/student/student-group.md +++ b/erpnext/docs/user/manual/en/schools/student/student-group.md @@ -1,8 +1,22 @@ # Student Group -A student group is a collection of students taking a same course. You can create Course Schedules and Examinations against a Student Group. -A student group needs to be created for every course in a particular academic term and academic year. +A student group is a collection of students taking same course. You can create Course Schedules and Examinations against a Student Group. + +A Student Group needs to be created for every course for **Academic Term** and **Academic Year**. The student group can be create based on **Batch, Course and Activity**. + +To create a Student Group go to: + +Schools >> Student >> New Student Group + +Student Group + +To create a Student group based on **Batch**, select the **Progam** and **Batch**, where as to create a Student group based on **Course**, you will only have to select the Course Code. Creating a student group based on activity allows you to group of student for events and activities happening in the institute. + +Once a student group is created you can mark attendance for the group. + +Student Group + +You can also update the **Email Group** for the Student Group. Click on Update Email Group to add all the email ids of the gaurdians in the respective email group and **Newsletter** can be created and sent to the Email group. -Student Group {next} \ No newline at end of file diff --git a/erpnext/docs/user/manual/en/schools/student/student-log.md b/erpnext/docs/user/manual/en/schools/student/student-log.md index a037a231ac6..160e39fcdc4 100644 --- a/erpnext/docs/user/manual/en/schools/student/student-log.md +++ b/erpnext/docs/user/manual/en/schools/student/student-log.md @@ -1,6 +1,7 @@ # Student Log -You can make a note of student activities using student log. +The Student log Doctype enables you to add and edit addtional information for a student. +You can make a note of student activities using Student log. Logs can be categorised as 'General', 'Academic', 'Medical' or 'Achievement' Student diff --git a/erpnext/docs/user/manual/en/schools/student/student.md b/erpnext/docs/user/manual/en/schools/student/student.md index dd99a80d2f4..09e44714e68 100644 --- a/erpnext/docs/user/manual/en/schools/student/student.md +++ b/erpnext/docs/user/manual/en/schools/student/student.md @@ -1,10 +1,12 @@ # Student A Student is a person who has enrolled at your institute and you have accepted their application. -The student doctype maintains personal details of the student. - -You can view everything related to a particular student on this page. Eg : Fees, Student Group, etc +The Student doctype maintains detials like personal information, date of birth, address etc. It also records the **Guardian** and sibling details. Student +The student is enrolled in a **Program** when the application is approved. Once the enrollement is done the **Student Applicant** status is update to Admitted. -{next} +You can view every doctype created for a particular student. Eg : Fees, Student Group, etc + + +{next} \ No newline at end of file diff --git a/erpnext/docs/user/manual/en/setting-up/articles/email-error.md b/erpnext/docs/user/manual/en/setting-up/articles/email-error.md new file mode 100644 index 00000000000..9d5abd021e7 --- /dev/null +++ b/erpnext/docs/user/manual/en/setting-up/articles/email-error.md @@ -0,0 +1,9 @@ +# Email Error in Sending or Receiving + +In ERPNext, you can customize the Incoming and Outgoing Email Gateway. On saving an Email Account, ERPNext tries establishing a connection with your email gateway. If your ERPNext account is able to connect fine, then Email Account is saved successfully. If not, then you might receive an error as below. + +Email Error + +This indicates that using login credentials and other email gateway details provided in the Email Account, ERPNext is not able to connect to your email server. Please ensure that you have entered valid email credentials for your Email Gateway. Once you have configured Email Account successfully, you should be able to send and receive emails from your ERPNext account fine. + +Note: Your ERPNext account is connected with an ERPNext email server by default. If you don't want to use your own email server, you can continue sending emails using ERPNext email server, without any configuration required in the Email Account. \ No newline at end of file diff --git a/erpnext/docs/user/manual/en/setting-up/articles/index.txt b/erpnext/docs/user/manual/en/setting-up/articles/index.txt index d175d929dae..259229ec8d6 100644 --- a/erpnext/docs/user/manual/en/setting-up/articles/index.txt +++ b/erpnext/docs/user/manual/en/setting-up/articles/index.txt @@ -14,4 +14,5 @@ using-custom-domain-on-erpnext setup-two-factor-authentication difference-between-system-user-and-website-user outgoing-email-gateway -print-format-sections \ No newline at end of file +print-format-sections +email-error \ No newline at end of file diff --git a/erpnext/docs/user/manual/en/setting-up/data/download-backup.md b/erpnext/docs/user/manual/en/setting-up/data/download-backup.md new file mode 100644 index 00000000000..6c98cf34f84 --- /dev/null +++ b/erpnext/docs/user/manual/en/setting-up/data/download-backup.md @@ -0,0 +1,13 @@ +# Download Backup + +In the ERPNext, you can manually download database backup. To get the latest database backup, go to: + +`Setup > Data > Download Backup` + +Backup available for the download is updated in every eight hours. Click on the link to download the backups at a given time. + +Download Backup + +By default three latest backups will be available for the download. If you want to customize no. of backups, then click on "Set Number of Backups". In the System Settings, you can set Number of Backups available for the download at a time. + +Download Backup \ No newline at end of file diff --git a/erpnext/docs/user/manual/en/setting-up/data/index.txt b/erpnext/docs/user/manual/en/setting-up/data/index.txt index 998b5a21647..d15f9175bb3 100644 --- a/erpnext/docs/user/manual/en/setting-up/data/index.txt +++ b/erpnext/docs/user/manual/en/setting-up/data/index.txt @@ -1,2 +1,3 @@ data-import-tool bulk-rename +download-backup diff --git a/erpnext/docs/user/manual/en/setting-up/setup-wizard/index.md b/erpnext/docs/user/manual/en/setting-up/setup-wizard/index.md index 244d893c7bb..c05c4af5b6a 100644 --- a/erpnext/docs/user/manual/en/setting-up/setup-wizard/index.md +++ b/erpnext/docs/user/manual/en/setting-up/setup-wizard/index.md @@ -1,6 +1,6 @@ # Setup Wizard -The Setup Wizard helps you quickly setup your ERPnext by helping you create your company, Items, Customer, Suppliers and will also setup a basic website with this data. +The Setup Wizard helps you quickly setup ERPnext as per your locale and sets up your organisation. Here is a quick overview of the steps: diff --git a/erpnext/docs/user/manual/en/setting-up/setup-wizard/index.txt b/erpnext/docs/user/manual/en/setting-up/setup-wizard/index.txt index eb7655826e7..b2f680a388b 100644 --- a/erpnext/docs/user/manual/en/setting-up/setup-wizard/index.txt +++ b/erpnext/docs/user/manual/en/setting-up/setup-wizard/index.txt @@ -1,10 +1,5 @@ step-1-language step-2-currency-and-timezone step-3-user-details +step-4-two-factor-authentication step-5-company-details -step-6-letterhead-and-logo -step-7-add-users -step-8-tax-details -step-9-customer-names -step-10-suppliers -step-11-item diff --git a/erpnext/docs/user/manual/en/setting-up/setup-wizard/step-10-suppliers.md b/erpnext/docs/user/manual/en/setting-up/setup-wizard/step-10-suppliers.md deleted file mode 100644 index 364b4d6982c..00000000000 --- a/erpnext/docs/user/manual/en/setting-up/setup-wizard/step-10-suppliers.md +++ /dev/null @@ -1,12 +0,0 @@ -# Step 10: Suppliers - -Enter a few of your Suppliers' names. - -Suppliers - ---- - -To understand Suppliers in detail visit [Supplier Master](/docs/user/manual/en/buying/supplier.html) - -{next} diff --git a/erpnext/docs/user/manual/en/setting-up/setup-wizard/step-11-item.md b/erpnext/docs/user/manual/en/setting-up/setup-wizard/step-11-item.md deleted file mode 100644 index 42d7e3d26d5..00000000000 --- a/erpnext/docs/user/manual/en/setting-up/setup-wizard/step-11-item.md +++ /dev/null @@ -1,16 +0,0 @@ -# Step 11: Item Names - -In this final step, please enter the names of the Items you buy or sell. - -Add Items - -Please set the group of the item (Product / Service) and unit of measure. Don't worry you will be able to edit all of this later. - ---- - -## Thats it! - -Once you are done with the setup wizard you will see the familiar desktop page. - -{next} diff --git a/erpnext/docs/user/manual/en/setting-up/setup-wizard/step-6-letterhead-and-logo.md b/erpnext/docs/user/manual/en/setting-up/setup-wizard/step-6-letterhead-and-logo.md deleted file mode 100644 index 0286a3ee23e..00000000000 --- a/erpnext/docs/user/manual/en/setting-up/setup-wizard/step-6-letterhead-and-logo.md +++ /dev/null @@ -1,23 +0,0 @@ -# Step 6: Letterhead and Logo - -Attach Company Letterhead and Company Logo. - -Company Logo and Letterhead - ---- - -### Letterhead - -A letterhead is the heading at the top of a sheet of letter paper (stationery). That heading usually consists of a name and an address, and a logo or corporate design. - -Click on the box ‘Attach Letterhead’ . Select the image file from the place it is stored and click enter. - -You may choose to skip this step if your letterhead is not ready. - -To select letterhead later through the setup module, read [Letter-head](/docs/user/manual/en/setting-up/print/letter-head.html) - -#### To "attach as web-link" - -For any attachments in ERPNext, you can also attach as a web-link. If you are using other tools like Dropbox or Google Docs to manage your files, you can set its public link. - -{next} diff --git a/erpnext/docs/user/manual/en/setting-up/setup-wizard/step-7-add-users.md b/erpnext/docs/user/manual/en/setting-up/setup-wizard/step-7-add-users.md deleted file mode 100644 index c92721c91b0..00000000000 --- a/erpnext/docs/user/manual/en/setting-up/setup-wizard/step-7-add-users.md +++ /dev/null @@ -1,7 +0,0 @@ -# Step 7: Add Users - -Add other users and assign them roles based on their job responsibilities. - -Users - -{next} diff --git a/erpnext/docs/user/manual/en/setting-up/setup-wizard/step-8-tax-details.md b/erpnext/docs/user/manual/en/setting-up/setup-wizard/step-8-tax-details.md deleted file mode 100644 index dae88e4432a..00000000000 --- a/erpnext/docs/user/manual/en/setting-up/setup-wizard/step-8-tax-details.md +++ /dev/null @@ -1,21 +0,0 @@ -# Step 8: Tax Details - -Enter any three types of taxes which you regularly pay. This wizard will create a tax master which will calculate the taxes as per the tax-type. - -Tax Details - -Just set the tax name and the standard percentage levied. - ---- - -Some examples of tax types are given below. - -#### VAT - -A value added tax (VAT) is a form of consumption tax. From the perspective of the buyer, it is a tax on the purchase price. From that of the seller, it is a tax only on the value added to a product, material, or a service. From an accounting point of view, by the stage of its manufacture or distribution. The manufacturer remits to the government the difference between these two amounts, and retains the rest for themselves to offset the taxes they had previously paid on the inputs. - -The purpose of VAT is to generate tax revenues to the government similar to the corporate income tax or the personal income tax. For Example: When you shop at a departmental store and avail discount on the products, the store charges you 5% extra on the total bill as the VAT. - -To setup VAT in the setup wizard , simply enter the percentage amount levied by your government. To setup VAT at a later stage read [setting-up-taxes](/docs/user/manual/en/setting-up/setting-up-taxes.html) - -{next} diff --git a/erpnext/docs/user/manual/en/setting-up/setup-wizard/step-9-customer-names.md b/erpnext/docs/user/manual/en/setting-up/setup-wizard/step-9-customer-names.md deleted file mode 100644 index e3489433fcb..00000000000 --- a/erpnext/docs/user/manual/en/setting-up/setup-wizard/step-9-customer-names.md +++ /dev/null @@ -1,22 +0,0 @@ -# Step 9: Customers - -Enter your Customer names and the contact person from that organisation. - - -Customers - ---- - -#### Difference between a customer name and a contact name - -A customer name is the name of the organisation and a contact name is the name of the person from that organisation. - -For Example: If American Power Mills is an organisation name and their founder Shiv Agarwal has installed ERPNext on his system. Then, - -Customer Name: American Power Mills - -Contact Name: Shiv Agarwal - -To understand Customer in detail visit [Customer Details](/docs/user/manual/en/CRM/customer.html) - -{next} diff --git a/erpnext/docs/user/manual/en/stock/articles/delivery-note-stock-error.md b/erpnext/docs/user/manual/en/stock/articles/delivery-note-stock-error.md new file mode 100644 index 00000000000..afb6de92d8c --- /dev/null +++ b/erpnext/docs/user/manual/en/stock/articles/delivery-note-stock-error.md @@ -0,0 +1,7 @@ +# Delivery Note Negative Stock Error + +**Question**: When submitting a Delivery Note, receiving a message says that item's stock is insufficient, but we have item's stock available in the Warehouse. + +**Answer**: On submission of Delivery Note, stock level is checked as on Posting Date and Posting Time of a Delivery Note. It's possible that you have stock of an Item available in the Warehouse. But if you are creating back-dated Delivery Note, and if item was not available in the warehouse on the Posting Date and Posting Time of Delivery Note, you are likely to receive an error message on the negative stock. You can refer to the Stock Ledger report to confirm the same. + +If this is the case, you should edit the Posting Date and Time of a Delivery Note, and ensure that it is after the Posting Date and Time of item's receipt entry. \ No newline at end of file diff --git a/erpnext/docs/user/manual/en/stock/articles/index.txt b/erpnext/docs/user/manual/en/stock/articles/index.txt index df779de628a..acf1375214e 100644 --- a/erpnext/docs/user/manual/en/stock/articles/index.txt +++ b/erpnext/docs/user/manual/en/stock/articles/index.txt @@ -10,4 +10,8 @@ repack-entry serial-no-naming stock-entry-purpose stock-level-report -track-items-using-barcode \ No newline at end of file +track-items-using-barcode +stock-received-but-not-billed +return-rejected-item +item-valuation-transactions +delivery-note-stock-error \ No newline at end of file diff --git a/erpnext/docs/user/manual/en/stock/articles/item-valuation-transactions.md b/erpnext/docs/user/manual/en/stock/articles/item-valuation-transactions.md new file mode 100644 index 00000000000..5cc6529d89a --- /dev/null +++ b/erpnext/docs/user/manual/en/stock/articles/item-valuation-transactions.md @@ -0,0 +1,17 @@ +# Item Valuation Methods and Transactions + +In ERPNext, Item's stock valuation is updated on the creation of one of the following transaction. + +1. Purchase Receipt +2. Stock Entry of type Material Receipt +3. Stock Reconciliation made for updating stock opening balance + +You can select valuation method based on which item's value will be calculated. Valuation Method can be set globally for all the items from the Stock Settings. + +Download Backup + +You can also set Valuation Method in the item master, especially when a valuation method for an item is different from the default Method. + +Download Backup + +[Click here to learn about the valuation methods available in the ERPNext, and how it works.](https://frappe.io/blog/erpnext-features/inventory-valuation-method-fifo-vs-moving-average) diff --git a/erpnext/docs/user/manual/en/stock/articles/return-rejected-item.md b/erpnext/docs/user/manual/en/stock/articles/return-rejected-item.md new file mode 100644 index 00000000000..aaaec2dea0b --- /dev/null +++ b/erpnext/docs/user/manual/en/stock/articles/return-rejected-item.md @@ -0,0 +1,13 @@ +# Return Rejected Items + +In the Purchase Receipt, you can receive the Items in the Accepted or the Rejected Warehouse. + +If you are creating Purchase Return for the items received in the Rejected Warehouse, then create return entry following these steps. + +1. In the Purchase Receipt Item table, for the item to be returned, in the Received Qty field, enter return entry in negative. +2. In the Accepted Warehouse field, set value as zero. +3. In the Rejected Warehouse field, set the quantity to be returned in negative. + +For detailed steps on how to create Purchase Return Entry for the Rejected Item, refer to the below example. + +Returning Rejected Items \ No newline at end of file diff --git a/erpnext/docs/user/manual/en/stock/articles/stock-received-but-not-billed.md b/erpnext/docs/user/manual/en/stock/articles/stock-received-but-not-billed.md new file mode 100644 index 00000000000..25ef3b8aa68 --- /dev/null +++ b/erpnext/docs/user/manual/en/stock/articles/stock-received-but-not-billed.md @@ -0,0 +1,9 @@ +# Purpose of Stock Received but not Billed + +When purchased items are received, an accounts posting is done based on the value of the purchased items in the Stock-in-hand / fixed-assets account. When you sell and deliver those items, an expense (cost-of-goods-sold) is booked, equal to the buying cost of the items. + +As stock balance increases through Purchase Receipt, Warehouse account is debited and an adjustment account called **Stock Received But Not Billed** account is credited. At the same time, the negative expense is booked in account **Expense included in Valuation** for the amount added for valuation purpose, to avoid double expense booking. + +On receiving Bill from the supplier, you will make Purchase Invoice against a Purchase Receipt. Here **Stock Received But Not Billed** account is debited, hence nullifies the balance in the Stock Received but not Billed Account. + +The balance in the Stock Received but not Billed account indicates the value of items for which Purchase Receipt has been made, but billing is pending. \ No newline at end of file diff --git a/erpnext/docs/user/manual/en/subscription/__init__.py b/erpnext/docs/user/manual/en/subscription/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/docs/user/manual/en/subscription/index.md b/erpnext/docs/user/manual/en/subscription/index.md new file mode 100644 index 00000000000..24d75eda95d --- /dev/null +++ b/erpnext/docs/user/manual/en/subscription/index.md @@ -0,0 +1,22 @@ +If you have a contract with the Customer where your organization gives bill to the Customer on a monthly, quarterly, half-yearly or annual basis, you can use subscription feature to make auto invoicing. + +Subscription + +#### Scenario + +Subscription for your hosted ERPNext account requires yearly renewal. We use Sales Invoice for generating proforma invoices. To automate proforma invoicing for renewal, we set original Sales Invoice on the subscription form. Recurring proforma invoice is created automatically just before customer's account is about to expire, and requires renewal. This recurring Proforma Invoice is also emailed automatically to the customer. + +To set the subscription for the sales invoice +Goto Subscription > select base doctype "Sales Invoice" > select base docname "Invoice No" > Save + +Subscription + +**From Date and To Date**: This defines contract period with the customer. + +**Repeat on Day**: If frequency is set as Monthly, then it will be day of the month on which recurring invoice will be generated. + +**Notify By Email**: If you want to notify the user about auto recurring invoice. + +**Print Format**: Select a print format to define document view which should be emailed to customer. + +**Disabled**: It will stop to make auto recurring documents against the subscription \ No newline at end of file diff --git a/erpnext/docs/user/manual/en/subscription/index.txt b/erpnext/docs/user/manual/en/subscription/index.txt new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/docs/user/manual/en/using-erpnext/articles/index.txt b/erpnext/docs/user/manual/en/using-erpnext/articles/index.txt index 4ca21aa969b..9dc7df3f31f 100644 --- a/erpnext/docs/user/manual/en/using-erpnext/articles/index.txt +++ b/erpnext/docs/user/manual/en/using-erpnext/articles/index.txt @@ -8,4 +8,5 @@ bulk-rename renaming-documents search-filter tree-master-renaming -pos-view \ No newline at end of file +pos-view +letter-head-in-the-report \ No newline at end of file diff --git a/erpnext/docs/user/manual/en/using-erpnext/articles/letter-head-in-the-report.md b/erpnext/docs/user/manual/en/using-erpnext/articles/letter-head-in-the-report.md new file mode 100644 index 00000000000..4f8b2e9ea3e --- /dev/null +++ b/erpnext/docs/user/manual/en/using-erpnext/articles/letter-head-in-the-report.md @@ -0,0 +1,15 @@ +# Letter Head in the Report's Print Format + +In the reports, Letter Head is fetched from the Company master. To have company's Letter Head fetched correctly in the report, please ensure that you have updated default Letter Head in the Company master. + +`Explore > Accounts > Company` + +Company Letter + +In a Company master, if no Letter Head is set as default, then in the reports, Letter Head having Default field checked will be fetched. + +Default Letter Head + +If you are managing multiple companies in a single ERPNext account, then ensure that for each Company, default Letter Head is set in the Company master. + +After updating Letter Head in the Company master, refresh your ERPNext account, and then check the print format of a report. \ No newline at end of file diff --git a/erpnext/docs/user/manual/en/website/articles/website-banner.md b/erpnext/docs/user/manual/en/website/articles/website-banner.md new file mode 100644 index 00000000000..514e23e58de --- /dev/null +++ b/erpnext/docs/user/manual/en/website/articles/website-banner.md @@ -0,0 +1,9 @@ +# Website Banner Resizing + +Each ERPNext account website automatically generated from it. On a website, logo is set based on logo image selected in the Setup Wizard. You can change or edit property for your company's logo from the Website Settings. + +`Explore > Website > Website Settings` + +For the exact steps on how to upload a Website Banner and resize it, please refer to the help given below. + +Website Banner image \ No newline at end of file diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 7e65fc92623..b2c328552f6 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -27,6 +27,8 @@ doctype_js = { # setup wizard setup_wizard_requires = "assets/erpnext/js/setup_wizard.js" setup_wizard_complete = "erpnext.setup.setup_wizard.setup_wizard.setup_complete" +setup_wizard_success = "erpnext.setup.setup_wizard.setup_wizard.setup_success" +setup_wizard_test = "erpnext.setup.setup_wizard.test_setup_wizard.run_setup_wizard_test" before_install = "erpnext.setup.install.check_setup_wizard_not_completed" after_install = "erpnext.setup.install.after_install" @@ -34,6 +36,8 @@ after_install = "erpnext.setup.install.after_install" boot_session = "erpnext.startup.boot.boot_session" notification_config = "erpnext.startup.notifications.get_notification_config" get_help_messages = "erpnext.utilities.activation.get_help_messages" +get_user_progress_slides = "erpnext.utilities.user_progress.get_user_progress_slides" +update_and_get_user_progress = "erpnext.utilities.user_progress_utils.update_default_domain_actions_and_get_state" on_session_creation = "erpnext.shopping_cart.utils.set_cart_count" on_logout = "erpnext.shopping_cart.utils.clear_cart_count" @@ -180,7 +184,7 @@ doc_events = { scheduler_events = { "hourly": [ - "erpnext.controllers.recurring_document.create_recurring_documents", + "erpnext.subscription.doctype.subscription.subscription.make_subscription_entry", 'erpnext.hr.doctype.daily_work_summary_settings.daily_work_summary_settings.trigger_emails' ], "daily": [ diff --git a/erpnext/hr/doctype/employee/employee.py b/erpnext/hr/doctype/employee/employee.py index a078ef2033b..03626cdc5cf 100755 --- a/erpnext/hr/doctype/employee/employee.py +++ b/erpnext/hr/doctype/employee/employee.py @@ -289,3 +289,15 @@ def create_user(employee, user = None): }) user.insert() return user.name + +def get_employee_emails(employee_list): + '''Returns list of employee emails either based on user_id or company_email''' + employee_emails = [] + for employee in employee_list: + if not employee: + continue + user, email = frappe.db.get_value('Employee', employee, ['user_id', 'company_email']) + if user or email: + employee_emails.append(user or email) + + return employee_emails \ No newline at end of file diff --git a/erpnext/hr/doctype/training_event/test_training_event.js b/erpnext/hr/doctype/training_event/tests/test_training_event.js similarity index 100% rename from erpnext/hr/doctype/training_event/test_training_event.js rename to erpnext/hr/doctype/training_event/tests/test_training_event.js diff --git a/erpnext/hr/doctype/training_event/tests/test_training_event_attendance.js b/erpnext/hr/doctype/training_event/tests/test_training_event_attendance.js new file mode 100644 index 00000000000..6364308f731 --- /dev/null +++ b/erpnext/hr/doctype/training_event/tests/test_training_event_attendance.js @@ -0,0 +1,40 @@ +QUnit.module('hr'); + +QUnit.test("test: Training Event", function (assert) { + // number of asserts + assert.expect(1); + let done = assert.async(); + + frappe.run_serially([ + // insert a new Training Event + () => frappe.set_route("List", "Training Event", "List"), + () => frappe.new_doc("Training Event"), + () => frappe.timeout(1), + () => frappe.click_link('Edit in full page'), + () => cur_frm.set_value("event_name", "Test Event " + frappe.utils.get_random(10)), + () => cur_frm.set_value("start_time", "2017-07-26, 2:00 pm PDT"), + () => cur_frm.set_value("end_time", "2017-07-26, 2:30 pm PDT"), + () => cur_frm.set_value("introduction", "This is a test report"), + () => cur_frm.set_value("location", "Fake office"), + () => frappe.click_button('Add Row'), + () => frappe.db.get_value('Employee', {'employee_name':'Test Employee 1'}, 'name'), + (r) => { + console.log(r); + return cur_frm.fields_dict.employees.grid.grid_rows[0].doc.employee = r.message.name; + }, + () => { + return cur_frm.fields_dict.employees.grid.grid_rows[0].doc.attendance = "Optional"; + }, + () => frappe.click_button('Save'), + () => frappe.timeout(2), + () => frappe.click_button('Submit'), + () => frappe.timeout(2), + () => frappe.click_button('Yes'), + () => frappe.timeout(1), + () => { + assert.equal(cur_frm.doc.docstatus, 1); + }, + () => done() + ]); + +}); \ No newline at end of file diff --git a/erpnext/hr/doctype/training_event/training_event.js b/erpnext/hr/doctype/training_event/training_event.js index ebe0c7907ce..6a6e8fe0a6a 100644 --- a/erpnext/hr/doctype/training_event/training_event.js +++ b/erpnext/hr/doctype/training_event/training_event.js @@ -18,4 +18,4 @@ frappe.ui.form.on('Training Event', { }); } } -}); +}); \ No newline at end of file diff --git a/erpnext/hr/doctype/training_event/training_event.json b/erpnext/hr/doctype/training_event/training_event.json index 03b58b48021..7be9d974fe6 100644 --- a/erpnext/hr/doctype/training_event/training_event.json +++ b/erpnext/hr/doctype/training_event/training_event.json @@ -25,7 +25,7 @@ "ignore_xss_filter": 0, "in_filter": 0, "in_global_search": 0, - "in_list_view": 0, + "in_list_view": 1, "in_standard_filter": 0, "label": "Event Name", "length": 0, @@ -55,7 +55,7 @@ "ignore_xss_filter": 0, "in_filter": 0, "in_global_search": 0, - "in_list_view": 0, + "in_list_view": 1, "in_standard_filter": 1, "label": "Event Status", "length": 0, @@ -115,12 +115,12 @@ "ignore_xss_filter": 0, "in_filter": 0, "in_global_search": 0, - "in_list_view": 0, + "in_list_view": 1, "in_standard_filter": 1, "label": "Type", "length": 0, "no_copy": 0, - "options": "Seminar\nTheory\nWorkshop\nConference\nExam\nInternet", + "options": "Seminar\nTheory\nWorkshop\nConference\nExam\nInternet\nSelf-Study", "permlevel": 0, "precision": "", "print_hide": 0, @@ -386,7 +386,7 @@ "ignore_xss_filter": 0, "in_filter": 0, "in_global_search": 0, - "in_list_view": 0, + "in_list_view": 1, "in_standard_filter": 1, "label": "Location", "length": 0, @@ -581,37 +581,6 @@ "set_only_once": 0, "unique": 0 }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "Will send an email about the event to employees with status 'Open'", - "fieldname": "send_email", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Send Email", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, { "allow_bulk_edit": 0, "allow_on_submit": 1, @@ -672,6 +641,37 @@ "search_index": 0, "set_only_once": 0, "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "employee_emails", + "fieldtype": "Small Text", + "hidden": 1, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Employee Emails", + "length": 0, + "no_copy": 0, + "options": "Email", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 } ], "has_web_view": 0, @@ -684,7 +684,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2017-05-29 06:13:38.411039", + "modified": "2017-08-11 03:11:25.768563", "modified_by": "Administrator", "module": "HR", "name": "Training Event", diff --git a/erpnext/hr/doctype/training_event/training_event.py b/erpnext/hr/doctype/training_event/training_event.py index 27ae8cf5491..cc568414a03 100644 --- a/erpnext/hr/doctype/training_event/training_event.py +++ b/erpnext/hr/doctype/training_event/training_event.py @@ -3,24 +3,10 @@ # For license information, please see license.txt from __future__ import unicode_literals -import frappe -from frappe import _ from frappe.model.document import Document +from erpnext.hr.doctype.employee.employee import get_employee_emails class TrainingEvent(Document): - def on_update(self): - self.invite_employee() - - def on_update_after_submit(self): - self.invite_employee() - - def invite_employee(self): - if self.event_status == "Scheduled" and self.send_email: - subject = _("""You are invited for to attend {0} - {1} scheduled from {2} to {3} at {4}."""\ - .format(self.type, self.event_name, self.start_time, self.end_time, self.location)) - - for emp in self.employees: - if emp.status== "Open": - frappe.sendmail(frappe.db.get_value("Employee", emp.employee, "company_email"), \ - subject=subject, content= self.introduction) - emp.status= "Invited" \ No newline at end of file + def validate(self): + self.employee_emails = ', '.join(get_employee_emails([d.employee + for d in self.employees])) diff --git a/erpnext/hr/doctype/training_event_employee/training_event_employee.json b/erpnext/hr/doctype/training_event_employee/training_event_employee.json index 575d0e75822..a8a72b1a701 100644 --- a/erpnext/hr/doctype/training_event_employee/training_event_employee.json +++ b/erpnext/hr/doctype/training_event_employee/training_event_employee.json @@ -1,5 +1,6 @@ { "allow_copy": 0, + "allow_guest_to_view": 0, "allow_import": 0, "allow_rename": 0, "beta": 0, @@ -11,6 +12,7 @@ "editable_grid": 1, "fields": [ { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -21,6 +23,7 @@ "ignore_user_permissions": 0, "ignore_xss_filter": 0, "in_filter": 0, + "in_global_search": 0, "in_list_view": 1, "in_standard_filter": 0, "label": "Employee", @@ -40,6 +43,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -50,6 +54,7 @@ "ignore_user_permissions": 0, "ignore_xss_filter": 0, "in_filter": 0, + "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, "label": "Employee Name", @@ -69,6 +74,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, "collapsible": 0, @@ -79,6 +85,7 @@ "ignore_user_permissions": 0, "ignore_xss_filter": 0, "in_filter": 0, + "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, "length": 0, @@ -96,6 +103,7 @@ "unique": 0 }, { + "allow_bulk_edit": 0, "allow_on_submit": 1, "bold": 0, "collapsible": 0, @@ -107,12 +115,44 @@ "ignore_user_permissions": 0, "ignore_xss_filter": 0, "in_filter": 0, + "in_global_search": 0, "in_list_view": 1, "in_standard_filter": 0, "label": "Status", "length": 0, "no_copy": 1, - "options": "Open\nInvited\nConfirmed\nAttended\nWithdrawn", + "options": "Open\nInvited\nCompleted\nFeedback Submitted", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "attendance", + "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Attendance", + "length": 0, + "no_copy": 0, + "options": "Mandatory\nOptional", "permlevel": 0, "precision": "", "print_hide": 0, @@ -126,17 +166,17 @@ "unique": 0 } ], + "has_web_view": 0, "hide_heading": 0, "hide_toolbar": 0, "idx": 0, "image_view": 0, "in_create": 0, - "in_dialog": 0, "is_submittable": 0, "issingle": 0, "istable": 1, "max_attachments": 0, - "modified": "2016-12-14 11:43:40.996578", + "modified": "2017-08-11 03:36:22.738253", "modified_by": "Administrator", "module": "HR", "name": "Training Event Employee", @@ -146,7 +186,9 @@ "quick_entry": 1, "read_only": 0, "read_only_onload": 0, + "show_name_in_global_search": 0, "sort_field": "modified", "sort_order": "DESC", + "track_changes": 0, "track_seen": 0 } \ No newline at end of file diff --git a/erpnext/hr/doctype/training_feedback/training_feedback.py b/erpnext/hr/doctype/training_feedback/training_feedback.py index 2a0403bd53a..b7eae38ae48 100644 --- a/erpnext/hr/doctype/training_feedback/training_feedback.py +++ b/erpnext/hr/doctype/training_feedback/training_feedback.py @@ -5,6 +5,19 @@ from __future__ import unicode_literals import frappe from frappe.model.document import Document +from frappe import _ class TrainingFeedback(Document): - pass + def validate(self): + training_event = frappe.get_doc("Training Event", self.training_event) + if training_event.docstatus != 1: + frappe.throw(_('{0} must be submitted').format(_('Training Event'))) + + def on_submit(self): + training_event = frappe.get_doc("Training Event", self.training_event) + for e in training_event.employees: + if e.employee == self.employee: + training_event.status = 'Feedback Submitted' + break + + training_event.save() diff --git a/erpnext/hr/doctype/training_result/test_training_result.js b/erpnext/hr/doctype/training_result/test_training_result.js new file mode 100644 index 00000000000..cb1d7fb27a3 --- /dev/null +++ b/erpnext/hr/doctype/training_result/test_training_result.js @@ -0,0 +1,23 @@ +/* eslint-disable */ +// rename this file from _test_[name] to test_[name] to activate +// and remove above this line + +QUnit.test("test: Training Result", function (assert) { + let done = assert.async(); + + // number of asserts + assert.expect(1); + + frappe.run_serially([ + // insert a new Training Result + () => frappe.tests.make('Training Result', [ + // values to be set + {key: 'value'} + ]), + () => { + assert.equal(cur_frm.doc.key, 'value'); + }, + () => done() + ]); + +}); diff --git a/erpnext/hr/doctype/training_result/training_result.json b/erpnext/hr/doctype/training_result/training_result.json index e5fbb5fd426..41142b59e69 100644 --- a/erpnext/hr/doctype/training_result/training_result.json +++ b/erpnext/hr/doctype/training_result/training_result.json @@ -26,7 +26,7 @@ "ignore_xss_filter": 0, "in_filter": 0, "in_global_search": 0, - "in_list_view": 0, + "in_list_view": 1, "in_standard_filter": 0, "label": "Training Event", "length": 0, @@ -133,6 +133,37 @@ "search_index": 0, "set_only_once": 0, "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "employee_emails", + "fieldtype": "Small Text", + "hidden": 1, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Employee Emails", + "length": 0, + "no_copy": 0, + "options": "Email", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 } ], "has_web_view": 0, @@ -145,7 +176,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2017-06-15 08:16:01.566531", + "modified": "2017-08-11 03:53:21.283968", "modified_by": "Administrator", "module": "HR", "name": "Training Result", diff --git a/erpnext/hr/doctype/training_result/training_result.py b/erpnext/hr/doctype/training_result/training_result.py index 36c3cb93bc1..7cdc51f8010 100644 --- a/erpnext/hr/doctype/training_result/training_result.py +++ b/erpnext/hr/doctype/training_result/training_result.py @@ -6,19 +6,27 @@ from __future__ import unicode_literals import frappe from frappe import _ from frappe.model.document import Document +from erpnext.hr.doctype.employee.employee import get_employee_emails class TrainingResult(Document): + def validate(self): + training_event = frappe.get_doc("Training Event", self.training_event) + if training_event.docstatus != 1: + frappe.throw(_('{0} must be submitted').format(_('Training Event'))) + + self.employee_emails = ', '.join(get_employee_emails([d.employee + for d in self.employees])) + def on_submit(self): - self.send_result() - - def send_result(self): - for emp in self.employees: - message = "Thank You for attending {0}.".format(self.training_event) - if emp.grade: - message = message + "Your grade: {0}".format(emp.grade) - frappe.sendmail(frappe.db.get_value("Employee", emp.employee, "company_email"), \ - subject=_("{0} Results".format(self.training_event)), \ - content=message) + training_event = frappe.get_doc("Training Event", self.training_event) + training_event.status = 'Completed' + for e in self.employees: + for e1 in training_event.employees: + if e1.employee == e.employee: + e1.status = 'Completed' + break + + training_event.save() @frappe.whitelist() def get_employees(training_event): diff --git a/erpnext/hr/email_alert/__init__.py b/erpnext/hr/email_alert/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/hr/email_alert/training_feedback/__init__.py b/erpnext/hr/email_alert/training_feedback/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/hr/email_alert/training_feedback/training_feedback.html b/erpnext/hr/email_alert/training_feedback/training_feedback.html new file mode 100644 index 00000000000..fd8fef9e82c --- /dev/null +++ b/erpnext/hr/email_alert/training_feedback/training_feedback.html @@ -0,0 +1,6 @@ +

{{ _("Hello") }},

+ +

You attended training {{ frappe.utils.get_link_to_form( + "Training Event", doc.training_event) }}

+ +

{{ _("Please share your feedback to the training by clicking on 'Training Feedback' and then 'New'") }}

\ No newline at end of file diff --git a/erpnext/hr/email_alert/training_feedback/training_feedback.json b/erpnext/hr/email_alert/training_feedback/training_feedback.json new file mode 100644 index 00000000000..51f6cedfecd --- /dev/null +++ b/erpnext/hr/email_alert/training_feedback/training_feedback.json @@ -0,0 +1,24 @@ +{ + "attach_print": 0, + "creation": "2017-08-11 03:17:11.769210", + "days_in_advance": 0, + "docstatus": 0, + "doctype": "Email Alert", + "document_type": "Training Result", + "enabled": 1, + "event": "Submit", + "idx": 0, + "is_standard": 1, + "message": "

{{_(\"Training Event\")}}

\n

{{ message }}

\n\n

{{_(\"Details\")}}

\n{{_(\"Event Name\")}}: {{ name }}\n
{{_(\"Event Location\")}}: {{ location }}\n
{{_(\"Start Time\")}}: {{ start_time }}\n
{{_(\"End Time\")}}: {{ end_time }}\n
{{_(\"Attendance\")}}: {{ attendance }}\n", + "modified": "2017-08-11 04:26:58.194793", + "modified_by": "Administrator", + "module": "HR", + "name": "Training Feedback", + "owner": "Administrator", + "recipients": [ + { + "email_by_document_field": "employee_emails" + } + ], + "subject": "Please Share your Feedback For {{ doc.training_event }}" +} \ No newline at end of file diff --git a/erpnext/hr/email_alert/training_feedback/training_feedback.md b/erpnext/hr/email_alert/training_feedback/training_feedback.md new file mode 100644 index 00000000000..bcadf7df590 --- /dev/null +++ b/erpnext/hr/email_alert/training_feedback/training_feedback.md @@ -0,0 +1,9 @@ +

{{_("Training Event")}}

+

{{ message }}

+ +

{{_("Details")}}

+{{_("Event Name")}}: {{ name }} +
{{_("Event Location")}}: {{ location }} +
{{_("Start Time")}}: {{ start_time }} +
{{_("End Time")}}: {{ end_time }} +
{{_("Attendance")}}: {{ attendance }} diff --git a/erpnext/hr/email_alert/training_feedback/training_feedback.py b/erpnext/hr/email_alert/training_feedback/training_feedback.py new file mode 100644 index 00000000000..2334f8b26d8 --- /dev/null +++ b/erpnext/hr/email_alert/training_feedback/training_feedback.py @@ -0,0 +1,7 @@ +from __future__ import unicode_literals + +import frappe + +def get_context(context): + # do your magic here + pass diff --git a/erpnext/hr/email_alert/training_scheduled/__init__.py b/erpnext/hr/email_alert/training_scheduled/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/hr/email_alert/training_scheduled/training_scheduled.html b/erpnext/hr/email_alert/training_scheduled/training_scheduled.html new file mode 100644 index 00000000000..b1aeb2c8739 --- /dev/null +++ b/erpnext/hr/email_alert/training_scheduled/training_scheduled.html @@ -0,0 +1,9 @@ +

{{_("Training Event")}}

+ +

{{ doc.introduction }}

+ +

{{_("Details")}}

+{{_("Event Name")}}: {{ frappe.utils.get_link_to_form(doc.doctype, doc.name) }} +
{{_("Event Location")}}: {{ doc.location }} +
{{_("Start Time")}}: {{ doc.start_time }} +
{{_("End Time")}}: {{ doc.end_time }} diff --git a/erpnext/hr/email_alert/training_scheduled/training_scheduled.json b/erpnext/hr/email_alert/training_scheduled/training_scheduled.json new file mode 100644 index 00000000000..e1631f86839 --- /dev/null +++ b/erpnext/hr/email_alert/training_scheduled/training_scheduled.json @@ -0,0 +1,24 @@ +{ + "attach_print": 0, + "creation": "2017-08-11 03:13:40.519614", + "days_in_advance": 0, + "docstatus": 0, + "doctype": "Email Alert", + "document_type": "Training Event", + "enabled": 1, + "event": "Submit", + "idx": 0, + "is_standard": 1, + "message": "

{{_(\"Training Event\")}}

\n\n

{{ doc.introduction }}

\n\n

{{_(\"Details\")}}

\n{{_(\"Event Name\")}}: {{ frappe.utils.get_link_to_form(doc.doctype, doc.name) }}\n
{{_(\"Event Location\")}}: {{ doc.location }}\n
{{_(\"Start Time\")}}: {{ doc.start_time }}\n
{{_(\"End Time\")}}: {{ doc.end_time }}\n", + "modified": "2017-08-13 22:49:42.338881", + "modified_by": "Administrator", + "module": "HR", + "name": "Training Scheduled", + "owner": "Administrator", + "recipients": [ + { + "email_by_document_field": "employee_emails" + } + ], + "subject": "Training Scheduled: {{ doc.name }}" +} \ No newline at end of file diff --git a/erpnext/hr/email_alert/training_scheduled/training_scheduled.md b/erpnext/hr/email_alert/training_scheduled/training_scheduled.md new file mode 100644 index 00000000000..bcadf7df590 --- /dev/null +++ b/erpnext/hr/email_alert/training_scheduled/training_scheduled.md @@ -0,0 +1,9 @@ +

{{_("Training Event")}}

+

{{ message }}

+ +

{{_("Details")}}

+{{_("Event Name")}}: {{ name }} +
{{_("Event Location")}}: {{ location }} +
{{_("Start Time")}}: {{ start_time }} +
{{_("End Time")}}: {{ end_time }} +
{{_("Attendance")}}: {{ attendance }} diff --git a/erpnext/hr/email_alert/training_scheduled/training_scheduled.py b/erpnext/hr/email_alert/training_scheduled/training_scheduled.py new file mode 100644 index 00000000000..2334f8b26d8 --- /dev/null +++ b/erpnext/hr/email_alert/training_scheduled/training_scheduled.py @@ -0,0 +1,7 @@ +from __future__ import unicode_literals + +import frappe + +def get_context(context): + # do your magic here + pass diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js index c58c89cd5d4..32c357f25c3 100644 --- a/erpnext/manufacturing/doctype/bom/bom.js +++ b/erpnext/manufacturing/doctype/bom/bom.js @@ -5,7 +5,10 @@ frappe.provide("erpnext.bom"); frappe.ui.form.on("BOM", { setup: function(frm) { - frm.add_fetch('buying_price_list', 'currency', 'currency'); + frm.add_fetch("item", "description", "description"); + frm.add_fetch("item", "image", "image"); + frm.add_fetch("item", "item_name", "item_name"); + frm.add_fetch("item", "stock_uom", "uom"); frm.set_query("bom_no", "items", function() { return { @@ -23,6 +26,38 @@ frappe.ui.form.on("BOM", { } }; }); + + frm.set_query("item", function() { + return { + query: "erpnext.controllers.queries.item_query" + }; + }); + + frm.set_query("project", function() { + return{ + filters:[ + ['Project', 'status', 'not in', 'Completed, Cancelled'] + ] + }; + }); + + frm.set_query("item_code", "items", function() { + return { + query: "erpnext.controllers.queries.item_query", + filters: [["Item", "name", "!=", cur_frm.doc.item]] + }; + }); + + frm.set_query("bom_no", "items", function(doc, cdt, cdn) { + var d = locals[cdt][cdn]; + return { + filters: { + 'item': d.item_code, + 'is_active': 1, + 'docstatus': 1 + } + }; + }); }, onload_post_render: function(frm) { @@ -69,14 +104,14 @@ frappe.ui.form.on("BOM", { }); erpnext.bom.BomController = erpnext.TransactionController.extend({ - conversion_rate: function(doc, cdt, cdn) { + conversion_rate: function(doc) { if(this.frm.doc.currency === this.get_company_currency()) { this.frm.set_value("conversion_rate", 1.0); } else { erpnext.bom.update_cost(doc); } }, - + item_code: function(doc, cdt, cdn){ var scrap_items = false; var child = locals[cdt][cdn]; @@ -90,39 +125,34 @@ erpnext.bom.BomController = erpnext.TransactionController.extend({ get_bom_material_detail(doc, cdt, cdn, scrap_items); }, - conversion_factor: function(doc, cdt, cdn, dont_fetch_price_list_rate) { + conversion_factor: function(doc, cdt, cdn) { if(frappe.meta.get_docfield(cdt, "stock_qty", cdn)) { var item = frappe.get_doc(cdt, cdn); frappe.model.round_floats_in(item, ["qty", "conversion_factor"]); item.stock_qty = flt(item.qty * item.conversion_factor, precision("stock_qty", item)); refresh_field("stock_qty", item.name, item.parentfield); this.toggle_conversion_factor(item); + this.frm.events.update_cost(this.frm); } }, -}) +}); $.extend(cur_frm.cscript, new erpnext.bom.BomController({frm: cur_frm})); -cur_frm.add_fetch("item", "description", "description"); -cur_frm.add_fetch("item", "image", "image"); -cur_frm.add_fetch("item", "item_name", "item_name"); -cur_frm.add_fetch("item", "stock_uom", "uom"); - - -cur_frm.cscript.hour_rate = function(doc, dt, dn) { +cur_frm.cscript.hour_rate = function(doc) { erpnext.bom.calculate_op_cost(doc); erpnext.bom.calculate_total(doc); -} +}; cur_frm.cscript.time_in_mins = cur_frm.cscript.hour_rate; cur_frm.cscript.bom_no = function(doc, cdt, cdn) { get_bom_material_detail(doc, cdt, cdn, false); -} +}; cur_frm.cscript.is_default = function(doc) { if (doc.is_default) cur_frm.set_value("is_active", 1); -} +}; var get_bom_material_detail= function(doc, cdt, cdn, scrap_items) { var d = locals[cdt][cdn]; @@ -141,6 +171,7 @@ var get_bom_material_detail= function(doc, cdt, cdn, scrap_items) { $.extend(d, r.message); refresh_field("items"); refresh_field("scrap_items"); + doc = locals[doc.doctype][doc.name]; erpnext.bom.calculate_rm_cost(doc); erpnext.bom.calculate_scrap_materials_cost(doc); @@ -149,13 +180,13 @@ var get_bom_material_detail= function(doc, cdt, cdn, scrap_items) { freeze: true }); } -} +}; -cur_frm.cscript.qty = function(doc, cdt, cdn) { +cur_frm.cscript.qty = function(doc) { erpnext.bom.calculate_rm_cost(doc); erpnext.bom.calculate_scrap_materials_cost(doc); erpnext.bom.calculate_total(doc); -} +}; cur_frm.cscript.rate = function(doc, cdt, cdn) { var d = locals[cdt][cdn]; @@ -173,14 +204,14 @@ cur_frm.cscript.rate = function(doc, cdt, cdn) { erpnext.bom.calculate_scrap_materials_cost(doc); erpnext.bom.calculate_total(doc); } -} +}; erpnext.bom.update_cost = function(doc) { erpnext.bom.calculate_op_cost(doc); erpnext.bom.calculate_rm_cost(doc); erpnext.bom.calculate_scrap_materials_cost(doc); erpnext.bom.calculate_total(doc); -} +}; erpnext.bom.calculate_op_cost = function(doc) { var op = doc.operations || []; @@ -189,7 +220,7 @@ erpnext.bom.calculate_op_cost = function(doc) { for(var i=0;i= %s + """.format(doctype), today(), as_dict=1) + +def make_subscription(doctype, data): + doc = frappe.get_doc({ + 'doctype': 'Subscription', + 'reference_doctype': doctype, + 'reference_document': data.name, + 'start_date': data.from_date, + 'end_date': data.end_date, + 'frequency': data.recurring_type, + 'repeat_on_day': data.repeat_on_day_of_month, + 'notify_by_email': data.notify_by_email, + 'recipients': data.notification_email_address, + 'next_schedule_date': data.next_date, + 'submit_on_creation': data.submit_on_creation + }).insert(ignore_permissions=True) + + doc.submit() + + if not doc.subscription: + frappe.db.set_value(doctype, data.name, "subscription", doc.name) \ No newline at end of file diff --git a/erpnext/patches/v8_7/set_offline_in_pos_settings.py b/erpnext/patches/v8_7/set_offline_in_pos_settings.py new file mode 100644 index 00000000000..64a3a7c8065 --- /dev/null +++ b/erpnext/patches/v8_7/set_offline_in_pos_settings.py @@ -0,0 +1,12 @@ +# Copyright (c) 2017, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals +import frappe + +def execute(): + frappe.reload_doc('accounts', 'doctype', 'pos_settings') + + doc = frappe.get_doc('POS Settings') + doc.is_online = 0 + doc.save() \ No newline at end of file diff --git a/erpnext/patches/v8_8/__init__.py b/erpnext/patches/v8_8/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/patches/v8_8/set_bom_rate_as_per_uom.py b/erpnext/patches/v8_8/set_bom_rate_as_per_uom.py new file mode 100644 index 00000000000..5b169cdff2b --- /dev/null +++ b/erpnext/patches/v8_8/set_bom_rate_as_per_uom.py @@ -0,0 +1,13 @@ +# Copyright (c) 2017, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals +import frappe + +def execute(): + frappe.db.sql(""" + update `tabBOM Item` + set rate = rate * conversion_factor + where uom != stock_uom and docstatus < 2 + and conversion_factor not in (0, 1) + """) \ No newline at end of file diff --git a/erpnext/patches/v8_9/__init__.py b/erpnext/patches/v8_9/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/patches/v8_9/add_setup_progress_actions.py b/erpnext/patches/v8_9/add_setup_progress_actions.py new file mode 100644 index 00000000000..25698cc1675 --- /dev/null +++ b/erpnext/patches/v8_9/add_setup_progress_actions.py @@ -0,0 +1,37 @@ + +from __future__ import unicode_literals +import frappe +from frappe import _ + +def execute(): + """Add setup progress actions""" + frappe.reload_doc("setup", "doctype", "setup_progress") + frappe.reload_doc("setup", "doctype", "setup_progress_action") + + actions = [ + {"action_name": _("Add Company"), "action_doctype": "Company", "min_doc_count": 1, "is_completed": 1, + "domains": '[]' }, + {"action_name": _("Add Customers"), "action_doctype": "Customer", "min_doc_count": 1, "is_completed": 0, + "domains": '["Manufacturing", "Services", "Retail", "Distribution"]' }, + {"action_name": _("Add Suppliers"), "action_doctype": "Supplier", "min_doc_count": 1, "is_completed": 0, + "domains": '["Manufacturing", "Services", "Retail", "Distribution"]' }, + {"action_name": _("Add Products"), "action_doctype": "Item", "min_doc_count": 1, "is_completed": 0, + "domains": '["Manufacturing", "Services", "Retail", "Distribution"]' }, + {"action_name": _("Add Programs"), "action_doctype": "Program", "min_doc_count": 1, "is_completed": 0, + "domains": '["Education"]' }, + {"action_name": _("Add Instructors"), "action_doctype": "Instructor", "min_doc_count": 1, "is_completed": 0, + "domains": '["Education"]' }, + {"action_name": _("Add Courses"), "action_doctype": "Course", "min_doc_count": 1, "is_completed": 0, + "domains": '["Education"]' }, + {"action_name": _("Add Rooms"), "action_doctype": "Room", "min_doc_count": 1, "is_completed": 0, + "domains": '["Education"]' }, + {"action_name": _("Add Users"), "action_doctype": "User", "min_doc_count": 4, "is_completed": 0, + "domains": '[]' } + ] + + setup_progress = frappe.get_doc("Setup Progress", "Setup Progress") + for action in actions: + setup_progress.append("actions", action) + + setup_progress.save(ignore_permissions=True) + diff --git a/erpnext/patches/v8_9/rename_company_sales_target_field.py b/erpnext/patches/v8_9/rename_company_sales_target_field.py new file mode 100644 index 00000000000..5433eb673e4 --- /dev/null +++ b/erpnext/patches/v8_9/rename_company_sales_target_field.py @@ -0,0 +1,8 @@ +from __future__ import unicode_literals +import frappe +from frappe.model.utils.rename_field import rename_field + +def execute(): + frappe.reload_doc("setup", "doctype", "company") + if frappe.db.has_column('Company', 'sales_target'): + rename_field("Company", "sales_target", "monthly_sales_target") diff --git a/erpnext/projects/doctype/timesheet/test_timesheet.py b/erpnext/projects/doctype/timesheet/test_timesheet.py index 955a2b0fe34..2588d566d56 100644 --- a/erpnext/projects/doctype/timesheet/test_timesheet.py +++ b/erpnext/projects/doctype/timesheet/test_timesheet.py @@ -54,19 +54,18 @@ class TestTimesheet(unittest.TestCase): def test_sales_invoice_from_timesheet(self): timesheet = make_timesheet("_T-Employee-0001", simulate=True, billable=1) - sales_invoice = make_sales_invoice(timesheet.name) - sales_invoice.customer = "_Test Customer" + sales_invoice = make_sales_invoice(timesheet.name, '_Test Item', '_Test Customer') sales_invoice.due_date = nowdate() - - item = sales_invoice.append('items', {}) - item.item_code = '_Test Item' - item.qty = 2 - item.rate = 100 - sales_invoice.submit() timesheet = frappe.get_doc('Timesheet', timesheet.name) self.assertEquals(sales_invoice.total_billing_amount, 100) self.assertEquals(timesheet.status, 'Billed') + self.assertEquals(sales_invoice.customer, '_Test Customer') + + item = sales_invoice.items[0] + self.assertEquals(item.item_code, '_Test Item') + self.assertEquals(item.qty, 2.00) + self.assertEquals(item.rate, 50.00) def test_timesheet_billing_based_on_project(self): timesheet = make_timesheet("_T-Employee-0001", simulate=True, billable=1, project = '_Test Project', company='_Test Company') diff --git a/erpnext/projects/doctype/timesheet/timesheet.js b/erpnext/projects/doctype/timesheet/timesheet.js index 9b330e7811b..62ae1ed62b6 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.js +++ b/erpnext/projects/doctype/timesheet/timesheet.js @@ -57,10 +57,37 @@ frappe.ui.form.on("Timesheet", { }, make_invoice: function(frm) { - frappe.model.open_mapped_doc({ - method: "erpnext.projects.doctype.timesheet.timesheet.make_sales_invoice", - frm: frm + let dialog = new frappe.ui.Dialog({ + title: __("For Item"), + fields: [ + {"fieldtype": "Link", "label": __("Item Code"), "fieldname": "item_code", "options":"Item"}, + {"fieldtype": "Link", "label": __("Customer"), "fieldname": "customer", "options":"Customer"} + ] }); + + dialog.set_primary_action(__("Make Sales Invoice"), () => { + var args = dialog.get_values(); + if(!args) return; + dialog.hide(); + return frappe.call({ + type: "GET", + method: "erpnext.projects.doctype.timesheet.timesheet.make_sales_invoice", + args: { + "source_name": frm.doc.name, + "item_code": args.item_code, + "customer": args.customer + }, + freeze: true, + callback: function(r) { + if(!r.exc) { + frappe.model.sync(r.message); + frappe.set_route("Form", r.message.doctype, r.message.name); + } + } + }) + }) + + dialog.show(); }, make_salary_slip: function(frm) { diff --git a/erpnext/projects/doctype/timesheet/timesheet.py b/erpnext/projects/doctype/timesheet/timesheet.py index 16abd2499cc..ad566d5ac11 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.py +++ b/erpnext/projects/doctype/timesheet/timesheet.py @@ -323,17 +323,32 @@ def get_timesheet_data(name, project): } @frappe.whitelist() -def make_sales_invoice(source_name, target=None): +def make_sales_invoice(source_name, item_code=None, customer=None): target = frappe.new_doc("Sales Invoice") timesheet = frappe.get_doc('Timesheet', source_name) + hours = flt(timesheet.total_billable_hours) - flt(timesheet.total_billed_hours) + billing_amount = flt(timesheet.total_billable_amount) - flt(timesheet.total_billed_amount) + billing_rate = billing_amount / hours + + if customer: + target.customer = customer + + if item_code: + target.append('items', { + 'item_code': item_code, + 'qty': hours, + 'rate': billing_rate + }) + target.append('timesheets', { 'time_sheet': timesheet.name, - 'billing_hours': flt(timesheet.total_billable_hours) - flt(timesheet.total_billed_hours), - 'billing_amount': flt(timesheet.total_billable_amount) - flt(timesheet.total_billed_amount) + 'billing_hours': hours, + 'billing_amount': billing_amount }) target.run_method("calculate_billing_amount_for_timesheet") + target.run_method("set_missing_values") return target diff --git a/erpnext/public/css/erpnext.css b/erpnext/public/css/erpnext.css index 0660b392086..13fdcf15010 100644 --- a/erpnext/public/css/erpnext.css +++ b/erpnext/public/css/erpnext.css @@ -308,16 +308,6 @@ body[data-route="pos"] .item-list .image-field .placeholder-text { body[data-route="pos"] .item-list .pos-item-wrapper { position: relative; } -body[data-route="pos"] .item-list .price-info { - position: absolute; - left: 0; - bottom: 0; - margin: 0 0 15px 15px; - background-color: rgba(141, 153, 166, 0.6); - padding: 5px 9px; - border-radius: 3px; - color: #fff; -} body[data-route="pos"] .pos-bill-toolbar { margin-top: 10px; } @@ -356,3 +346,13 @@ body[data-route="pos"] .btn-more { body[data-route="pos"] .collapse-btn { cursor: pointer; } +.price-info { + position: absolute; + left: 0; + bottom: 0; + margin: 0 0 15px 15px; + background-color: rgba(141, 153, 166, 0.6); + padding: 5px 9px; + border-radius: 3px; + color: #fff; +} diff --git a/erpnext/public/css/pos.css b/erpnext/public/css/pos.css new file mode 100644 index 00000000000..f66abc80816 --- /dev/null +++ b/erpnext/public/css/pos.css @@ -0,0 +1,174 @@ +[data-route="point-of-sale"] .layout-main-section-wrapper { + margin-bottom: 0; +} +[data-route="point-of-sale"] .pos-items-wrapper { + max-height: calc(100vh - 210px); +} +.pos { + padding: 15px; +} +.list-item { + min-height: 40px; + height: auto; +} +.cart-container { + padding: 0 15px; + display: inline-block; + width: 39%; + vertical-align: top; +} +.item-container { + padding: 0 15px; + display: inline-block; + width: 60%; + vertical-align: top; +} +.search-field { + width: 60%; +} +.search-field input::placeholder { + font-size: 12px; +} +.item-group-field { + width: 40%; + margin-left: 15px; +} +.cart-wrapper { + margin-bottom: 10px; +} +.cart-wrapper .list-item__content:not(:first-child) { + justify-content: flex-end; +} +.cart-wrapper .list-item--head .list-item__content:nth-child(2) { + flex: 1.5; +} +.cart-items { + height: 150px; + overflow: auto; +} +.cart-items .list-item.current-item { + background-color: #fffce7; +} +.cart-items .list-item.current-item.qty input { + border: 1px solid #5E64FF; + font-weight: bold; +} +.cart-items .list-item.current-item.disc .discount { + font-weight: bold; +} +.cart-items .list-item.current-item.rate .rate { + font-weight: bold; +} +.cart-items .list-item .quantity { + flex: 1.5; +} +.cart-items input { + text-align: right; + height: 22px; + font-size: 12px; +} +.fields { + display: flex; +} +.pos-items-wrapper { + max-height: 480px; + overflow-y: auto; +} +.pos-items { + overflow: hidden; +} +.pos-item-wrapper { + display: flex; + flex-direction: column; + position: relative; + width: 25%; +} +.image-view-container { + display: block; +} +.image-view-container .image-field { + height: auto; +} +.empty-state { + height: 100%; + position: relative; +} +.empty-state span { + position: absolute; + color: #8D99A6; + font-size: 12px; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} +@keyframes yellow-fade { + 0% { + background-color: #fffce7; + } + 100% { + background-color: transparent; + } +} +.highlight { + animation: yellow-fade 1s ease-in 1; +} +input[type=number]::-webkit-inner-spin-button, +input[type=number]::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; +} +.number-pad { + border-collapse: collapse; + cursor: pointer; + display: table; + margin: auto; +} +.num-row { + display: table-row; +} +.num-col { + display: table-cell; + border: 1px solid #d1d8dd; +} +.num-col > div { + width: 50px; + height: 50px; + text-align: center; + line-height: 50px; +} +.num-col.active { + background-color: #fffce7; +} +.num-col.brand-primary { + background-color: #5E64FF; + color: #ffffff; +} +.discount-amount .discount-inputs { + display: flex; + flex-direction: column; + padding: 15px 0; +} +.discount-amount input:first-child { + margin-bottom: 10px; +} +.taxes-and-totals { + border-top: 1px solid #d1d8dd; +} +.taxes-and-totals .taxes { + display: flex; + flex-direction: column; + padding: 15px 0; + align-items: flex-end; +} +.taxes-and-totals .taxes > div:first-child { + margin-bottom: 10px; +} +.grand-total { + border-top: 1px solid #d1d8dd; +} +.grand-total .list-item { + height: 60px; +} +.grand-total .grand-total-value { + font-size: 24px; +} diff --git a/erpnext/public/images/illustrations/shop.jpg b/erpnext/public/images/illustrations/shop.jpg new file mode 100644 index 00000000000..f92f7dbd4eb Binary files /dev/null and b/erpnext/public/images/illustrations/shop.jpg differ diff --git a/erpnext/public/images/illustrations/shop2.jpg b/erpnext/public/images/illustrations/shop2.jpg new file mode 100644 index 00000000000..62e464956ba Binary files /dev/null and b/erpnext/public/images/illustrations/shop2.jpg differ diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 3a8ddb59278..f5bcf1ce41c 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -4,6 +4,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ setup: function() { this._super(); + frappe.flags.hide_serial_batch_dialog = false; frappe.ui.form.on(this.frm.doctype + " Item", "rate", function(frm, cdt, cdn) { var item = frappe.get_doc(cdt, cdn); var has_margin_field = frappe.meta.has_field(cdt, 'margin_type'); @@ -314,12 +315,15 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ if(!r.exc) { me.frm.script_manager.trigger("price_list_rate", cdt, cdn); me.toggle_conversion_factor(item); - if(show_batch_dialog) { + if(show_batch_dialog && !frappe.flags.hide_serial_batch_dialog) { var d = locals[cdt][cdn]; $.each(r.message, function(k, v) { if(!d[k]) d[k] = v; }); - erpnext.show_serial_batch_selector(me.frm, d); + + erpnext.show_serial_batch_selector(me.frm, d, (item) => { + me.frm.script_manager.trigger('qty', item.doctype, item.name); + }); } } } @@ -527,12 +531,23 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ if(this.frm.doc.ignore_pricing_rule) { this.calculate_taxes_and_totals(); } else if (!this.in_apply_price_list){ + this.set_actual_charges_based_on_currency(); this.apply_price_list(); } } }, + set_actual_charges_based_on_currency: function() { + var me = this; + $.each(this.frm.doc.taxes || [], function(i, d) { + if(d.charge_type == "Actual") { + frappe.model.set_value(d.doctype, d.name, "tax_amount", + flt(d.tax_amount) / flt(me.frm.doc.conversion_rate)); + } + }); + }, + get_exchange_rate: function(transaction_date, from_currency, to_currency, callback) { if (!transaction_date || !from_currency || !to_currency) return; return frappe.call({ @@ -1102,7 +1117,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ }, }); -erpnext.show_serial_batch_selector = function(frm, d) { +erpnext.show_serial_batch_selector = function(frm, d, callback, show_dialog) { frappe.require("assets/erpnext/js/utils/serial_no_batch_selector.js", function() { new erpnext.SerialNoBatchSelector({ frm: frm, @@ -1111,6 +1126,7 @@ erpnext.show_serial_batch_selector = function(frm, d) { type: "Warehouse", name: d.warehouse }, - }); + callback: callback + }, show_dialog); }); } diff --git a/erpnext/public/js/pos/clusterize.js b/erpnext/public/js/pos/clusterize.js new file mode 100644 index 00000000000..075c9ca4ae6 --- /dev/null +++ b/erpnext/public/js/pos/clusterize.js @@ -0,0 +1,330 @@ +/* eslint-disable */ +/*! Clusterize.js - v0.17.6 - 2017-03-05 +* http://NeXTs.github.com/Clusterize.js/ +* Copyright (c) 2015 Denis Lukov; Licensed GPLv3 */ + +;(function(name, definition) { + if (typeof module != 'undefined') module.exports = definition(); + else if (typeof define == 'function' && typeof define.amd == 'object') define(definition); + else this[name] = definition(); +}('Clusterize', function() { + "use strict" + + // detect ie9 and lower + // https://gist.github.com/padolsey/527683#comment-786682 + var ie = (function(){ + for( var v = 3, + el = document.createElement('b'), + all = el.all || []; + el.innerHTML = '', + all[0]; + ){} + return v > 4 ? v : document.documentMode; + }()), + is_mac = navigator.platform.toLowerCase().indexOf('mac') + 1; + var Clusterize = function(data) { + if( ! (this instanceof Clusterize)) + return new Clusterize(data); + var self = this; + + var defaults = { + rows_in_block: 50, + blocks_in_cluster: 4, + tag: null, + show_no_data_row: true, + no_data_class: 'clusterize-no-data', + no_data_text: 'No data', + keep_parity: true, + callbacks: {} + } + + // public parameters + self.options = {}; + var options = ['rows_in_block', 'blocks_in_cluster', 'show_no_data_row', 'no_data_class', 'no_data_text', 'keep_parity', 'tag', 'callbacks']; + for(var i = 0, option; option = options[i]; i++) { + self.options[option] = typeof data[option] != 'undefined' && data[option] != null + ? data[option] + : defaults[option]; + } + + var elems = ['scroll', 'content']; + for(var i = 0, elem; elem = elems[i]; i++) { + self[elem + '_elem'] = data[elem + 'Id'] + ? document.getElementById(data[elem + 'Id']) + : data[elem + 'Elem']; + if( ! self[elem + '_elem']) + throw new Error("Error! Could not find " + elem + " element"); + } + + // tabindex forces the browser to keep focus on the scrolling list, fixes #11 + if( ! self.content_elem.hasAttribute('tabindex')) + self.content_elem.setAttribute('tabindex', 0); + + // private parameters + var rows = isArray(data.rows) + ? data.rows + : self.fetchMarkup(), + cache = {}, + scroll_top = self.scroll_elem.scrollTop; + + // append initial data + self.insertToDOM(rows, cache); + + // restore the scroll position + self.scroll_elem.scrollTop = scroll_top; + + // adding scroll handler + var last_cluster = false, + scroll_debounce = 0, + pointer_events_set = false, + scrollEv = function() { + // fixes scrolling issue on Mac #3 + if (is_mac) { + if( ! pointer_events_set) self.content_elem.style.pointerEvents = 'none'; + pointer_events_set = true; + clearTimeout(scroll_debounce); + scroll_debounce = setTimeout(function () { + self.content_elem.style.pointerEvents = 'auto'; + pointer_events_set = false; + }, 50); + } + if (last_cluster != (last_cluster = self.getClusterNum())) + self.insertToDOM(rows, cache); + if (self.options.callbacks.scrollingProgress) + self.options.callbacks.scrollingProgress(self.getScrollProgress()); + }, + resize_debounce = 0, + resizeEv = function() { + clearTimeout(resize_debounce); + resize_debounce = setTimeout(self.refresh, 100); + } + on('scroll', self.scroll_elem, scrollEv); + on('resize', window, resizeEv); + + // public methods + self.destroy = function(clean) { + off('scroll', self.scroll_elem, scrollEv); + off('resize', window, resizeEv); + self.html((clean ? self.generateEmptyRow() : rows).join('')); + } + self.refresh = function(force) { + if(self.getRowsHeight(rows) || force) self.update(rows); + } + self.update = function(new_rows) { + rows = isArray(new_rows) + ? new_rows + : []; + var scroll_top = self.scroll_elem.scrollTop; + // fixes #39 + if(rows.length * self.options.item_height < scroll_top) { + self.scroll_elem.scrollTop = 0; + last_cluster = 0; + } + self.insertToDOM(rows, cache); + self.scroll_elem.scrollTop = scroll_top; + } + self.clear = function() { + self.update([]); + } + self.getRowsAmount = function() { + return rows.length; + } + self.getScrollProgress = function() { + return this.options.scroll_top / (rows.length * this.options.item_height) * 100 || 0; + } + + var add = function(where, _new_rows) { + var new_rows = isArray(_new_rows) + ? _new_rows + : []; + if( ! new_rows.length) return; + rows = where == 'append' + ? rows.concat(new_rows) + : new_rows.concat(rows); + self.insertToDOM(rows, cache); + } + self.append = function(rows) { + add('append', rows); + } + self.prepend = function(rows) { + add('prepend', rows); + } + } + + Clusterize.prototype = { + constructor: Clusterize, + // fetch existing markup + fetchMarkup: function() { + var rows = [], rows_nodes = this.getChildNodes(this.content_elem); + while (rows_nodes.length) { + rows.push(rows_nodes.shift().outerHTML); + } + return rows; + }, + // get tag name, content tag name, tag height, calc cluster height + exploreEnvironment: function(rows, cache) { + var opts = this.options; + opts.content_tag = this.content_elem.tagName.toLowerCase(); + if( ! rows.length) return; + if(ie && ie <= 9 && ! opts.tag) opts.tag = rows[0].match(/<([^>\s/]*)/)[1].toLowerCase(); + if(this.content_elem.children.length <= 1) cache.data = this.html(rows[0] + rows[0] + rows[0]); + if( ! opts.tag) opts.tag = this.content_elem.children[0].tagName.toLowerCase(); + this.getRowsHeight(rows); + }, + getRowsHeight: function(rows) { + var opts = this.options, + prev_item_height = opts.item_height; + opts.cluster_height = 0; + if( ! rows.length) return; + var nodes = this.content_elem.children; + var node = nodes[Math.floor(nodes.length / 2)]; + opts.item_height = node.offsetHeight; + // consider table's border-spacing + if(opts.tag == 'tr' && getStyle('borderCollapse', this.content_elem) != 'collapse') + opts.item_height += parseInt(getStyle('borderSpacing', this.content_elem), 10) || 0; + // consider margins (and margins collapsing) + if(opts.tag != 'tr') { + var marginTop = parseInt(getStyle('marginTop', node), 10) || 0; + var marginBottom = parseInt(getStyle('marginBottom', node), 10) || 0; + opts.item_height += Math.max(marginTop, marginBottom); + } + opts.block_height = opts.item_height * opts.rows_in_block; + opts.rows_in_cluster = opts.blocks_in_cluster * opts.rows_in_block; + opts.cluster_height = opts.blocks_in_cluster * opts.block_height; + return prev_item_height != opts.item_height; + }, + // get current cluster number + getClusterNum: function () { + this.options.scroll_top = this.scroll_elem.scrollTop; + return Math.floor(this.options.scroll_top / (this.options.cluster_height - this.options.block_height)) || 0; + }, + // generate empty row if no data provided + generateEmptyRow: function() { + var opts = this.options; + if( ! opts.tag || ! opts.show_no_data_row) return []; + var empty_row = document.createElement(opts.tag), + no_data_content = document.createTextNode(opts.no_data_text), td; + empty_row.className = opts.no_data_class; + if(opts.tag == 'tr') { + td = document.createElement('td'); + // fixes #53 + td.colSpan = 100; + td.appendChild(no_data_content); + } + empty_row.appendChild(td || no_data_content); + return [empty_row.outerHTML]; + }, + // generate cluster for current scroll position + generate: function (rows, cluster_num) { + var opts = this.options, + rows_len = rows.length; + if (rows_len < opts.rows_in_block) { + return { + top_offset: 0, + bottom_offset: 0, + rows_above: 0, + rows: rows_len ? rows : this.generateEmptyRow() + } + } + var items_start = Math.max((opts.rows_in_cluster - opts.rows_in_block) * cluster_num, 0), + items_end = items_start + opts.rows_in_cluster, + top_offset = Math.max(items_start * opts.item_height, 0), + bottom_offset = Math.max((rows_len - items_end) * opts.item_height, 0), + this_cluster_rows = [], + rows_above = items_start; + if(top_offset < 1) { + rows_above++; + } + for (var i = items_start; i < items_end; i++) { + rows[i] && this_cluster_rows.push(rows[i]); + } + return { + top_offset: top_offset, + bottom_offset: bottom_offset, + rows_above: rows_above, + rows: this_cluster_rows + } + }, + renderExtraTag: function(class_name, height) { + var tag = document.createElement(this.options.tag), + clusterize_prefix = 'clusterize-'; + tag.className = [clusterize_prefix + 'extra-row', clusterize_prefix + class_name].join(' '); + height && (tag.style.height = height + 'px'); + return tag.outerHTML; + }, + // if necessary verify data changed and insert to DOM + insertToDOM: function(rows, cache) { + // explore row's height + if( ! this.options.cluster_height) { + this.exploreEnvironment(rows, cache); + } + var data = this.generate(rows, this.getClusterNum()), + this_cluster_rows = data.rows.join(''), + this_cluster_content_changed = this.checkChanges('data', this_cluster_rows, cache), + top_offset_changed = this.checkChanges('top', data.top_offset, cache), + only_bottom_offset_changed = this.checkChanges('bottom', data.bottom_offset, cache), + callbacks = this.options.callbacks, + layout = []; + + if(this_cluster_content_changed || top_offset_changed) { + if(data.top_offset) { + this.options.keep_parity && layout.push(this.renderExtraTag('keep-parity')); + layout.push(this.renderExtraTag('top-space', data.top_offset)); + } + layout.push(this_cluster_rows); + data.bottom_offset && layout.push(this.renderExtraTag('bottom-space', data.bottom_offset)); + callbacks.clusterWillChange && callbacks.clusterWillChange(); + this.html(layout.join('')); + this.options.content_tag == 'ol' && this.content_elem.setAttribute('start', data.rows_above); + callbacks.clusterChanged && callbacks.clusterChanged(); + } else if(only_bottom_offset_changed) { + this.content_elem.lastChild.style.height = data.bottom_offset + 'px'; + } + }, + // unfortunately ie <= 9 does not allow to use innerHTML for table elements, so make a workaround + html: function(data) { + var content_elem = this.content_elem; + if(ie && ie <= 9 && this.options.tag == 'tr') { + var div = document.createElement('div'), last; + div.innerHTML = '' + data + '
'; + while((last = content_elem.lastChild)) { + content_elem.removeChild(last); + } + var rows_nodes = this.getChildNodes(div.firstChild.firstChild); + while (rows_nodes.length) { + content_elem.appendChild(rows_nodes.shift()); + } + } else { + content_elem.innerHTML = data; + } + }, + getChildNodes: function(tag) { + var child_nodes = tag.children, nodes = []; + for (var i = 0, ii = child_nodes.length; i < ii; i++) { + nodes.push(child_nodes[i]); + } + return nodes; + }, + checkChanges: function(type, value, cache) { + var changed = value != cache[type]; + cache[type] = value; + return changed; + } + } + + // support functions + function on(evt, element, fnc) { + return element.addEventListener ? element.addEventListener(evt, fnc, false) : element.attachEvent("on" + evt, fnc); + } + function off(evt, element, fnc) { + return element.removeEventListener ? element.removeEventListener(evt, fnc, false) : element.detachEvent("on" + evt, fnc); + } + function isArray(arr) { + return Object.prototype.toString.call(arr) === '[object Array]'; + } + function getStyle(prop, elem) { + return window.getComputedStyle ? window.getComputedStyle(elem)[prop] : elem.currentStyle[prop]; + } + + return Clusterize; +})); \ No newline at end of file diff --git a/erpnext/public/js/setup_wizard.js b/erpnext/public/js/setup_wizard.js index d5518857000..320d871849f 100644 --- a/erpnext/public/js/setup_wizard.js +++ b/erpnext/public/js/setup_wizard.js @@ -7,7 +7,11 @@ frappe.pages['setup-wizard'].on_page_load = function(wrapper) { } }; -var erpnext_slides = [ +frappe.setup.on("before_load", function () { + erpnext.setup.slides_settings.map(frappe.setup.add_slide); +}); + +erpnext.setup.slides_settings = [ { // Domain name: 'domain', @@ -18,14 +22,14 @@ var erpnext_slides = [ fieldname: 'domain', label: __('Domain'), fieldtype: 'Select', options: [ { "label": __("Distribution"), "value": "Distribution" }, - { "label": __("Education"), "value": "Education" }, { "label": __("Manufacturing"), "value": "Manufacturing" }, { "label": __("Retail"), "value": "Retail" }, - { "label": __("Services"), "value": "Services" } + { "label": __("Services"), "value": "Services" }, + { "label": __("Education"), "value": "Education" } ], reqd: 1 }, ], - help: __('Select the nature of your business.'), + // help: __('Select the nature of your business.'), onload: function (slide) { slide.get_input("domain").on("change", function () { frappe.setup.domain = $(this).val(); @@ -40,7 +44,7 @@ var erpnext_slides = [ domains: ["all"], icon: "fa fa-bookmark", title: __("The Brand"), - help: __('Upload your letter head and logo. (you can edit them later).'), + // help: __('Upload your letter head and logo. (you can edit them later).'), fields: [ { fieldtype: "Attach Image", fieldname: "attach_logo", @@ -79,6 +83,12 @@ var erpnext_slides = [ slide.get_field("company_abbr").set_value(""); } }); + }, + validate: function() { + if (!this.values.company_abbr) { + return false; + } + return true; } }, { @@ -87,9 +97,9 @@ var erpnext_slides = [ domains: ["all"], title: __("Your Organization"), icon: "fa fa-building", - help: (frappe.setup.domain === 'Education' ? - __('The name of the institute for which you are setting up this system.') : - __('The name of your company for which you are setting up this system.')), + // help: (frappe.setup.domain === 'Education' ? + // __('The name of the institute for which you are setting up this system.') : + // __('The name of your company for which you are setting up this system.')), fields: [ { fieldname: 'company_tagline', @@ -189,213 +199,6 @@ var erpnext_slides = [ slide.form.fields_dict.fy_end_date.set_value(year_end_date); }); } - }, - - { - // Users - name: 'users', - domains: ["all"], - title: __("Add Users"), - help: __("Add users to your organization, other than yourself"), - add_more: 1, - max_count: 3, - fields: [ - {fieldtype:"Section Break"}, - {fieldtype:"Data", fieldname:"user_fullname", - label:__("Full Name"), static: 1}, - {fieldtype:"Data", fieldname:"user_email", label:__("Email ID"), - placeholder:__("user@example.com"), options: "Email", static: 1}, - {fieldtype:"Column Break"}, - {fieldtype: "Check", fieldname: "user_sales", - label:__("Sales"), "default": 1, static: 1, - hidden: frappe.setup.domain==='Education' ? 1 : 0}, - {fieldtype: "Check", fieldname: "user_purchaser", - label:__("Purchaser"), "default": 1, static: 1, - hidden: frappe.setup.domain==='Education' ? 1 : 0}, - {fieldtype: "Check", fieldname: "user_accountant", - label:__("Accountant"), "default": 1, static: 1, - hidden: frappe.setup.domain==='Education' ? 1 : 0}, - ] - }, - - { - // Sales Target - name: 'Goals', - domains: ['manufacturing', 'services', 'retail', 'distribution'], - title: __("Set your Target"), - help: __("Set a sales target you'd like to achieve."), - fields: [ - {fieldtype:"Currency", fieldname:"sales_target", label:__("Monthly Sales Target")}, - ] - }, - - { - // Taxes - name: 'taxes', - domains: ['manufacturing', 'services', 'retail', 'distribution'], - icon: "fa fa-money", - title: __("Add Taxes"), - help: __("List your tax heads (e.g. VAT, Customs etc; they should have unique names) and their standard rates. This will create a standard template, which you can edit and add more later."), - add_more: 1, - max_count: 3, - mandatory_entry: 0, - fields: [ - {fieldtype:"Section Break"}, - {fieldtype:"Data", fieldname:"tax", label:__("Tax"), - placeholder:__("e.g. VAT")}, - {fieldtype:"Column Break"}, - {fieldtype:"Float", fieldname:"tax_rate", label:__("Rate (%)"), placeholder:__("e.g. 5")} - ] - }, - - { - // Customers - name: 'customers', - domains: ['manufacturing', 'services', 'retail', 'distribution'], - icon: "fa fa-group", - title: __("Add Customers"), - help: __("List a few of your customers. They could be organizations or individuals."), - add_more: 1, - max_count: 5, - mandatory_entry: 1, - fields: [ - {fieldtype:"Section Break"}, - {fieldtype:"Data", fieldname:"customer", label:__("Customer"), - placeholder:__("Customer Name")}, - {fieldtype:"Column Break"}, - {fieldtype:"Data", fieldname:"customer_contact", - label:__("Contact Name"), placeholder:__("Contact Name")} - ], - }, - - { - // Suppliers - name: 'suppliers', - domains: ['manufacturing', 'services', 'retail', 'distribution'], - icon: "fa fa-group", - title: __("Your Suppliers"), - help: __("List a few of your suppliers. They could be organizations or individuals."), - add_more: 1, - max_count: 5, - mandatory_entry: 1, - fields: [ - {fieldtype:"Section Break"}, - {fieldtype:"Data", fieldname:"supplier", label:__("Supplier"), - placeholder:__("Supplier Name")}, - {fieldtype:"Column Break"}, - {fieldtype:"Data", fieldname:"supplier_contact", - label:__("Contact Name"), placeholder:__("Contact Name")}, - ] - }, - - { - // Products - name: 'products', - domains: ['manufacturing', 'services', 'retail', 'distribution'], - icon: "fa fa-barcode", - title: __("Your Products or Services"), - help: __("List your products or services that you buy or sell. Make sure to check the Item Group, Unit of Measure and other properties when you start."), - add_more: 1, - max_count: 5, - mandatory_entry: 1, - fields: [ - {fieldtype:"Section Break", show_section_border: true}, - {fieldtype:"Data", fieldname:"item", label:__("Item"), - placeholder:__("A Product or Service")}, - {fieldtype:"Select", label:__("Group"), fieldname:"item_group", - options:[__("Products"), __("Services"), - __("Raw Material"), __("Consumable"), __("Sub Assemblies")], - "default": __("Products"), static: 1}, - {fieldtype:"Select", fieldname:"item_uom", label:__("UOM"), - options:[__("Unit"), __("Nos"), __("Box"), __("Pair"), __("Kg"), __("Set"), - __("Hour"), __("Minute"), __("Litre"), __("Meter"), __("Gram")], - "default": __("Unit"), static: 1}, - {fieldtype: "Check", fieldname: "is_sales_item", - label:__("We sell this Item"), default: 1, static: 1}, - {fieldtype: "Check", fieldname: "is_purchase_item", - label:__("We buy this Item"), default: 1, static: 1}, - {fieldtype:"Column Break"}, - {fieldtype:"Currency", fieldname:"item_price", label:__("Rate"), static: 1}, - {fieldtype:"Attach Image", fieldname:"item_img", label:__("Attach Image"), is_private: 0, static: 1}, - ], - get_item_count: function() { - return this.item_count; - } - }, - - { - // Program - name: 'program', - domains: ["education"], - title: __("Program"), - help: __("Example: Masters in Computer Science"), - add_more: 1, - max_count: 5, - mandatory_entry: 1, - fields: [ - {fieldtype:"Section Break", show_section_border: true}, - {fieldtype:"Data", fieldname:"program", label:__("Program"), placeholder: __("Program Name")}, - ], - }, - - { - // Course - name: 'course', - domains: ["education"], - title: __("Course"), - help: __("Example: Basic Mathematics"), - add_more: 1, - max_count: 5, - mandatory_entry: 1, - fields: [ - {fieldtype:"Section Break", show_section_border: true}, - {fieldtype:"Data", fieldname:"course", label:__("Course"), placeholder: __("Course Name")}, - ] - }, - - { - // Instructor - name: 'instructor', - domains: ["education"], - title: __("Instructor"), - help: __("People who teach at your organisation"), - add_more: 1, - max_count: 5, - mandatory_entry: 1, - fields: [ - {fieldtype:"Section Break", show_section_border: true}, - {fieldtype:"Data", fieldname:"instructor", label:__("Instructor"), placeholder: __("Instructor Name")}, - ] - }, - - { - // Room - name: 'room', - domains: ["education"], - title: __("Room"), - help: __("Classrooms/ Laboratories etc where lectures can be scheduled."), - add_more: 1, - max_count: 3, - mandatory_entry: 1, - fields: [ - {fieldtype:"Section Break", show_section_border: true}, - {fieldtype:"Data", fieldname:"room", label:__("Room")}, - {fieldtype:"Column Break"}, - {fieldtype:"Int", fieldname:"room_capacity", label:__("Room") + " Capacity", static: 1}, - ] - }, - - { - // last slide: Sample Data - name: 'bootstrap', - domains: ["all"], - title: __("Sample Data"), - fields: [{fieldtype: "Section Break"}, - {fieldtype: "Check", fieldname: "add_sample_data", - label: __("Add a few sample records"), "default": 1}, - {fieldtype: "Check", fieldname: "setup_website", - label: __("Setup a simple website for my organization"), "default": 1} - ] } ]; @@ -422,23 +225,19 @@ erpnext.setup.fiscal_years = { "United Kingdom": ["04-01", "03-31"], }; -frappe.setup.on("before_load", function () { - erpnext_slides.map(frappe.setup.add_slide); -}); - -var test_values_edu = { - "language": "english", - "domain": "Education", - "country": "India", - "timezone": "Asia/Kolkata", - "currency": "INR", - "first_name": "Tester", - "email": "test@example.com", - "password": "test", - "company_name": "Hogwarts", - "company_abbr": "HS", - "company_tagline": "School for magicians", - "bank_account": "Gringotts Wizarding Bank", - "fy_start_date": "2016-04-01", - "fy_end_date": "2017-03-31" -} +// var test_values_edu = { +// "language": "english", +// "domain": "Education", +// "country": "India", +// "timezone": "Asia/Kolkata", +// "currency": "INR", +// "first_name": "Tester", +// "email": "test@example.com", +// "password": "test", +// "company_name": "Hogwarts", +// "company_abbr": "HS", +// "company_tagline": "School for magicians", +// "bank_account": "Gringotts Wizarding Bank", +// "fy_start_date": "2016-04-01", +// "fy_end_date": "2017-03-31" +// } diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index a333ca82d68..8a47df63716 100644 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -127,6 +127,20 @@ $.extend(erpnext.utils, { } }, + make_subscription: function(doctype, docname) { + frappe.call({ + method: "erpnext.subscription.doctype.subscription.subscription.make_subscription", + args: { + doctype: doctype, + docname: docname + }, + callback: function(r) { + var doclist = frappe.model.sync(r.message); + frappe.set_route("Form", doclist[0].doctype, doclist[0].name); + } + }) + }, + /** * Checks if the first row of a given child table is empty * @param child_table - Child table Doctype diff --git a/erpnext/public/js/utils/serial_no_batch_selector.js b/erpnext/public/js/utils/serial_no_batch_selector.js index 08630e59984..3e2414e665b 100644 --- a/erpnext/public/js/utils/serial_no_batch_selector.js +++ b/erpnext/public/js/utils/serial_no_batch_selector.js @@ -1,15 +1,16 @@ erpnext.SerialNoBatchSelector = Class.extend({ - init: function(opts) { + init: function(opts, show_dialog) { $.extend(this, opts); + this.show_dialog = show_dialog; // frm, item, warehouse_details, has_batch, oldest let d = this.item; // Don't show dialog if batch no or serial no already set - if(d && d.has_batch_no && !d.batch_no) { + if(d && d.has_batch_no && (!d.batch_no || this.show_dialog)) { this.has_batch = 1; this.setup(); - } else if(d && d.has_serial_no && !d.serial_no) { + } else if(d && d.has_serial_no && (!d.serial_no || this.show_dialog)) { this.has_batch = 0; this.setup(); } @@ -93,6 +94,11 @@ erpnext.SerialNoBatchSelector = Class.extend({ } }); + if(this.show_dialog) { + let d = this.item; + this.dialog.set_value('serial_no', d.serial_no); + } + this.dialog.show(); }, @@ -140,6 +146,7 @@ erpnext.SerialNoBatchSelector = Class.extend({ this.map_row_values(this.item, this.values, 'serial_no', 'qty'); } refresh_field("items"); + this.callback && this.callback(this.item); }, map_row_values: function(row, values, number, qty_field, warehouse) { diff --git a/erpnext/public/less/erpnext.less b/erpnext/public/less/erpnext.less index 6c616c9e32a..de46c53df88 100644 --- a/erpnext/public/less/erpnext.less +++ b/erpnext/public/less/erpnext.less @@ -364,17 +364,6 @@ body[data-route="pos"] { .pos-item-wrapper { position: relative; } - - .price-info { - position: absolute; - left: 0; - bottom: 0; - margin: 0 0 15px 15px; - background-color: rgba(141, 153, 166, 0.6); - padding: 5px 9px; - border-radius: 3px; - color: #fff; - } } .pos-bill-toolbar { @@ -423,4 +412,15 @@ body[data-route="pos"] { .collapse-btn { cursor: pointer; } +} + +.price-info { + position: absolute; + left: 0; + bottom: 0; + margin: 0 0 15px 15px; + background-color: rgba(141, 153, 166, 0.6); + padding: 5px 9px; + border-radius: 3px; + color: #fff; } \ No newline at end of file diff --git a/erpnext/public/less/pos.less b/erpnext/public/less/pos.less new file mode 100644 index 00000000000..9653a826585 --- /dev/null +++ b/erpnext/public/less/pos.less @@ -0,0 +1,222 @@ +@import "../../../../frappe/frappe/public/less/variables.less"; + +[data-route="point-of-sale"] { + .layout-main-section-wrapper { + margin-bottom: 0; + } + + .pos-items-wrapper { + max-height: ~"calc(100vh - 210px)"; + } +} + +.pos { + // display: flex; + padding: 15px; +} + +.list-item { + min-height: 40px; + height: auto; +} + +.cart-container { + padding: 0 15px; + // flex: 2; + display: inline-block; + width: 39%; + vertical-align: top; +} + +.item-container { + padding: 0 15px; + // flex: 3; + display: inline-block; + width: 60%; + vertical-align: top; +} + +.search-field { + width: 60%; + + input::placeholder { + font-size: @text-medium; + } +} + +.item-group-field { + width: 40%; + margin-left: 15px; +} + +.cart-wrapper { + margin-bottom: 10px; + .list-item__content:not(:first-child) { + justify-content: flex-end; + } + + .list-item--head .list-item__content:nth-child(2) { + flex: 1.5; + } +} + +.cart-items { + height: 150px; + overflow: auto; + + .list-item.current-item { + background-color: @light-yellow; + } + + .list-item.current-item.qty input { + border: 1px solid @brand-primary; + font-weight: bold; + } + + .list-item.current-item.disc .discount { + font-weight: bold; + } + + .list-item.current-item.rate .rate { + font-weight: bold; + } + + .list-item .quantity { + flex: 1.5; + } + + input { + text-align: right; + height: 22px; + font-size: @text-medium; + } +} + +.fields { + display: flex; +} + +.pos-items-wrapper { + max-height: 480px; + overflow-y: auto; +} + +.pos-items { + overflow: hidden; +} + +.pos-item-wrapper { + display: flex; + flex-direction: column; + position: relative; + width: 25%; +} + +.image-view-container { + display: block; +} + +.image-view-container .image-field { + height: auto; +} + +.empty-state { + height: 100%; + position: relative; + + span { + position: absolute; + color: @text-muted; + font-size: @text-medium; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + } +} + +@keyframes yellow-fade { + 0% {background-color: @light-yellow;} + 100% {background-color: transparent;} +} + +.highlight { + animation: yellow-fade 1s ease-in 1; +} + +input[type=number]::-webkit-inner-spin-button, +input[type=number]::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; +} + +// number pad + +.number-pad { + border-collapse: collapse; + cursor: pointer; + display: table; + margin: auto; +} +.num-row { + display: table-row; +} +.num-col { + display: table-cell; + border: 1px solid @border-color; + + & > div { + width: 50px; + height: 50px; + text-align: center; + line-height: 50px; + } + + &.active { + background-color: @light-yellow; + } + + &.brand-primary { + background-color: @brand-primary; + color: #ffffff; + } +} + +// taxes, totals and discount area +.discount-amount { + .discount-inputs { + display: flex; + flex-direction: column; + padding: 15px 0; + } + + input:first-child { + margin-bottom: 10px; + } +} + +.taxes-and-totals { + border-top: 1px solid @border-color; + + .taxes { + display: flex; + flex-direction: column; + padding: 15px 0; + align-items: flex-end; + + & > div:first-child { + margin-bottom: 10px; + } + } +} + +.grand-total { + border-top: 1px solid @border-color; + + .list-item { + height: 60px; + } + + .grand-total-value { + font-size: 24px; + } +} \ No newline at end of file diff --git a/erpnext/regional/doctype/gst_hsn_code/gst_hsn_code.json b/erpnext/regional/doctype/gst_hsn_code/gst_hsn_code.json index 23cf0823628..7b3a8d6b724 100644 --- a/erpnext/regional/doctype/gst_hsn_code/gst_hsn_code.json +++ b/erpnext/regional/doctype/gst_hsn_code/gst_hsn_code.json @@ -84,13 +84,34 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2017-06-30 20:12:57.903983", - "modified_by": "Administrator", + "modified": "2017-08-31 14:38:52.220743", + "modified_by": "ewdszx@ed.ews", "module": "Regional", "name": "GST HSN Code", "name_case": "", "owner": "Administrator", - "permissions": [], + "permissions": [ + { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "if_owner": 0, + "import": 0, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "set_user_permissions": 0, + "share": 1, + "submit": 0, + "write": 1 + } + ], "quick_entry": 1, "read_only": 0, "read_only_onload": 0, diff --git a/erpnext/regional/doctype/gst_hsn_code/test_gst_hsn_code.js b/erpnext/regional/doctype/gst_hsn_code/test_gst_hsn_code.js new file mode 100644 index 00000000000..24c5fd355ff --- /dev/null +++ b/erpnext/regional/doctype/gst_hsn_code/test_gst_hsn_code.js @@ -0,0 +1,23 @@ +/* eslint-disable */ +// rename this file from _test_[name] to test_[name] to activate +// and remove above this line + +QUnit.test("test: GST HSN Code", function (assert) { + let done = assert.async(); + + // number of asserts + assert.expect(1); + + frappe.run_serially([ + // insert a new GST HSN Code + () => frappe.tests.make('GST HSN Code', [ + // values to be set + {key: 'value'} + ]), + () => { + assert.equal(cur_frm.doc.key, 'value'); + }, + () => done() + ]); + +}); diff --git a/erpnext/regional/doctype/gst_settings/gst_settings.json b/erpnext/regional/doctype/gst_settings/gst_settings.json index 61af138e440..04065e29dfa 100644 --- a/erpnext/regional/doctype/gst_settings/gst_settings.json +++ b/erpnext/regional/doctype/gst_settings/gst_settings.json @@ -83,13 +83,34 @@ "issingle": 1, "istable": 0, "max_attachments": 0, - "modified": "2017-06-28 16:20:21.206397", - "modified_by": "Administrator", + "modified": "2017-08-31 14:39:15.625952", + "modified_by": "ewdszx@ed.ews", "module": "Regional", "name": "GST Settings", "name_case": "", "owner": "Administrator", - "permissions": [], + "permissions": [ + { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 0, + "if_owner": 0, + "import": 0, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 0, + "role": "System Manager", + "set_user_permissions": 0, + "share": 1, + "submit": 0, + "write": 1 + } + ], "quick_entry": 1, "read_only": 0, "read_only_onload": 0, diff --git a/erpnext/regional/doctype/gst_settings/test_gst_settings.js b/erpnext/regional/doctype/gst_settings/test_gst_settings.js new file mode 100644 index 00000000000..00fcca6f326 --- /dev/null +++ b/erpnext/regional/doctype/gst_settings/test_gst_settings.js @@ -0,0 +1,23 @@ +/* eslint-disable */ +// rename this file from _test_[name] to test_[name] to activate +// and remove above this line + +QUnit.test("test: GST Settings", function (assert) { + let done = assert.async(); + + // number of asserts + assert.expect(1); + + frappe.run_serially([ + // insert a new GST Settings + () => frappe.tests.make('GST Settings', [ + // values to be set + {key: 'value'} + ]), + () => { + assert.equal(cur_frm.doc.key, 'value'); + }, + () => done() + ]); + +}); diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index 0c59ba003a8..46afeece1bf 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -39,12 +39,12 @@ def add_hsn_sac_codes(): hsn_codes = json.loads(f.read()) create_hsn_codes(hsn_codes, code_field="hsn_code") - + # SAC Codes with open(os.path.join(os.path.dirname(__file__), 'sac_code_data.json'), 'r') as f: sac_codes = json.loads(f.read()) create_hsn_codes(sac_codes, code_field="sac_code") - + def create_hsn_codes(data, code_field): for d in data: if not frappe.db.exists("GST HSN Code", d[code_field]): @@ -54,8 +54,6 @@ def create_hsn_codes(data, code_field): hsn_code.name = d[code_field] hsn_code.db_insert() - frappe.db.commit() - def add_custom_roles_for_reports(): for report_name in ('GST Sales Register', 'GST Purchase Register', 'GST Itemised Sales Register', 'GST Itemised Purchase Register'): @@ -101,7 +99,7 @@ def make_custom_fields(): dict(fieldname='ecommerce_gstin', label='E-commerce GSTIN', fieldtype='Data', insert_after='export_type', print_hide=1) ] - + purchase_invoice_gst_fields = [ dict(fieldname='supplier_gstin', label='Supplier GSTIN', fieldtype='Data', insert_after='supplier_address', @@ -110,7 +108,7 @@ def make_custom_fields(): fieldtype='Data', insert_after='shipping_address', options='shipping_address.gstin', print_hide=1) ] - + sales_invoice_gst_fields = [ dict(fieldname='customer_gstin', label='Customer GSTIN', fieldtype='Data', insert_after='shipping_address', @@ -122,7 +120,7 @@ def make_custom_fields(): fieldtype='Data', insert_after='company_address', options='company_address.gstin', print_hide=1) ] - + custom_fields = { 'Address': [ dict(fieldname='gstin', label='Party GSTIN', fieldtype='Data', diff --git a/erpnext/schools/doctype/assessment_plan/assessment_plan.js b/erpnext/schools/doctype/assessment_plan/assessment_plan.js index be628b83c35..e83c4d3c70e 100644 --- a/erpnext/schools/doctype/assessment_plan/assessment_plan.js +++ b/erpnext/schools/doctype/assessment_plan/assessment_plan.js @@ -27,6 +27,14 @@ frappe.ui.form.on("Assessment Plan", { frappe.set_route("Form", "Assessment Result Tool"); }); } + + frm.set_query('grading_scale', function(){ + return { + filters: { + docstatus: 1 + } + } + }); }, course: function(frm) { diff --git a/erpnext/schools/doctype/course/course.js b/erpnext/schools/doctype/course/course.js index c667eca2b7b..e31ba726861 100644 --- a/erpnext/schools/doctype/course/course.js +++ b/erpnext/schools/doctype/course/course.js @@ -28,4 +28,12 @@ frappe.ui.form.on("Course", "refresh", function(frm) { frappe.set_route("List", "Assessment Plan"); }); } + + frm.set_query('default_grading_scale', function(){ + return { + filters: { + docstatus: 1 + } + } + }); }); \ No newline at end of file diff --git a/erpnext/selling/doctype/quotation/quotation.js b/erpnext/selling/doctype/quotation/quotation.js index 3e5e52fe2af..1863fb2a5e2 100644 --- a/erpnext/selling/doctype/quotation/quotation.js +++ b/erpnext/selling/doctype/quotation/quotation.js @@ -10,12 +10,15 @@ frappe.ui.form.on('Quotation', { 'Sales Order': 'Make Sales Order' } }, + refresh: function(frm) { frm.trigger("set_label"); }, + quotation_to: function(frm) { frm.trigger("set_label"); }, + set_label: function(frm) { frm.fields_dict.customer_address.set_label(__(frm.doc.quotation_to + " Address")); } @@ -44,14 +47,22 @@ erpnext.selling.QuotationController = erpnext.selling.SellingController.extend({ if(doc.docstatus == 1 && doc.status!=='Lost') { if(!doc.valid_till || frappe.datetime.get_diff(doc.valid_till, frappe.datetime.get_today()) > 0) { - cur_frm.add_custom_button(__('Make Sales Order'), - cur_frm.cscript['Make Sales Order']); + cur_frm.add_custom_button(__('Sales Order'), + cur_frm.cscript['Make Sales Order'], __("Make")); } if(doc.status!=="Ordered") { cur_frm.add_custom_button(__('Set as Lost'), cur_frm.cscript['Declare Order Lost']); } + + if(!doc.subscription) { + cur_frm.add_custom_button(__('Subscription'), function() { + erpnext.utils.make_subscription(doc.doctype, doc.name) + }, __("Make")) + } + + cur_frm.page.set_inner_btn_group_as_primary(__("Make")); } if (this.frm.doc.docstatus===0) { @@ -161,7 +172,7 @@ cur_frm.cscript['Make Sales Order'] = function() { cur_frm.cscript['Declare Order Lost'] = function(){ var dialog = new frappe.ui.Dialog({ - title: "Set as Lost", + title: __('Set as Lost'), fields: [ {"fieldtype": "Text", "label": __("Reason for losing"), "fieldname": "reason", "reqd": 1 }, diff --git a/erpnext/selling/doctype/quotation/quotation.json b/erpnext/selling/doctype/quotation/quotation.json index 0afc5ca3d07..ab879a1b057 100644 --- a/erpnext/selling/doctype/quotation/quotation.json +++ b/erpnext/selling/doctype/quotation/quotation.json @@ -2337,6 +2337,67 @@ "set_only_once": 0, "unique": 0 }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "subscription_section", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Subscription Section", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "subscription", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Subscription", + "length": 0, + "no_copy": 1, + "options": "Subscription", + "permlevel": 0, + "precision": "", + "print_hide": 1, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, { "allow_bulk_edit": 0, "allow_on_submit": 0, @@ -2633,7 +2694,7 @@ "istable": 0, "max_attachments": 1, "menu_index": 0, - "modified": "2017-08-09 06:35:48.691648", + "modified": "2017-08-31 11:22:15.268846", "modified_by": "Administrator", "module": "Selling", "name": "Quotation", diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index f3ebe810565..1cdd8404288 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -108,6 +108,9 @@ class Quotation(SellingController): print_lst.append(lst1) return print_lst + def on_recurring(self, reference_doc, subscription_doc): + self.valid_till = None + def get_list_context(context=None): from erpnext.controllers.website_list_for_contact import get_list_context list_context = get_list_context(context) diff --git a/erpnext/selling/doctype/quotation/quotation_dashboard.py b/erpnext/selling/doctype/quotation/quotation_dashboard.py index f1c41e560a1..c6297e22ab5 100644 --- a/erpnext/selling/doctype/quotation/quotation_dashboard.py +++ b/erpnext/selling/doctype/quotation/quotation_dashboard.py @@ -3,9 +3,17 @@ from frappe import _ def get_data(): return { 'fieldname': 'prevdoc_docname', + 'non_standard_fieldnames': { + 'Subscription': 'reference_document', + }, 'transactions': [ { + 'label': _('Sales Order'), 'items': ['Sales Order'] }, + { + 'label': _('Subscription'), + 'items': ['Subscription'] + }, ] } \ No newline at end of file diff --git a/erpnext/selling/doctype/quotation/tests/test_quotation.js b/erpnext/selling/doctype/quotation/tests/test_quotation.js index 44173cc0e1f..31b17970fe9 100644 --- a/erpnext/selling/doctype/quotation/tests/test_quotation.js +++ b/erpnext/selling/doctype/quotation/tests/test_quotation.js @@ -50,4 +50,4 @@ QUnit.test("test: quotation", function (assert) { }, () => done() ]); -}); \ No newline at end of file +}); diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index 901e236bd2e..00d2121897a 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -141,6 +141,12 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( function() { me.make_project() }, __("Make")); } + if(!doc.subscription) { + this.frm.add_custom_button(__('Subscription'), function() { + erpnext.utils.make_subscription(doc.doctype, doc.name) + }, __("Make")) + } + } else { if (this.frm.has_perm("submit")) { // un-close diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json index b69b3fda77b..3b8eb68592b 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.json +++ b/erpnext/selling/doctype/sales_order/sales_order.json @@ -3179,6 +3179,67 @@ "set_only_once": 0, "unique": 0 }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 1, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "subscription_section", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Subscription Section", + "length": 0, + "no_copy": 1, + "permlevel": 0, + "precision": "", + "print_hide": 1, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "subscription", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Subscription", + "length": 0, + "no_copy": 0, + "options": "Subscription", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, { "allow_bulk_edit": 0, "allow_on_submit": 0, @@ -3189,7 +3250,7 @@ "depends_on": "eval:doc.docstatus<2 && !doc.__islocal", "fieldname": "recurring_order", "fieldtype": "Section Break", - "hidden": 0, + "hidden": 1, "ignore_user_permissions": 0, "ignore_xss_filter": 0, "in_filter": 0, @@ -3659,7 +3720,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2017-08-07 21:27:10.073581", + "modified": "2017-08-31 11:21:36.332326", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order", diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 5f904c2e3d5..5f828900d5e 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -11,9 +11,9 @@ from frappe.model.utils import get_fetch_values from frappe.model.mapper import get_mapped_doc from erpnext.stock.stock_balance import update_bin_qty, get_reserved_qty from frappe.desk.notifications import clear_doctype_notifications -from erpnext.controllers.recurring_document import month_map, get_next_date from frappe.contacts.doctype.address.address import get_company_address from erpnext.controllers.selling_controller import SellingController +from erpnext.subscription.doctype.subscription.subscription import month_map, get_next_date form_grid_templates = { "items": "templates/form_grid/item_grid.html" @@ -346,17 +346,17 @@ class SalesOrder(SellingController): return items - def on_recurring(self, reference_doc): - mcount = month_map[reference_doc.recurring_type] + def on_recurring(self, reference_doc, subscription_doc): + mcount = month_map[subscription_doc.frequency] self.set("delivery_date", get_next_date(reference_doc.delivery_date, mcount, - cint(reference_doc.repeat_on_day_of_month))) + cint(subscription_doc.repeat_on_day))) for d in self.get("items"): reference_delivery_date = frappe.db.get_value("Sales Order Item", {"parent": reference_doc.name, "item_code": d.item_code, "idx": d.idx}, "delivery_date") d.set("delivery_date", - get_next_date(reference_delivery_date, mcount, cint(reference_doc.repeat_on_day_of_month))) + get_next_date(reference_delivery_date, mcount, cint(subscription_doc.repeat_on_day))) def get_list_context(context=None): from erpnext.controllers.website_list_for_contact import get_list_context diff --git a/erpnext/selling/doctype/sales_order/sales_order_dashboard.py b/erpnext/selling/doctype/sales_order/sales_order_dashboard.py index a0ed034313a..ffce7ce102d 100644 --- a/erpnext/selling/doctype/sales_order/sales_order_dashboard.py +++ b/erpnext/selling/doctype/sales_order/sales_order_dashboard.py @@ -7,7 +7,8 @@ def get_data(): 'Delivery Note': 'against_sales_order', 'Journal Entry': 'reference_name', 'Payment Entry': 'reference_name', - 'Payment Request': 'reference_name' + 'Payment Request': 'reference_name', + 'Subscription': 'reference_document', }, 'internal_links': { 'Quotation': ['items', 'prevdoc_docname'] @@ -31,7 +32,7 @@ def get_data(): }, { 'label': _('Reference'), - 'items': ['Quotation'] + 'items': ['Quotation', 'Subscription'] }, { 'label': _('Payment'), diff --git a/erpnext/selling/doctype/sales_order/tests/test_sales_order.js b/erpnext/selling/doctype/sales_order/tests/test_sales_order.js index 3eceb89ca26..6568d5cad09 100644 --- a/erpnext/selling/doctype/sales_order/tests/test_sales_order.js +++ b/erpnext/selling/doctype/sales_order/tests/test_sales_order.js @@ -1,7 +1,7 @@ QUnit.module('Sales Order'); QUnit.test("test sales order", function(assert) { - assert.expect(8); + assert.expect(10); let done = assert.async(); frappe.run_serially([ () => { @@ -12,7 +12,7 @@ QUnit.test("test sales order", function(assert) { {'delivery_date': frappe.datetime.add_days(frappe.defaults.get_default("year_end_date"), 1)}, {'qty': 5}, {'item_code': 'Test Product 4'}, - {'uom': 'unit'}, + {'uom': 'Nos'}, {'margin_type': 'Percentage'}, {'discount_percentage': 10}, ] @@ -33,7 +33,7 @@ QUnit.test("test sales order", function(assert) { {additional_discount_percentage:10} ]); }, - () => cur_frm.save(), + () => frappe.timeout(1), () => { // get_item_details assert.ok(cur_frm.doc.items[0].item_name=='Test Product 4', "Item name correct"); @@ -42,15 +42,19 @@ QUnit.test("test sales order", function(assert) { // get tax account head details assert.ok(cur_frm.doc.taxes[0].account_head=='CGST - '+frappe.get_abbr(frappe.defaults.get_default('Company')), " Account Head abbr correct"); // calculate totals - assert.ok(cur_frm.doc.items[0].price_list_rate==1000, "Item 1 price_list_rate"); - assert.ok(cur_frm.doc.total== 4500, "total correct "); - assert.ok(cur_frm.doc.rounded_total== 4414.5, "rounded total correct "); - + assert.ok(cur_frm.doc.items[0].price_list_rate==90, "Item 1 price_list_rate"); + assert.ok(cur_frm.doc.total== 405, "total correct "); + assert.ok(cur_frm.doc.net_total== 364.5, "net total correct "); + assert.ok(cur_frm.doc.grand_total== 397.30, "grand total correct "); + assert.ok(cur_frm.doc.rounded_total== 397.30, "rounded total correct "); }, + () => cur_frm.save(), + () => frappe.timeout(1), () => cur_frm.print_doc(), () => frappe.timeout(1), () => { assert.ok($('.btn-print-print').is(':visible'), "Print Format Available"); + frappe.timeout(1); assert.ok($(".section-break+ .section-break .column-break:nth-child(1) .data-field:nth-child(1) .value").text().includes("Billing Street 1"), "Print Preview Works As Expected"); }, () => cur_frm.print_doc(), diff --git a/erpnext/selling/doctype/sales_order/tests/test_sales_order_with_multi_uom.js b/erpnext/selling/doctype/sales_order/tests/test_sales_order_with_multi_uom.js index 74f51ca72cf..84301f5a86b 100644 --- a/erpnext/selling/doctype/sales_order/tests/test_sales_order_with_multi_uom.js +++ b/erpnext/selling/doctype/sales_order/tests/test_sales_order_with_multi_uom.js @@ -12,7 +12,7 @@ QUnit.test("test sales order", function(assert) { {'delivery_date': frappe.datetime.add_days(frappe.defaults.get_default("year_end_date"), 1)}, {'qty': 5}, {'item_code': 'Test Product 4'}, - {'uom': 'unit'}, + {'uom': 'Unit'}, ] ]}, {customer_address: 'Test1-Billing'}, diff --git a/erpnext/selling/page/point_of_sale/__init__.py b/erpnext/selling/page/point_of_sale/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.js b/erpnext/selling/page/point_of_sale/point_of_sale.js new file mode 100644 index 00000000000..9cd2a499126 --- /dev/null +++ b/erpnext/selling/page/point_of_sale/point_of_sale.js @@ -0,0 +1,1284 @@ +/* global Clusterize */ +frappe.provide('erpnext.pos'); + +frappe.pages['point-of-sale'].on_page_load = function(wrapper) { + frappe.ui.make_app_page({ + parent: wrapper, + title: 'Point of Sale', + single_column: true + }); + + frappe.db.get_value('POS Settings', {name: 'POS Settings'}, 'is_online', (r) => { + if (r && r.is_online && cint(r.is_online)) { + // online + wrapper.pos = new erpnext.pos.PointOfSale(wrapper); + window.cur_pos = wrapper.pos; + } else { + // offline + frappe.set_route('pos'); + } + }); +}; + +erpnext.pos.PointOfSale = class PointOfSale { + constructor(wrapper) { + this.wrapper = $(wrapper).find('.layout-main-section'); + this.page = wrapper.page; + + const assets = [ + 'assets/erpnext/js/pos/clusterize.js', + 'assets/erpnext/css/pos.css' + ]; + + frappe.require(assets, () => { + this.make(); + }); + } + + make() { + return frappe.run_serially([ + () => { + this.prepare_dom(); + this.prepare_menu(); + this.set_online_status(); + }, + () => this.setup_pos_profile(), + () => { + this.make_items(); + this.bind_events(); + }, + () => this.make_new_invoice(), + () => this.page.set_title(__('Point of Sale')) + ]); + } + + set_online_status() { + this.connection_status = false; + this.page.set_indicator(__("Offline"), "grey"); + frappe.call({ + method: "frappe.handler.ping", + callback: r => { + if (r.message) { + this.connection_status = true; + this.page.set_indicator(__("Online"), "green"); + } + } + }); + } + + prepare_dom() { + this.wrapper.append(` +
+
+ +
+
+ +
+
+ `); + } + + make_cart() { + this.cart = new POSCart({ + frm: this.frm, + wrapper: this.wrapper.find('.cart-container'), + events: { + on_customer_change: (customer) => this.frm.set_value('customer', customer), + on_field_change: (item_code, field, value) => { + this.update_item_in_cart(item_code, field, value); + }, + on_numpad: (value) => { + if (value == 'Pay') { + if (!this.payment) { + this.make_payment_modal(); + } + this.payment.open_modal(); + } + }, + on_select_change: () => { + this.cart.numpad.set_inactive(); + } + } + }); + } + + toggle_editing(flag) { + let disabled; + if (flag !== undefined) { + disabled = !flag; + } else { + disabled = this.frm.doc.docstatus == 1 ? true: false; + } + const pointer_events = disabled ? 'none' : 'inherit'; + + this.wrapper.find('input, button, select').prop("disabled", disabled); + this.wrapper.find('.number-pad-container').toggleClass("hide", disabled); + + this.wrapper.find('.cart-container').css('pointer-events', pointer_events); + this.wrapper.find('.item-container').css('pointer-events', pointer_events); + + this.page.clear_actions(); + } + + make_items() { + this.items = new POSItems({ + wrapper: this.wrapper.find('.item-container'), + pos_profile: this.pos_profile, + events: { + update_cart: (item, field, value) => { + if(!this.frm.doc.customer) { + frappe.throw(__('Please select a customer')); + } + this.update_item_in_cart(item, field, value); + this.cart && this.cart.unselect_all(); + } + } + }); + } + + update_item_in_cart(item_code, field='qty', value=1) { + if(this.cart.exists(item_code)) { + const item = this.frm.doc.items.find(i => i.item_code === item_code); + frappe.flags.hide_serial_batch_dialog = false; + + if (typeof value === 'string' && !in_list(['serial_no', 'batch_no'], field)) { + // value can be of type '+1' or '-1' + value = item[field] + flt(value); + } + + if(field === 'serial_no') { + value = item.serial_no + '\n'+ value; + } + + if(field === 'qty' && (item.serial_no || item.batch_no)) { + this.select_batch_and_serial_no(item); + } else { + this.update_item_in_frm(item, field, value) + .then(() => { + // update cart + this.update_cart_data(item); + }); + } + return; + } + + let args = { item_code: item_code }; + if (in_list(['serial_no', 'batch_no'], field)) { + args[field] = value; + } + + // add to cur_frm + const item = this.frm.add_child('items', args); + frappe.flags.hide_serial_batch_dialog = true; + this.frm.script_manager + .trigger('item_code', item.doctype, item.name) + .then(() => { + const show_dialog = item.has_serial_no || item.has_batch_no; + if (show_dialog && field == 'qty') { + // check has serial no/batch no and update cart + this.select_batch_and_serial_no(item); + } else { + // update cart + this.update_cart_data(item); + } + }); + } + + select_batch_and_serial_no(item) { + erpnext.show_serial_batch_selector(this.frm, item, () => { + this.update_item_in_frm(item) + .then(() => { + // update cart + this.update_cart_data(item); + }); + }, true); + } + + update_cart_data(item) { + this.cart.add_item(item); + this.cart.update_taxes_and_totals(); + this.cart.update_grand_total(); + } + + update_item_in_frm(item, field, value) { + if (field) { + frappe.model.set_value(item.doctype, item.name, field, value); + } + + return this.frm.script_manager + .trigger('qty', item.doctype, item.name) + .then(() => { + if (field === 'qty' && value === 0) { + frappe.model.clear_doc(item.doctype, item.name); + } + }); + } + + make_payment_modal() { + this.payment = new Payment({ + frm: this.frm, + events: { + submit_form: () => { + this.submit_sales_invoice(); + } + } + }); + } + + submit_sales_invoice() { + + frappe.confirm(__("Permanently Submit {0}?", [this.frm.doc.name]), () => { + frappe.call({ + method: 'erpnext.selling.page.point_of_sale.point_of_sale.submit_invoice', + freeze: true, + args: { + doc: this.frm.doc + } + }).then(r => { + if(r.message) { + this.frm.doc = r.message; + frappe.show_alert({ + indicator: 'green', + message: __(`Sales invoice ${r.message.name} created succesfully`) + }); + + this.toggle_editing(); + this.set_form_action(); + } + }); + }); + } + + bind_events() { + + } + + setup_pos_profile() { + return frappe.call({ + method: 'erpnext.stock.get_item_details.get_pos_profile', + args: { + company: frappe.sys_defaults.company + } + }).then(r => { + this.pos_profile = r.message; + + if (!this.pos_profile) { + this.pos_profile = { + currency: frappe.defaults.get_default('currency'), + selling_price_list: frappe.defaults.get_default('selling_price_list') + }; + } + }); + } + + make_new_invoice() { + return frappe.run_serially([ + () => this.make_sales_invoice_frm(), + () => { + if (this.cart) { + this.cart.frm = this.frm; + this.cart.reset(); + } else { + this.make_cart(); + } + this.toggle_editing(true); + } + ]); + } + + make_sales_invoice_frm() { + const doctype = 'Sales Invoice'; + return new Promise(resolve => { + if (this.frm) { + this.frm = get_frm(this.frm); + resolve(); + } else { + frappe.model.with_doctype(doctype, () => { + this.frm = get_frm(); + resolve(); + }); + } + }); + + function get_frm(_frm) { + const page = $('
'); + const frm = _frm || new _f.Frm(doctype, page, false); + const name = frappe.model.make_new_doc_and_get_name(doctype, true); + frm.refresh(name); + frm.doc.items = []; + frm.set_value('is_pos', 1); + frm.meta.default_print_format = 'POS Invoice'; + return frm; + } + } + + prepare_menu() { + var me = this; + this.page.clear_menu(); + + // for mobile + // this.page.add_menu_item(__("Pay"), function () { + // + // }).addClass('visible-xs'); + + this.page.add_menu_item(__("Form View"), function () { + frappe.model.sync(me.frm.doc); + frappe.set_route("Form", me.frm.doc.doctype, me.frm.doc.name); + }); + + this.page.add_menu_item(__("POS Profile"), function () { + frappe.set_route('List', 'POS Profile'); + }); + + this.page.add_menu_item(__('POS Settings'), function() { + frappe.set_route('Form', 'POS Settings'); + }); + } + + set_form_action() { + if(this.frm.doc.docstatus !== 1) return; + + this.page.set_secondary_action(__("Print"), () => { + if (this.pos_profile && this.pos_profile.print_format_for_online) { + this.frm.meta.default_print_format = this.pos_profile.print_format_for_online; + } + this.frm.print_preview.printit(true); + }); + + this.page.set_primary_action(__("New"), () => { + this.make_new_invoice(); + }); + + this.page.add_menu_item(__("Email"), () => { + this.frm.email_doc(); + }); + } +}; + +class POSCart { + constructor({frm, wrapper, events}) { + this.frm = frm; + this.wrapper = wrapper; + this.events = events; + this.make(); + this.bind_events(); + } + + make() { + this.make_dom(); + this.make_customer_field(); + this.make_numpad(); + } + + make_dom() { + this.wrapper.append(` +
+
+
+
+
+
+
${__('Item Name')}
+
${__('Quantity')}
+
${__('Discount')}
+
${__('Rate')}
+
+
+
+ No Items added to cart +
+
+
+ ${this.get_taxes_and_totals()} +
+
+ ${this.get_discount_amount()} +
+
+ ${this.get_grand_total()} +
+
+
+
+
+
+ `); + this.$cart_items = this.wrapper.find('.cart-items'); + this.$empty_state = this.wrapper.find('.cart-items .empty-state'); + this.$taxes_and_totals = this.wrapper.find('.taxes-and-totals'); + this.$discount_amount = this.wrapper.find('.discount-amount'); + this.$grand_total = this.wrapper.find('.grand-total'); + + this.toggle_taxes_and_totals(false); + this.$grand_total.on('click', () => { + this.toggle_taxes_and_totals(); + }); + } + + reset() { + this.$cart_items.find('.list-item').remove(); + this.$empty_state.show(); + this.$taxes_and_totals.html(this.get_taxes_and_totals()); + this.numpad && this.numpad.reset_value(); + this.customer_field.set_value(""); + } + + get_grand_total() { + return ` +
+
${__('Grand Total')}
+
0.00
+
+ `; + } + + get_discount_amount() { + const get_currency_symbol = window.get_currency_symbol; + + return ` +
+
${__('Discount')}
+
+ + +
+
+ `; + } + + get_taxes_and_totals() { + return ` +
+
${__('Net Total')}
+
0.00
+
+
+
${__('Taxes')}
+
0.00
+
+ `; + } + + toggle_taxes_and_totals(flag) { + if (flag !== undefined) { + this.tax_area_is_shown = flag; + } else { + this.tax_area_is_shown = !this.tax_area_is_shown; + } + + this.$taxes_and_totals.toggle(this.tax_area_is_shown); + this.$discount_amount.toggle(this.tax_area_is_shown); + } + + update_taxes_and_totals() { + const currency = this.frm.doc.currency; + this.frm.refresh_field('taxes'); + + // Update totals + this.$taxes_and_totals.find('.net-total') + .html(format_currency(this.frm.doc.net_total, currency)); + + // Update taxes + const taxes_html = this.frm.doc.taxes.map(tax => { + return ` +
+ ${tax.description} + + ${format_currency(tax.tax_amount, currency)} + +
+ `; + }).join(""); + this.$taxes_and_totals.find('.taxes').html(taxes_html); + } + + update_grand_total() { + this.$grand_total.find('.grand-total-value').text( + format_currency(this.frm.doc.grand_total, this.frm.currency) + ); + } + + make_customer_field() { + this.customer_field = frappe.ui.form.make_control({ + df: { + fieldtype: 'Link', + label: 'Customer', + fieldname: 'customer', + options: 'Customer', + reqd: 1, + default: this.frm.doc.customer, + onchange: () => { + this.events.on_customer_change(this.customer_field.get_value()); + } + }, + parent: this.wrapper.find('.customer-field'), + render_input: true + }); + } + + make_numpad() { + this.numpad = new NumberPad({ + button_array: [ + [1, 2, 3, 'Qty'], + [4, 5, 6, 'Disc'], + [7, 8, 9, 'Rate'], + ['Del', 0, '.', 'Pay'] + ], + add_class: { + 'Pay': 'brand-primary' + }, + disable_highlight: ['Qty', 'Disc', 'Rate', 'Pay'], + reset_btns: ['Qty', 'Disc', 'Rate', 'Pay'], + del_btn: 'Del', + wrapper: this.wrapper.find('.number-pad-container'), + onclick: (btn_value) => { + // on click + if (!this.selected_item && btn_value !== 'Pay') { + frappe.show_alert({ + indicator: 'red', + message: __('Please select an item in the cart') + }); + return; + } + if (['Qty', 'Disc', 'Rate'].includes(btn_value)) { + this.set_input_active(btn_value); + } else if (btn_value !== 'Pay') { + if (!this.selected_item.active_field) { + frappe.show_alert({ + indicator: 'red', + message: __('Please select a field to edit from numpad') + }); + return; + } + + const item_code = this.selected_item.attr('data-item-code'); + const field = this.selected_item.active_field; + const value = this.numpad.get_value(); + + this.events.on_field_change(item_code, field, value); + } + + this.events.on_numpad(btn_value); + } + }); + } + + set_input_active(btn_value) { + this.selected_item.removeClass('qty disc rate'); + + this.numpad.set_active(btn_value); + if (btn_value === 'Qty') { + this.selected_item.addClass('qty'); + this.selected_item.active_field = 'qty'; + } else if (btn_value == 'Disc') { + this.selected_item.addClass('disc'); + this.selected_item.active_field = 'discount_percentage'; + } else if (btn_value == 'Rate') { + this.selected_item.addClass('rate'); + this.selected_item.active_field = 'rate'; + } + } + + add_item(item) { + this.$empty_state.hide(); + + if (this.exists(item.item_code)) { + // update quantity + this.update_item(item); + } else { + // add to cart + const $item = $(this.get_item_html(item)); + $item.appendTo(this.$cart_items); + } + this.highlight_item(item.item_code); + this.scroll_to_item(item.item_code); + } + + update_item(item) { + const $item = this.$cart_items.find(`[data-item-code="${item.item_code}"]`); + + if(item.qty > 0) { + const indicator_class = item.actual_qty >= item.qty ? 'green' : 'red'; + const remove_class = indicator_class == 'green' ? 'red' : 'green'; + + $item.find('.quantity input').val(item.qty); + $item.find('.discount').text(item.discount_percentage + '%'); + $item.find('.rate').text(format_currency(item.rate, this.frm.doc.currency)); + $item.addClass(indicator_class); + $item.removeClass(remove_class); + } else { + $item.remove(); + } + } + + get_item_html(item) { + const rate = format_currency(item.rate, this.frm.doc.currency); + const indicator_class = item.actual_qty >= item.qty ? 'green' : 'red'; + return ` +
+
+ ${item.item_name} +
+
+ ${get_quantity_html(item.qty)} +
+
+ ${item.discount_percentage}% +
+
+ ${rate} +
+
+ `; + + function get_quantity_html(value) { + return ` +
+ + + + + + + + + +
+ `; + } + } + + exists(item_code) { + let $item = this.$cart_items.find(`[data-item-code="${item_code}"]`); + return $item.length > 0; + } + + highlight_item(item_code) { + const $item = this.$cart_items.find(`[data-item-code="${item_code}"]`); + $item.addClass('highlight'); + setTimeout(() => $item.removeClass('highlight'), 1000); + } + + scroll_to_item(item_code) { + const $item = this.$cart_items.find(`[data-item-code="${item_code}"]`); + if ($item.length === 0) return; + const scrollTop = $item.offset().top - this.$cart_items.offset().top + this.$cart_items.scrollTop(); + this.$cart_items.animate({ scrollTop }); + } + + bind_events() { + const me = this; + const events = this.events; + + // quantity change + this.$cart_items.on('click', + '[data-action="increment"], [data-action="decrement"]', function() { + const $btn = $(this); + const $item = $btn.closest('.list-item[data-item-code]'); + const item_code = $item.attr('data-item-code'); + const action = $btn.attr('data-action'); + + if(action === 'increment') { + events.on_field_change(item_code, 'qty', '+1'); + } else if(action === 'decrement') { + events.on_field_change(item_code, 'qty', '-1'); + } + }); + + // this.$cart_items.on('focus', '.quantity input', function(e) { + // const $input = $(this); + // const $item = $input.closest('.list-item[data-item-code]'); + // me.set_selected_item($item); + // me.set_input_active('Qty'); + // e.preventDefault(); + // e.stopPropagation(); + // return false; + // }); + + this.$cart_items.on('change', '.quantity input', function() { + const $input = $(this); + const $item = $input.closest('.list-item[data-item-code]'); + const item_code = $item.attr('data-item-code'); + events.on_field_change(item_code, 'qty', flt($input.val())); + }); + + // current item + this.$cart_items.on('click', '.list-item', function() { + me.set_selected_item($(this)); + }); + + // disable current item + // $('body').on('click', function(e) { + // console.log(e); + // if($(e.target).is('.list-item')) { + // return; + // } + // me.$cart_items.find('.list-item').removeClass('current-item qty disc rate'); + // me.selected_item = null; + // }); + + this.wrapper.find('.additional_discount_percentage').on('change', (e) => { + frappe.model.set_value(this.frm.doctype, this.frm.docname, + 'additional_discount_percentage', e.target.value) + .then(() => { + let discount_wrapper = this.wrapper.find('.discount_amount'); + discount_wrapper.val(this.frm.doc.discount_amount); + discount_wrapper.trigger('change'); + }); + }); + + this.wrapper.find('.discount_amount').on('change', (e) => { + frappe.model.set_value(this.frm.doctype, this.frm.docname, + 'discount_amount', e.target.value); + this.frm.trigger('discount_amount') + .then(() => { + let discount_wrapper = this.wrapper.find('.additional_discount_percentage'); + discount_wrapper.val(this.frm.doc.additional_discount_percentage); + this.update_taxes_and_totals(); + this.update_grand_total(); + }); + }); + } + + set_selected_item($item) { + this.selected_item = $item; + this.$cart_items.find('.list-item').removeClass('current-item qty disc rate'); + this.selected_item.addClass('current-item'); + this.events.on_select_change(); + } + + unselect_all() { + this.$cart_items.find('.list-item').removeClass('current-item qty disc rate'); + this.selected_item = null; + this.events.on_select_change(); + } +} + +class POSItems { + constructor({wrapper, pos_profile, events}) { + this.wrapper = wrapper; + this.pos_profile = pos_profile; + this.items = {}; + this.events = events; + this.currency = this.pos_profile.currency; + + this.make_dom(); + this.make_fields(); + + this.init_clusterize(); + this.bind_events(); + + // bootstrap with 20 items + this.get_items() + .then(({ items }) => { + this.all_items = items; + this.items = items; + this.render_items(items); + }); + } + + make_dom() { + this.wrapper.html(` +
+
+
+
+
+
+
+
+ `); + + this.items_wrapper = this.wrapper.find('.items-wrapper'); + this.items_wrapper.append(` +
+
+
+
+ `); + } + + make_fields() { + // Search field + this.search_field = frappe.ui.form.make_control({ + df: { + fieldtype: 'Data', + label: 'Search Item (Ctrl + I)', + placeholder: 'Search by item code, serial number, batch no or barcode' + }, + parent: this.wrapper.find('.search-field'), + render_input: true, + }); + + frappe.ui.keys.on('ctrl+i', () => { + this.search_field.set_focus(); + }); + + this.search_field.$input.on('input', (e) => { + clearTimeout(this.last_search); + this.last_search = setTimeout(() => { + const search_term = e.target.value; + this.filter_items({ search_term }); + }, 300); + }); + + this.item_group_field = frappe.ui.form.make_control({ + df: { + fieldtype: 'Link', + label: 'Item Group', + options: 'Item Group', + default: 'All Item Groups', + onchange: () => { + const item_group = this.item_group_field.get_value(); + if (item_group) { + this.filter_items({ item_group: item_group }); + } + }, + }, + parent: this.wrapper.find('.item-group-field'), + render_input: true + }); + } + + init_clusterize() { + this.clusterize = new Clusterize({ + scrollElem: this.wrapper.find('.pos-items-wrapper')[0], + contentElem: this.wrapper.find('.pos-items')[0], + rows_in_block: 6 + }); + } + + render_items(items) { + let _items = items || this.items; + + const all_items = Object.values(_items).map(item => this.get_item_html(item)); + let row_items = []; + + const row_container = '
'; + let curr_row = row_container; + + for (let i=0; i < all_items.length; i++) { + // wrap 4 items in a div to emulate + // a row for clusterize + if(i % 4 === 0 && i !== 0) { + curr_row += '
'; + row_items.push(curr_row); + curr_row = row_container; + } + curr_row += all_items[i]; + + if(i == all_items.length - 1 && all_items.length % 4 !== 0) { + row_items.push(curr_row); + } + } + + this.clusterize.update(row_items); + } + + filter_items({ search_term='', item_group='All Item Groups' }={}) { + if (search_term) { + search_term = search_term.toLowerCase(); + + // memoize + this.search_index = this.search_index || {}; + if (this.search_index[search_term]) { + const items = this.search_index[search_term]; + this.render_items(items); + return; + } + } else if (item_group == "All Item Groups") { + return this.render_items(this.all_items); + } + + this.get_items({search_value: search_term, item_group }) + .then(({ items, serial_no, batch_no }) => { + if (search_term) { + this.search_index[search_term] = items; + } + + this.render_items(items); + if(serial_no) { + this.events.update_cart(items[0].item_code, + 'serial_no', serial_no); + this.search_field.set_value(''); + } + if(batch_no) { + this.events.update_cart(items[0].item_code, + 'batch_no', serial_no); + this.search_field.set_value(''); + } + }); + } + + bind_events() { + var me = this; + this.wrapper.on('click', '.pos-item-wrapper', function() { + const $item = $(this); + const item_code = $item.attr('data-item-code'); + me.events.update_cart(item_code, 'qty', '+1'); + }); + } + + get(item_code) { + return this.items[item_code]; + } + + get_all() { + return this.items; + } + + get_item_html(item) { + const price_list_rate = format_currency(item.price_list_rate, this.currency); + const { item_code, item_name, item_image} = item; + const item_title = item_name || item_code; + + const template = ` + + `; + + return template; + } + + get_items({start = 0, page_length = 40, search_value='', item_group="All Item Groups"}={}) { + return new Promise(res => { + frappe.call({ + method: "erpnext.selling.page.point_of_sale.point_of_sale.get_items", + args: { + start, + page_length, + 'price_list': this.pos_profile.selling_price_list, + item_group, + search_value + } + }).then(r => { + // const { items, serial_no, batch_no } = r.message; + + // this.serial_no = serial_no || ""; + res(r.message); + }); + }); + } +} + +class NumberPad { + constructor({ + wrapper, onclick, button_array, + add_class={}, disable_highlight=[], + reset_btns=[], del_btn='', + }) { + this.wrapper = wrapper; + this.onclick = onclick; + this.button_array = button_array; + this.add_class = add_class; + this.disable_highlight = disable_highlight; + this.reset_btns = reset_btns; + this.del_btn = del_btn; + this.make_dom(); + this.bind_events(); + this.value = ''; + } + + make_dom() { + if (!this.button_array) { + this.button_array = [ + [1, 2, 3], + [4, 5, 6], + [7, 8, 9], + ['', 0, ''] + ]; + } + + this.wrapper.html(` +
+ ${this.button_array.map(get_row).join("")} +
+ `); + + function get_row(row) { + return '
' + row.map(get_col).join("") + '
'; + } + + function get_col(col) { + return `
${col}
`; + } + + this.set_class(); + } + + set_class() { + for (const btn in this.add_class) { + const class_name = this.add_class[btn]; + this.get_btn(btn).addClass(class_name); + } + } + + bind_events() { + // bind click event + const me = this; + this.wrapper.on('click', '.num-col', function() { + const $btn = $(this); + const btn_value = $btn.attr('data-value'); + if (!me.disable_highlight.includes(btn_value)) { + me.highlight_button($btn); + } + if (me.reset_btns.includes(btn_value)) { + me.reset_value(); + } else { + if (btn_value === me.del_btn) { + me.value = me.value.substr(0, me.value.length - 1); + } else { + me.value += btn_value; + } + } + me.onclick(btn_value); + }); + } + + reset_value() { + this.value = ''; + } + + get_value() { + return flt(this.value); + } + + get_btn(btn_value) { + return this.wrapper.find(`.num-col[data-value="${btn_value}"]`); + } + + highlight_button($btn) { + $btn.addClass('highlight'); + setTimeout(() => $btn.removeClass('highlight'), 1000); + } + + set_active(btn_value) { + const $btn = this.get_btn(btn_value); + this.wrapper.find('.num-col').removeClass('active'); + $btn.addClass('active'); + } + + set_inactive() { + this.wrapper.find('.num-col').removeClass('active'); + } +} + +class Payment { + constructor({frm, events}) { + this.frm = frm; + this.events = events; + this.make(); + this.bind_events(); + this.set_primary_action(); + } + + open_modal() { + this.dialog.show(); + } + + make() { + this.set_flag(); + + let title = __('Total Amount {0}', + [format_currency(this.frm.doc.grand_total, this.frm.doc.currency)]); + + this.dialog = new frappe.ui.Dialog({ + title: title, + fields: this.get_fields(), + width: 800 + }); + + this.$body = this.dialog.body; + + this.numpad = new NumberPad({ + wrapper: $(this.$body).find('[data-fieldname="numpad"]'), + button_array: [ + [1, 2, 3], + [4, 5, 6], + [7, 8, 9], + ['Del', 0, '.'], + ], + onclick: () => { + if(this.fieldname) { + this.dialog.set_value(this.fieldname, this.numpad.get_value()); + } + } + }); + } + + bind_events() { + var me = this; + $(this.dialog.body).find('.input-with-feedback').focusin(function() { + me.numpad.reset_value(); + me.fieldname = $(this).prop('dataset').fieldname; + }); + } + + set_primary_action() { + var me = this; + + this.dialog.set_primary_action(__("Submit"), function() { + me.dialog.hide(); + me.events.submit_form(); + }); + } + + get_fields() { + const me = this; + + let fields = this.frm.doc.payments.map(p => { + return { + fieldtype: 'Currency', + label: __(p.mode_of_payment), + options: me.frm.doc.currency, + fieldname: p.mode_of_payment, + default: p.amount, + onchange: () => { + const value = this.dialog.get_value(this.fieldname); + me.update_payment_value(this.fieldname, value); + } + }; + }); + + fields = fields.concat([ + { + fieldtype: 'Column Break', + }, + { + fieldtype: 'HTML', + fieldname: 'numpad' + }, + { + fieldtype: 'Section Break', + }, + { + fieldtype: 'Currency', + label: __("Write off Amount"), + options: me.frm.doc.currency, + fieldname: "write_off_amount", + default: me.frm.doc.write_off_amount, + onchange: () => { + me.update_cur_frm_value('write_off_amount', () => { + frappe.flags.change_amount = false; + me.update_change_amount(); + }); + } + }, + { + fieldtype: 'Column Break', + }, + { + fieldtype: 'Currency', + label: __("Change Amount"), + options: me.frm.doc.currency, + fieldname: "change_amount", + default: me.frm.doc.change_amount, + onchange: () => { + me.update_cur_frm_value('change_amount', () => { + frappe.flags.write_off_amount = false; + me.update_write_off_amount(); + }); + } + }, + { + fieldtype: 'Section Break', + }, + { + fieldtype: 'Currency', + label: __("Paid Amount"), + options: me.frm.doc.currency, + fieldname: "paid_amount", + default: me.frm.doc.paid_amount, + read_only: 1 + }, + { + fieldtype: 'Column Break', + }, + { + fieldtype: 'Currency', + label: __("Outstanding Amount"), + options: me.frm.doc.currency, + fieldname: "outstanding_amount", + default: me.frm.doc.outstanding_amount, + read_only: 1 + }, + ]); + + return fields; + } + + set_flag() { + frappe.flags.write_off_amount = true; + frappe.flags.change_amount = true; + } + + update_cur_frm_value(fieldname, callback) { + if (frappe.flags[fieldname]) { + const value = this.dialog.get_value(fieldname); + this.frm.set_value(fieldname, value) + .then(() => { + callback(); + }); + } + + frappe.flags[fieldname] = true; + } + + update_payment_value(fieldname, value) { + var me = this; + $.each(this.frm.doc.payments, function(i, data) { + if (__(data.mode_of_payment) == __(fieldname)) { + frappe.model.set_value('Sales Invoice Payment', data.name, 'amount', value) + .then(() => { + me.update_change_amount(); + me.update_write_off_amount(); + }); + } + }); + } + + update_change_amount() { + this.dialog.set_value("change_amount", this.frm.doc.change_amount); + this.show_paid_amount(); + } + + update_write_off_amount() { + this.dialog.set_value("write_off_amount", this.frm.doc.write_off_amount); + } + + show_paid_amount() { + this.dialog.set_value("paid_amount", this.frm.doc.paid_amount); + this.dialog.set_value("outstanding_amount", this.frm.doc.outstanding_amount); + } +} diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.json b/erpnext/selling/page/point_of_sale/point_of_sale.json new file mode 100644 index 00000000000..1e348c09af8 --- /dev/null +++ b/erpnext/selling/page/point_of_sale/point_of_sale.json @@ -0,0 +1,20 @@ +{ + "content": null, + "creation": "2017-08-07 17:08:56.737947", + "docstatus": 0, + "doctype": "Page", + "idx": 0, + "modified": "2017-08-07 17:08:56.737947", + "modified_by": "Administrator", + "module": "Selling", + "name": "point-of-sale", + "owner": "Administrator", + "page_name": "Point of Sale", + "restrict_to_domain": "Retail", + "roles": [], + "script": null, + "standard": "Yes", + "style": null, + "system_page": 0, + "title": "Point of Sale" +} \ No newline at end of file diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.py b/erpnext/selling/page/point_of_sale/point_of_sale.py new file mode 100644 index 00000000000..8ed288b6e9d --- /dev/null +++ b/erpnext/selling/page/point_of_sale/point_of_sale.py @@ -0,0 +1,71 @@ +# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals +import frappe, json + +@frappe.whitelist() +def get_items(start, page_length, price_list, item_group, search_value=""): + serial_no = "" + batch_no = "" + item_code = search_value + + if search_value: + # search serial no + serial_no_data = frappe.db.get_value('Serial No', search_value, ['name', 'item_code']) + if serial_no_data: + serial_no, item_code = serial_no_data + + if not serial_no: + batch_no_data = frappe.db.get_value('Batch', search_value, ['name', 'item']) + if batch_no_data: + batch_no, item_code = batch_no_data + + lft, rgt = frappe.db.get_value('Item Group', item_group, ['lft', 'rgt']) + # locate function is used to sort by closest match from the beginning of the value + res = frappe.db.sql("""select i.name as item_code, i.item_name, i.image as item_image, + item_det.price_list_rate, item_det.currency + from `tabItem` i LEFT JOIN + (select item_code, price_list_rate, currency from + `tabItem Price` where price_list=%(price_list)s) item_det + ON + (item_det.item_code=i.name or item_det.item_code=i.variant_of) + where + i.disabled = 0 and i.has_variants = 0 + and i.item_group in (select name from `tabItem Group` where lft >= {lft} and rgt <= {rgt}) + and (i.item_code like %(item_code)s + or i.item_name like %(item_code)s or i.barcode like %(item_code)s) + limit {start}, {page_length}""".format(start=start, page_length=page_length, lft=lft, rgt=rgt), + { + 'item_code': '%%%s%%'%(frappe.db.escape(item_code)), + 'price_list': price_list + } , as_dict=1) + + res = { + 'items': res + } + + if serial_no: + res.update({ + 'serial_no': serial_no + }) + + if batch_no: + res.update({ + 'batch_no': batch_no + }) + + return res + +@frappe.whitelist() +def submit_invoice(doc): + if isinstance(doc, basestring): + args = json.loads(doc) + + doc = frappe.new_doc('Sales Invoice') + doc.update(args) + doc.run_method("set_missing_values") + doc.run_method("calculate_taxes_and_totals") + doc.submit() + + return doc diff --git a/erpnext/selling/page/point_of_sale/tests/test_point_of_sale.js b/erpnext/selling/page/point_of_sale/tests/test_point_of_sale.js new file mode 100644 index 00000000000..c70d076c70a --- /dev/null +++ b/erpnext/selling/page/point_of_sale/tests/test_point_of_sale.js @@ -0,0 +1,38 @@ +QUnit.test("test:Point of Sales", function(assert) { + assert.expect(1); + let done = assert.async(); + + frappe.run_serially([ + () => frappe.set_route('point-of-sale'), + () => frappe.timeout(2), + () => frappe.set_control('customer', 'Test Customer 1'), + () => frappe.timeout(0.2), + () => cur_frm.set_value('customer', 'Test Customer 1'), + () => frappe.timeout(2), + () => frappe.click_link('Test Product 2'), + () => frappe.timeout(0.2), + () => frappe.click_element(`.cart-items [data-item-code="Test Product 2"]`), + () => frappe.timeout(0.2), + () => frappe.click_element(`.number-pad [data-value="Rate"]`), + () => frappe.timeout(0.2), + () => frappe.click_element(`.number-pad [data-value="2"]`), + () => frappe.timeout(0.2), + () => frappe.click_element(`.number-pad [data-value="5"]`), + () => frappe.timeout(0.2), + () => frappe.click_element(`.number-pad [data-value="0"]`), + () => frappe.timeout(0.2), + () => frappe.click_element(`.number-pad [data-value="Pay"]`), + () => frappe.timeout(0.2), + () => frappe.click_element(`.frappe-control [data-value="4"]`), + () => frappe.timeout(0.2), + () => frappe.click_element(`.frappe-control [data-value="5"]`), + () => frappe.timeout(0.2), + () => frappe.click_element(`.frappe-control [data-value="0"]`), + () => frappe.timeout(0.2), + () => frappe.click_button('Submit'), + () => frappe.click_button('Yes'), + () => frappe.timeout(3), + () => assert.ok(cur_frm.doc.docstatus==1, "Sales invoice created successfully"), + () => done() + ]); +}); \ No newline at end of file diff --git a/erpnext/selling/page/point_of_sale/tests/test_pos_settings.js b/erpnext/selling/page/point_of_sale/tests/test_pos_settings.js new file mode 100644 index 00000000000..d9b8cf8274b --- /dev/null +++ b/erpnext/selling/page/point_of_sale/tests/test_pos_settings.js @@ -0,0 +1,17 @@ +QUnit.test("test:POS Settings", function(assert) { + assert.expect(1); + let done = assert.async(); + + frappe.run_serially([ + () => frappe.set_route('Form', 'POS Settings'), + () => cur_frm.set_value('is_online', 1), + () => frappe.timeout(0.2), + () => cur_frm.save(), + () => frappe.timeout(1), + () => frappe.ui.toolbar.clear_cache(), + () => frappe.timeout(10), + () => assert.ok(cur_frm.doc.is_online==1, "Enabled online"), + () => frappe.timeout(2), + () => done() + ]); +}); \ No newline at end of file diff --git a/erpnext/setup/doctype/company/company.js b/erpnext/setup/doctype/company/company.js index 8c814e3de8b..c15d737a310 100644 --- a/erpnext/setup/doctype/company/company.js +++ b/erpnext/setup/doctype/company/company.js @@ -8,6 +8,16 @@ frappe.ui.form.on("Company", { erpnext.company.setup_queries(frm); }, + company_name: function(frm) { + if(frm.doc.__islocal) { + let parts = frm.doc.company_name.split(); + let abbr = $.map(parts, function (p) { + return p? p.substr(0, 1) : null; + }).join(""); + frm.set_value("abbr", abbr); + } + }, + refresh: function(frm) { if(frm.doc.abbr && !frm.doc.__islocal) { frm.set_df_property("abbr", "read_only", 1); diff --git a/erpnext/setup/doctype/company/company.json b/erpnext/setup/doctype/company/company.json index c338a81a8ed..15e6b4b5b9d 100644 --- a/erpnext/setup/doctype/company/company.json +++ b/erpnext/setup/doctype/company/company.json @@ -195,6 +195,157 @@ "set_only_once": 0, "unique": 0 }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "sales_settings", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Sales", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "sales_monthly_history", + "fieldtype": "Small Text", + "hidden": 1, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Sales Monthly History", + "length": 0, + "no_copy": 1, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "monthly_sales_target", + "fieldtype": "Currency", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Monthly Sales Target", + "length": 0, + "no_copy": 0, + "options": "default_currency", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "column_break_goals", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "total_monthly_sales", + "fieldtype": "Currency", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Total Monthly Sales", + "length": 0, + "no_copy": 1, + "options": "default_currency", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, { "allow_bulk_edit": 0, "allow_on_submit": 0, @@ -500,157 +651,6 @@ "set_only_once": 0, "unique": 0 }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "sales_settings", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Sales", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "sales_monthly_history", - "fieldtype": "Small Text", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Sales Monthly History", - "length": 0, - "no_copy": 1, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "sales_target", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Sales Target", - "length": 0, - "no_copy": 1, - "options": "default_currency", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_goals", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "total_monthly_sales", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Total Monthly Sales", - "length": 0, - "no_copy": 1, - "options": "default_currency", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, { "allow_bulk_edit": 0, "allow_on_submit": 0, @@ -1991,7 +1991,7 @@ "istable": 0, "max_attachments": 0, "menu_index": 0, - "modified": "2017-08-03 16:17:31.206886", + "modified": "2017-08-31 11:48:56.278568", "modified_by": "Administrator", "module": "Setup", "name": "Company", diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index b945ee41048..d3503cc1fa4 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -76,7 +76,10 @@ class Company(Document): self.create_default_accounts() self.create_default_warehouses() - self.install_country_fixtures() + if cint(frappe.db.get_single_value('System Settings', 'setup_complete')): + # In the case of setup, fixtures should be installed after setup_success + # This also prevents db commits before setup is successful + install_country_fixtures(self.name) if not frappe.db.get_value("Cost Center", {"is_group": 0, "company": self.name}): self.create_default_cost_center() @@ -95,12 +98,6 @@ class Company(Document): frappe.clear_cache() - def install_country_fixtures(self): - path = frappe.get_app_path('erpnext', 'regional', frappe.scrub(self.country)) - if os.path.exists(path.encode("utf-8")): - frappe.get_attr("erpnext.regional.{0}.setup.setup" - .format(self.country.lower()))(self) - def create_default_warehouses(self): for wh_detail in [ {"warehouse_name": _("All Warehouses"), "is_group": 1}, @@ -311,6 +308,13 @@ def get_name_with_abbr(name, company): return " - ".join(parts) +def install_country_fixtures(company): + company_doc = frappe.get_doc("Company", company) + path = frappe.get_app_path('erpnext', 'regional', frappe.scrub(company_doc.country)) + if os.path.exists(path.encode("utf-8")): + frappe.get_attr("erpnext.regional.{0}.setup.setup" + .format(company_doc.country.lower()))(company_doc) + def update_company_current_month_sales(company): current_month_year = formatdate(today(), "MM-yyyy") diff --git a/erpnext/setup/doctype/company/company_dashboard.py b/erpnext/setup/doctype/company/company_dashboard.py index da7f2b582a7..7526dd6c547 100644 --- a/erpnext/setup/doctype/company/company_dashboard.py +++ b/erpnext/setup/doctype/company/company_dashboard.py @@ -8,8 +8,8 @@ def get_data(): 'graph': True, 'graph_method': "frappe.utils.goal.get_monthly_goal_graph_data", 'graph_method_args': { - 'title': 'Sales', - 'goal_value_field': 'sales_target', + 'title': _('Sales'), + 'goal_value_field': 'monthly_sales_target', 'goal_total_field': 'total_monthly_sales', 'goal_history_field': 'sales_monthly_history', 'goal_doctype': 'Sales Invoice', diff --git a/erpnext/setup/doctype/setup_progress/__init__.py b/erpnext/setup/doctype/setup_progress/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/setup/doctype/setup_progress/setup_progress.js b/erpnext/setup/doctype/setup_progress/setup_progress.js new file mode 100644 index 00000000000..5c78bd50758 --- /dev/null +++ b/erpnext/setup/doctype/setup_progress/setup_progress.js @@ -0,0 +1,8 @@ +// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Setup Progress', { + refresh: function() { + + } +}); diff --git a/erpnext/setup/doctype/setup_progress/setup_progress.json b/erpnext/setup/doctype/setup_progress/setup_progress.json new file mode 100644 index 00000000000..2f886afe3b1 --- /dev/null +++ b/erpnext/setup/doctype/setup_progress/setup_progress.json @@ -0,0 +1,123 @@ +{ + "allow_copy": 0, + "allow_guest_to_view": 0, + "allow_import": 0, + "allow_rename": 0, + "beta": 0, + "creation": "2017-08-27 21:01:42.032109", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "", + "editable_grid": 1, + "engine": "InnoDB", + "fields": [ + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "actions_sb", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Actions", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "actions", + "fieldtype": "Table", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Actions", + "length": 0, + "no_copy": 0, + "options": "Setup Progress Action", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + } + ], + "has_web_view": 0, + "hide_heading": 0, + "hide_toolbar": 0, + "idx": 0, + "image_view": 0, + "in_create": 1, + "is_submittable": 0, + "issingle": 1, + "istable": 0, + "max_attachments": 0, + "modified": "2017-08-28 17:44:43.100932", + "modified_by": "Administrator", + "module": "Setup", + "name": "Setup Progress", + "name_case": "", + "owner": "Administrator", + "permissions": [ + { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 0, + "if_owner": 0, + "import": 0, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 0, + "role": "System Manager", + "set_user_permissions": 0, + "share": 1, + "submit": 0, + "write": 1 + } + ], + "quick_entry": 1, + "read_only": 1, + "read_only_onload": 0, + "show_name_in_global_search": 0, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1, + "track_seen": 0 +} \ No newline at end of file diff --git a/erpnext/setup/doctype/setup_progress/setup_progress.py b/erpnext/setup/doctype/setup_progress/setup_progress.py new file mode 100644 index 00000000000..26eecd96347 --- /dev/null +++ b/erpnext/setup/doctype/setup_progress/setup_progress.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe, json +from frappe.model.document import Document + +class SetupProgress(Document): + pass + +def get_setup_progress(): + if not getattr(frappe.local, "setup_progress", None): + frappe.local.setup_progress = frappe.get_doc("Setup Progress", "Setup Progress") + + return frappe.local.setup_progress + +def get_action_completed_state(action_name): + return [d.is_completed for d in get_setup_progress().actions + if d.action_name == action_name][0] + +def update_action_completed_state(action_name): + action_table_doc = [d for d in get_setup_progress().actions + if d.action_name == action_name][0] + update_action(action_table_doc) + +def update_action(action_table_doc): + if not action_table_doc.is_completed and frappe.db.count(action_table_doc.action_doctype) >= action_table_doc.min_doc_count: + action_table_doc.is_completed = 1 + action_table_doc.save() + +def update_domain_actions(domain): + for d in get_setup_progress().actions: + domains = json.loads(d.domains) + if domains == [] or domain in domains: + update_action(d) + +def get_domain_actions_state(domain): + state = {} + for d in get_setup_progress().actions: + domains = json.loads(d.domains) + if domains == [] or domain in domains: + state[d.action_name] = d.is_completed + return state + +@frappe.whitelist() +def set_action_completed_state(action_name): + action_table_doc = [d for d in get_setup_progress().actions + if d.action_name == action_name][0] + action_table_doc.is_completed = 1 + action_table_doc.save() diff --git a/erpnext/setup/doctype/setup_progress/test_setup_progress.js b/erpnext/setup/doctype/setup_progress/test_setup_progress.js new file mode 100644 index 00000000000..9e84e0cb154 --- /dev/null +++ b/erpnext/setup/doctype/setup_progress/test_setup_progress.js @@ -0,0 +1,23 @@ +/* eslint-disable */ +// rename this file from _test_[name] to test_[name] to activate +// and remove above this line + +QUnit.test("test: Setup Progress", function (assert) { + let done = assert.async(); + + // number of asserts + assert.expect(1); + + frappe.run_serially([ + // insert a new Setup Progress + () => frappe.tests.make('Setup Progress', [ + // values to be set + {key: 'value'} + ]), + () => { + assert.equal(cur_frm.doc.key, 'value'); + }, + () => done() + ]); + +}); diff --git a/erpnext/setup/doctype/setup_progress_action/__init__.py b/erpnext/setup/doctype/setup_progress_action/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/setup/doctype/setup_progress_action/setup_progress_action.json b/erpnext/setup/doctype/setup_progress_action/setup_progress_action.json new file mode 100644 index 00000000000..030fd99a338 --- /dev/null +++ b/erpnext/setup/doctype/setup_progress_action/setup_progress_action.json @@ -0,0 +1,192 @@ +{ + "allow_copy": 0, + "allow_guest_to_view": 0, + "allow_import": 0, + "allow_rename": 0, + "beta": 0, + "creation": "2017-08-27 21:00:40.715360", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "", + "editable_grid": 1, + "engine": "InnoDB", + "fields": [ + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "action_name", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Action Name", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "action_doctype", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Action Doctype", + "length": 0, + "no_copy": 0, + "options": "DocType", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "min_doc_count", + "fieldtype": "Int", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Min Doc Count", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "domains", + "fieldtype": "Code", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Domains", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "is_completed", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Is Completed", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + } + ], + "has_web_view": 0, + "hide_heading": 0, + "hide_toolbar": 0, + "idx": 0, + "image_view": 0, + "in_create": 1, + "is_submittable": 0, + "issingle": 0, + "istable": 1, + "max_attachments": 0, + "modified": "2017-08-28 17:44:58.008526", + "modified_by": "Administrator", + "module": "Setup", + "name": "Setup Progress Action", + "name_case": "", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "read_only": 1, + "read_only_onload": 0, + "show_name_in_global_search": 0, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1, + "track_seen": 0 +} \ No newline at end of file diff --git a/erpnext/setup/doctype/setup_progress_action/setup_progress_action.py b/erpnext/setup/doctype/setup_progress_action/setup_progress_action.py new file mode 100644 index 00000000000..24af94347e5 --- /dev/null +++ b/erpnext/setup/doctype/setup_progress_action/setup_progress_action.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +from frappe.model.document import Document + +class SetupProgressAction(Document): + pass diff --git a/erpnext/setup/setup_wizard/install_fixtures.py b/erpnext/setup/setup_wizard/install_fixtures.py index ea6da04641a..53e58a15af4 100644 --- a/erpnext/setup/setup_wizard/install_fixtures.py +++ b/erpnext/setup/setup_wizard/install_fixtures.py @@ -20,6 +20,28 @@ def install(country=None): { 'doctype': 'Domain', 'domain': _('Services')}, { 'doctype': 'Domain', 'domain': _('Education')}, + # Setup Progress + {'doctype': "Setup Progress", "actions": [ + {"action_name": _("Add Company"), "action_doctype": "Company", "min_doc_count": 1, "is_completed": 1, + "domains": '[]' }, + {"action_name": _("Add Customers"), "action_doctype": "Customer", "min_doc_count": 1, "is_completed": 0, + "domains": '["Manufacturing", "Services", "Retail", "Distribution"]' }, + {"action_name": _("Add Suppliers"), "action_doctype": "Supplier", "min_doc_count": 1, "is_completed": 0, + "domains": '["Manufacturing", "Services", "Retail", "Distribution"]' }, + {"action_name": _("Add Products"), "action_doctype": "Item", "min_doc_count": 1, "is_completed": 0, + "domains": '["Manufacturing", "Services", "Retail", "Distribution"]' }, + {"action_name": _("Add Programs"), "action_doctype": "Program", "min_doc_count": 1, "is_completed": 0, + "domains": '["Education"]' }, + {"action_name": _("Add Instructors"), "action_doctype": "Instructor", "min_doc_count": 1, "is_completed": 0, + "domains": '["Education"]' }, + {"action_name": _("Add Courses"), "action_doctype": "Course", "min_doc_count": 1, "is_completed": 0, + "domains": '["Education"]' }, + {"action_name": _("Add Rooms"), "action_doctype": "Room", "min_doc_count": 1, "is_completed": 0, + "domains": '["Education"]' }, + {"action_name": _("Add Users"), "action_doctype": "User", "min_doc_count": 4, "is_completed": 0, + "domains": '[]' } + ]}, + # address template {'doctype':"Address Template", "country": country}, diff --git a/erpnext/setup/setup_wizard/sample_data.py b/erpnext/setup/setup_wizard/sample_data.py index cfc6726d1ca..bc26e09677c 100644 --- a/erpnext/setup/setup_wizard/sample_data.py +++ b/erpnext/setup/setup_wizard/sample_data.py @@ -10,26 +10,27 @@ import random, os, json from frappe import _ from markdown2 import markdown -def make_sample_data(args): +def make_sample_data(domain, make_dependent = False): """Create a few opportunities, quotes, material requests, issues, todos, projects to help the user get started""" - items = frappe.get_all("Item", {'is_sales_item': 1}) - customers = frappe.get_all("Customer") - warehouses = frappe.get_all("Warehouse") + if make_dependent: + items = frappe.get_all("Item", {'is_sales_item': 1}) + customers = frappe.get_all("Customer") + warehouses = frappe.get_all("Warehouse") - if items and customers: - for i in range(3): - customer = random.choice(customers).name - make_opportunity(items, customer) - make_quote(items, customer) + if items and customers: + for i in range(3): + customer = random.choice(customers).name + make_opportunity(items, customer) + make_quote(items, customer) - make_projects(args.get('domain')) + if items and warehouses: + make_material_request(frappe.get_all("Item")) + + make_projects(domain) import_email_alert() - if items and warehouses: - make_material_request(frappe.get_all("Item")) - frappe.db.commit() def make_opportunity(items, customer): diff --git a/erpnext/setup/setup_wizard/setup_wizard.py b/erpnext/setup/setup_wizard/setup_wizard.py index 4dec3d75f79..40d11e5bdc4 100644 --- a/erpnext/setup/setup_wizard/setup_wizard.py +++ b/erpnext/setup/setup_wizard/setup_wizard.py @@ -15,6 +15,7 @@ from .sample_data import make_sample_data from erpnext.accounts.doctype.account.account import RootNotEditable from frappe.core.doctype.communication.comment import add_info_comment from erpnext.setup.setup_wizard.domainify import setup_domain +from erpnext.setup.doctype.company.company import install_country_fixtures def setup_complete(args=None): if frappe.db.sql("select name from tabCompany"): @@ -25,24 +26,16 @@ def setup_complete(args=None): create_price_lists(args) create_fiscal_year_and_company(args) create_sales_tax(args) - create_users(args) + create_employee_for_self(args) set_defaults(args) create_territories() create_feed_and_todo() create_email_digest() create_letter_head(args) - create_taxes(args) - create_items(args) - create_customers(args) - create_suppliers(args) if args.get('domain').lower() == 'education': create_academic_year() create_academic_term() - create_program(args) - create_course(args) - create_instructor(args) - create_room(args) if args.get('setup_website'): website_maker(args) @@ -58,16 +51,19 @@ def setup_complete(args=None): frappe.db.commit() frappe.clear_cache() - if args.get("add_sample_data"): - try: - make_sample_data(args) - frappe.clear_cache() - except: - # clear message - if frappe.message_log: - frappe.message_log.pop() + try: + make_sample_data(args.get('domain')) + frappe.clear_cache() + except: + # clear message + if frappe.message_log: + frappe.message_log.pop() - pass + pass + +def setup_success(args=None): + company = frappe.db.sql("select name from tabCompany", as_dict=True)[0]["name"] + install_country_fixtures(company) def create_fiscal_year_and_company(args): if (args.get('fy_start_date')): @@ -91,8 +87,7 @@ def create_fiscal_year_and_company(args): 'country': args.get('country'), 'create_chart_of_accounts_based_on': 'Standard Template', 'chart_of_accounts': args.get('chart_of_accounts'), - 'domain': args.get('domain'), - 'sales_target': args.get('sales_target') + 'domain': args.get('domain') }).insert() #Enable shopping cart @@ -259,22 +254,7 @@ def create_sales_tax(args): tax_data.get('account_name'), tax_data.get('tax_rate'), sales_tax) -def get_country_wise_tax(country): - data = {} - with open (os.path.join(os.path.dirname(__file__), "data", "country_wise_tax.json")) as countrywise_tax: - data = json.load(countrywise_tax).get(country) - - return data - -def create_taxes(args): - for i in xrange(1,6): - if args.get("tax_" + str(i)): - # replace % in case someone also enters the % symbol - tax_rate = cstr(args.get("tax_rate_" + str(i)) or "").replace("%", "") - account_name = args.get("tax_" + str(i)) - - make_tax_account_and_template(args.get("company_name") , account_name, tax_rate) - +# Tax utils start def make_tax_account_and_template(company, account_name, tax_rate, template_name=None): try: if not isinstance(account_name, (list, tuple)): @@ -292,15 +272,6 @@ def make_tax_account_and_template(company, account_name, tax_rate, template_name except RootNotEditable: pass -def get_tax_account_group(company): - tax_group = frappe.db.get_value("Account", - {"account_name": "Duties and Taxes", "is_group": 1, "company": company}) - if not tax_group: - tax_group = frappe.db.get_value("Account", {"is_group": 1, "root_type": "Liability", - "account_type": "Tax", "company": company}) - - return tax_group - def make_tax_account(company, account_name, tax_rate): tax_group = get_tax_account_group(company) if tax_group: @@ -345,115 +316,23 @@ def make_sales_and_purchase_tax_templates(accounts, template_name=None): doc = frappe.get_doc(purchase_tax_template) doc.insert(ignore_permissions=True) -def create_items(args): - for i in xrange(1,6): - item = args.get("item_" + str(i)) - if item: - item_group = _(args.get("item_group_" + str(i))) - is_sales_item = args.get("is_sales_item_" + str(i)) - is_purchase_item = args.get("is_purchase_item_" + str(i)) - is_stock_item = item_group!=_("Services") - default_warehouse = "" - if is_stock_item: - default_warehouse = frappe.db.get_value("Warehouse", filters={ - "warehouse_name": _("Finished Goods") if is_sales_item else _("Stores"), - "company": args.get("company_name") - }) +def get_tax_account_group(company): + tax_group = frappe.db.get_value("Account", + {"account_name": "Duties and Taxes", "is_group": 1, "company": company}) + if not tax_group: + tax_group = frappe.db.get_value("Account", {"is_group": 1, "root_type": "Liability", + "account_type": "Tax", "company": company}) - try: - frappe.get_doc({ - "doctype":"Item", - "item_code": item, - "item_name": item, - "description": item, - "show_in_website": 1, - "is_sales_item": is_sales_item, - "is_purchase_item": is_purchase_item, - "is_stock_item": is_stock_item and 1 or 0, - "item_group": item_group, - "stock_uom": _(args.get("item_uom_" + str(i))), - "default_warehouse": default_warehouse - }).insert() + return tax_group - if args.get("item_img_" + str(i)): - item_image = args.get("item_img_" + str(i)).split(",") - if len(item_image)==3: - filename, filetype, content = item_image - fileurl = save_file(filename, content, "Item", item, decode=True).file_url - frappe.db.set_value("Item", item, "image", fileurl) +# Tax utils end - if args.get("item_price_" + str(i)): - item_price = flt(args.get("item_price_" + str(i))) +def get_country_wise_tax(country): + data = {} + with open (os.path.join(os.path.dirname(__file__), "data", "country_wise_tax.json")) as countrywise_tax: + data = json.load(countrywise_tax).get(country) - if is_sales_item: - price_list_name = frappe.db.get_value("Price List", {"selling": 1}) - make_item_price(item, price_list_name, item_price) - - if is_purchase_item: - price_list_name = frappe.db.get_value("Price List", {"buying": 1}) - make_item_price(item, price_list_name, item_price) - - except frappe.NameError: - pass - -def make_item_price(item, price_list_name, item_price): - frappe.get_doc({ - "doctype": "Item Price", - "price_list": price_list_name, - "item_code": item, - "price_list_rate": item_price - }).insert() - - -def create_customers(args): - for i in xrange(1,6): - customer = args.get("customer_" + str(i)) - if customer: - try: - doc = frappe.get_doc({ - "doctype":"Customer", - "customer_name": customer, - "customer_type": "Company", - "customer_group": _("Commercial"), - "territory": args.get("country"), - "company": args.get("company_name") - }).insert() - - if args.get("customer_contact_" + str(i)): - create_contact(args.get("customer_contact_" + str(i)), - "Customer", doc.name) - except frappe.NameError: - pass - -def create_suppliers(args): - for i in xrange(1,6): - supplier = args.get("supplier_" + str(i)) - if supplier: - try: - doc = frappe.get_doc({ - "doctype":"Supplier", - "supplier_name": supplier, - "supplier_type": _("Local"), - "company": args.get("company_name") - }).insert() - - if args.get("supplier_contact_" + str(i)): - create_contact(args.get("supplier_contact_" + str(i)), - "Supplier", doc.name) - except frappe.NameError: - pass - -def create_contact(contact, party_type, party): - """Create contact based on given contact name""" - contact = contact .split(" ") - - contact = frappe.get_doc({ - "doctype":"Contact", - "first_name":contact[0], - "last_name": len(contact) > 1 and contact[1] or "" - }) - contact.append('links', dict(link_doctype=party_type, link_name=party)) - contact.insert() + return data def create_letter_head(args): if args.get("attach_letterhead"): @@ -497,7 +376,7 @@ def login_as_first_user(args): if args.get("email") and hasattr(frappe.local, "login_manager"): frappe.local.login_manager.login_as(args.get("email")) -def create_users(args): +def create_employee_for_self(args): if frappe.session.user == 'Administrator': return @@ -512,50 +391,7 @@ def create_users(args): emp.flags.ignore_mandatory = True emp.insert(ignore_permissions = True) - for i in xrange(1,5): - email = args.get("user_email_" + str(i)) - fullname = args.get("user_fullname_" + str(i)) - if email: - if not fullname: - fullname = email.split("@")[0] - - parts = fullname.split(" ", 1) - - user = frappe.get_doc({ - "doctype": "User", - "email": email, - "first_name": parts[0], - "last_name": parts[1] if len(parts) > 1 else "", - "enabled": 1, - "user_type": "System User" - }) - - # default roles - user.append_roles("Projects User", "Stock User", "Support Team") - - if args.get("user_sales_" + str(i)): - user.append_roles("Sales User", "Sales Manager", "Accounts User") - if args.get("user_purchaser_" + str(i)): - user.append_roles("Purchase User", "Purchase Manager", "Accounts User") - if args.get("user_accountant_" + str(i)): - user.append_roles("Accounts Manager", "Accounts User") - - user.flags.delay_emails = True - - if not frappe.db.get_value("User", email): - user.insert(ignore_permissions=True) - - # create employee - emp = frappe.get_doc({ - "doctype": "Employee", - "employee_name": fullname, - "user_id": email, - "status": "Active", - "company": args.get("company_name") - }) - emp.flags.ignore_mandatory = True - emp.insert(ignore_permissions = True) - +# Schools def create_academic_term(): at = ["Semester 1", "Semester 2", "Semester 3"] ay = ["2013-14", "2014-15", "2015-16", "2016-17", "2017-18"] @@ -578,46 +414,3 @@ def create_academic_year(): academic_year.save() except frappe.DuplicateEntryError: pass - -def create_program(args): - for i in xrange(1,6): - if args.get("program_" + str(i)): - program = frappe.new_doc("Program") - program.program_code = args.get("program_" + str(i)) - program.program_name = args.get("program_" + str(i)) - try: - program.save() - except frappe.DuplicateEntryError: - pass - -def create_course(args): - for i in xrange(1,6): - if args.get("course_" + str(i)): - course = frappe.new_doc("Course") - course.course_code = args.get("course_" + str(i)) - course.course_name = args.get("course_" + str(i)) - try: - course.save() - except frappe.DuplicateEntryError: - pass - -def create_instructor(args): - for i in xrange(1,6): - if args.get("instructor_" + str(i)): - instructor = frappe.new_doc("Instructor") - instructor.instructor_name = args.get("instructor_" + str(i)) - try: - instructor.save() - except frappe.DuplicateEntryError: - pass - -def create_room(args): - for i in xrange(1,6): - if args.get("room_" + str(i)): - room = frappe.new_doc("Room") - room.room_name = args.get("room_" + str(i)) - room.seating_capacity = args.get("room_capacity_" + str(i)) - try: - room.save() - except frappe.DuplicateEntryError: - pass diff --git a/erpnext/setup/setup_wizard/test_setup_wizard.py b/erpnext/setup/setup_wizard/test_setup_wizard.py new file mode 100644 index 00000000000..2db63c1b44b --- /dev/null +++ b/erpnext/setup/setup_wizard/test_setup_wizard.py @@ -0,0 +1,60 @@ +# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals + +import frappe, time +from frappe.utils.selenium_testdriver import TestDriver + +def run_setup_wizard_test(): + driver = TestDriver() + frappe.db.set_default('in_selenium', '1') + frappe.db.commit() + + driver.login('#page-setup-wizard') + print('Running Setup Wizard Test...') + + # Language slide + driver.set_select("language", "English (United States)") + driver.wait_for_ajax(True) + driver.wait_till_clickable(".next-btn").click() + + # Region slide + driver.wait_for_ajax(True) + driver.set_select("country", "India") + driver.wait_for_ajax(True) + driver.wait_till_clickable(".next-btn").click() + + # Profile slide + driver.set_field("full_name", "Great Tester") + driver.set_field("email", "great@example.com") + driver.set_field("password", "test") + driver.wait_till_clickable(".next-btn").click() + + # Brand slide + driver.set_select("domain", "Manufacturing") + driver.wait_till_clickable(".next-btn").click() + + # Org slide + driver.set_field("company_name", "For Testing") + driver.wait_till_clickable(".next-btn").click() + driver.set_field("company_tagline", "Just for GST") + driver.set_field("bank_account", "HDFC") + driver.wait_till_clickable(".complete-btn").click() + + # Wait for desktop + driver.wait_for('#page-desktop', timeout=600) + + console = driver.get_console() + if frappe.flags.tests_verbose: + for line in console: + print(line) + print('-' * 40) + time.sleep(1) + + frappe.db.set_default('in_selenium', None) + frappe.db.commit() + + driver.close() + + return True \ No newline at end of file diff --git a/erpnext/setup/setup_wizard/utils.py b/erpnext/setup/setup_wizard/utils.py index dc4abd4f502..d821a129899 100644 --- a/erpnext/setup/setup_wizard/utils.py +++ b/erpnext/setup/setup_wizard/utils.py @@ -10,6 +10,3 @@ def complete(): #setup_wizard.create_sales_tax(data) setup_complete(data) - - - diff --git a/erpnext/startup/notifications.py b/erpnext/startup/notifications.py index 5b0ce391a13..b32f41e0c04 100644 --- a/erpnext/startup/notifications.py +++ b/erpnext/startup/notifications.py @@ -60,8 +60,8 @@ def get_notification_config(): "targets": { "Company": { - "filters" : { "sales_target": ( ">", 0 ) }, - "target_field" : "sales_target", + "filters" : { "monthly_sales_target": ( ">", 0 ) }, + "target_field" : "monthly_sales_target", "value_field" : "total_monthly_sales" } } diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.js b/erpnext/stock/doctype/delivery_note/delivery_note.js index 46d536dacea..e0ee3708f43 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.js +++ b/erpnext/stock/doctype/delivery_note/delivery_note.js @@ -166,6 +166,12 @@ erpnext.stock.DeliveryNoteController = erpnext.selling.SellingController.extend( __("Status")) } erpnext.stock.delivery_note.set_print_hide(doc, dt, dn); + + if(doc.docstatus==1 && !doc.subscription) { + cur_frm.add_custom_button(__('Subscription'), function() { + erpnext.utils.make_subscription(doc.doctype, doc.name) + }, __("Make")) + } }, make_sales_invoice: function() { diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json index 41f8b8493e5..980f79b2a2d 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.json +++ b/erpnext/stock/doctype/delivery_note/delivery_note.json @@ -3251,6 +3251,67 @@ "set_only_once": 0, "unique": 0 }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "subscription_section", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Subscription Section", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "subscription", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Subscription", + "length": 0, + "no_copy": 1, + "options": "Subscription", + "permlevel": 0, + "precision": "", + "print_hide": 1, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, { "allow_bulk_edit": 0, "allow_on_submit": 0, @@ -3487,7 +3548,7 @@ "istable": 0, "max_attachments": 0, "menu_index": 0, - "modified": "2017-08-23 13:25:34.182268", + "modified": "2017-08-31 11:21:59.084183", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note", diff --git a/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py b/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py index c296d8cb9f9..2e150f70d00 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py @@ -5,7 +5,8 @@ def get_data(): 'fieldname': 'delivery_note', 'non_standard_fieldnames': { 'Stock Entry': 'delivery_note_no', - 'Quality Inspection': 'reference_name' + 'Quality Inspection': 'reference_name', + 'Subscription': 'reference_document', }, 'internal_links': { 'Sales Order': ['items', 'against_sales_order'], @@ -23,5 +24,9 @@ def get_data(): 'label': _('Returns'), 'items': ['Stock Entry'] }, + { + 'label': _('Subscription'), + 'items': ['Subscription'] + }, ] } \ No newline at end of file diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.js b/erpnext/stock/doctype/delivery_note/test_delivery_note.js index 482f8929974..bbc97b8d402 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.js +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.js @@ -34,4 +34,3 @@ QUnit.test("test delivery note", function(assert) { ]); }); - diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index 5dc97f623b3..f2ea1d88bca 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -488,6 +488,8 @@ class Item(WebsiteGenerator): def validate_warehouse_for_reorder(self): warehouse = [] for i in self.get("reorder_levels"): + if not i.warehouse_group: + i.warehouse_group = i.warehouse if i.get("warehouse") and i.get("warehouse") not in warehouse: warehouse += [i.get("warehouse")] else: diff --git a/erpnext/stock/doctype/material_request/material_request.json b/erpnext/stock/doctype/material_request/material_request.json index ed647d6c8be..87cde0d3e2f 100644 --- a/erpnext/stock/doctype/material_request/material_request.json +++ b/erpnext/stock/doctype/material_request/material_request.json @@ -86,7 +86,7 @@ "ignore_xss_filter": 0, "in_filter": 0, "in_global_search": 0, - "in_list_view": 0, + "in_list_view": 1, "in_standard_filter": 1, "label": "Type", "length": 0, @@ -144,7 +144,7 @@ "ignore_xss_filter": 0, "in_filter": 0, "in_global_search": 0, - "in_list_view": 0, + "in_list_view": 1, "in_standard_filter": 0, "label": "Series", "length": 0, @@ -211,7 +211,7 @@ "ignore_xss_filter": 0, "in_filter": 0, "in_global_search": 0, - "in_list_view": 0, + "in_list_view": 1, "in_standard_filter": 1, "label": "Company", "length": 0, @@ -369,7 +369,7 @@ "ignore_xss_filter": 0, "in_filter": 0, "in_global_search": 0, - "in_list_view": 0, + "in_list_view": 1, "in_standard_filter": 0, "label": "Transaction Date", "length": 0, @@ -686,7 +686,7 @@ "istable": 0, "max_attachments": 0, "menu_index": 0, - "modified": "2017-06-13 14:29:18.032657", + "modified": "2017-07-26 19:43:31.823549", "modified_by": "Administrator", "module": "Stock", "name": "Material Request", diff --git a/erpnext/stock/doctype/material_request/test_material_request.js b/erpnext/stock/doctype/material_request/test_material_request.js new file mode 100644 index 00000000000..793cad0f3b6 --- /dev/null +++ b/erpnext/stock/doctype/material_request/test_material_request.js @@ -0,0 +1,23 @@ +/* eslint-disable */ +// rename this file from _test_[name] to test_[name] to activate +// and remove above this line + +QUnit.test("test: Material Request", function (assert) { + let done = assert.async(); + + // number of asserts + assert.expect(1); + + frappe.run_serially('Material Request', [ + // insert a new Material Request + () => frappe.tests.make([ + // values to be set + {key: 'value'} + ]), + () => { + assert.equal(cur_frm.doc.key, 'value'); + }, + () => done() + ]); + +}); diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js index 19cc44a1191..4447fb8cfab 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js @@ -48,7 +48,7 @@ frappe.ui.form.on("Purchase Receipt", { toggle_display_account_head: function(frm) { var enabled = erpnext.is_perpetual_inventory_enabled(frm.doc.company) frm.fields_dict["items"].grid.set_column_disp(["cost_center"], enabled); - } + }, }); erpnext.stock.PurchaseReceiptController = erpnext.buying.BuyingController.extend({ @@ -98,6 +98,13 @@ erpnext.stock.PurchaseReceiptController = erpnext.buying.BuyingController.extend if(flt(this.frm.doc.per_billed) < 100) { cur_frm.add_custom_button(__('Invoice'), this.make_purchase_invoice, __("Make")); } + + if(!this.frm.doc.subscription) { + cur_frm.add_custom_button(__('Subscription'), function() { + erpnext.utils.make_subscription(me.frm.doc.doctype, me.frm.doc.name) + }, __("Make")) + } + cur_frm.page.set_inner_btn_group_as_primary(__("Make")); } } diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json index adcea6d76cb..7140dbd8bda 100755 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json @@ -2611,6 +2611,67 @@ "set_only_once": 0, "unique": 0 }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "subscription_detail", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Subscription Detail", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "subscription", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Subscription", + "length": 0, + "no_copy": 1, + "options": "Subscription", + "permlevel": 0, + "precision": "", + "print_hide": 1, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, { "allow_bulk_edit": 0, "allow_on_submit": 0, diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt_dashboard.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt_dashboard.py index 9722d8791f6..9ade1afd8a0 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt_dashboard.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt_dashboard.py @@ -5,7 +5,8 @@ def get_data(): 'fieldname': 'purchase_receipt_no', 'non_standard_fieldnames': { 'Purchase Invoice': 'purchase_receipt', - 'Landed Cost Voucher': 'receipt_document' + 'Landed Cost Voucher': 'receipt_document', + 'Subscription': 'reference_document' }, 'internal_links': { 'Purchase Order': ['items', 'purchase_order'], @@ -25,5 +26,9 @@ def get_data(): 'label': _('Returns'), 'items': ['Stock Entry'] }, + { + 'label': _('Subscription'), + 'items': ['Subscription'] + }, ] } \ No newline at end of file diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 80ef70805a2..8d084dcb8cf 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -79,7 +79,7 @@ def get_item_details(args): and out.warehouse and out.stock_qty > 0: if out.has_serial_no: - out.serial_no = get_serial_no(out) + out.serial_no = get_serial_no(out, args.serial_no) if out.has_batch_no and not args.get("batch_no"): out.batch_no = get_batch_no(out.item_code, out.warehouse, out.qty) @@ -554,7 +554,8 @@ def get_gross_profit(out): return out @frappe.whitelist() -def get_serial_no(args): +def get_serial_no(args, serial_nos=None): + serial_no = None if isinstance(args, basestring): args = json.loads(args) args = frappe._dict(args) @@ -568,4 +569,9 @@ def get_serial_no(args): args = json.dumps({"item_code": args.get('item_code'),"warehouse": args.get('warehouse'),"stock_qty": args.get('stock_qty')}) args = process_args(args) serial_no = get_serial_nos_by_fifo(args) - return serial_no + + if not serial_no and serial_nos: + # For POS + serial_no = serial_nos + + return serial_no diff --git a/erpnext/subscription/__init__.py b/erpnext/subscription/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/subscription/doctype/__init__.py b/erpnext/subscription/doctype/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/subscription/doctype/subscription/__init__.py b/erpnext/subscription/doctype/subscription/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/subscription/doctype/subscription/subscription.js b/erpnext/subscription/doctype/subscription/subscription.js new file mode 100644 index 00000000000..75e1473b000 --- /dev/null +++ b/erpnext/subscription/doctype/subscription/subscription.js @@ -0,0 +1,36 @@ +// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Subscription', { + setup: function(frm) { + frm.fields_dict['reference_document'].get_query = function() { + return { + filters: { + "docstatus": 1 + } + }; + }; + + frm.fields_dict['print_format'].get_query = function() { + return { + filters: { + "doc_type": frm.doc.reference_doctype + } + }; + }; + }, + + refresh: function(frm) { + if(frm.doc.docstatus == 1) { + let label = __('View {0}', [frm.doc.reference_doctype]); + frm.add_custom_button(__(label), + function() { + frappe.route_options = { + "subscription": frm.doc.name, + }; + frappe.set_route("List", frm.doc.reference_doctype); + } + ); + } + } +}); \ No newline at end of file diff --git a/erpnext/subscription/doctype/subscription/subscription.json b/erpnext/subscription/doctype/subscription/subscription.json new file mode 100644 index 00000000000..6cfee1e44f7 --- /dev/null +++ b/erpnext/subscription/doctype/subscription/subscription.json @@ -0,0 +1,731 @@ +{ + "allow_copy": 0, + "allow_guest_to_view": 0, + "allow_import": 1, + "allow_rename": 1, + "autoname": "naming_series:", + "beta": 0, + "creation": "2017-07-18 17:50:43.967266", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "", + "editable_grid": 1, + "engine": "InnoDB", + "fields": [ + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "section_break_1", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "naming_series", + "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Series", + "length": 0, + "no_copy": 0, + "options": "SUB-", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "reference_doctype", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Reference Doctype", + "length": 0, + "no_copy": 0, + "options": "DocType", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "reference_document", + "fieldtype": "Dynamic Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Reference Document", + "length": 0, + "no_copy": 1, + "options": "reference_doctype", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 1, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "disabled", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Disabled", + "length": 0, + "no_copy": 1, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "submit_on_creation", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Submit on Creation", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "column_break_5", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "start_date", + "fieldtype": "Date", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Start Date", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 1, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "end_date", + "fieldtype": "Date", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "End Date", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 1, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "next_schedule_date", + "fieldtype": "Date", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Next Schedule Date", + "length": 0, + "no_copy": 1, + "permlevel": 0, + "precision": "", + "print_hide": 1, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "frequency_detail", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "frequency", + "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Frequency", + "length": 0, + "no_copy": 0, + "options": "\nDaily\nWeekly\nMonthly\nQuarterly\nHalf-yearly\nYearly", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "column_break_12", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 1, + "bold": 0, + "collapsible": 0, + "columns": 0, + "depends_on": "eval: in_list([\"Monthly\", \"Quarterly\", \"Yearly\"], doc.frequency)", + "fieldname": "repeat_on_day", + "fieldtype": "Int", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Repeat on Day", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "notification", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Notification", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "notify_by_email", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Notify by Email", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "column_break_17", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "depends_on": "notify_by_email", + "fieldname": "recipients", + "fieldtype": "Small Text", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Recipients", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "depends_on": "notify_by_email", + "fieldname": "print_format", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Print Format", + "length": 0, + "no_copy": 0, + "options": "Print Format", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 1, + "columns": 0, + "fieldname": "section_break_16", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "Draft", + "fieldname": "status", + "fieldtype": "Select", + "hidden": 1, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Status", + "length": 0, + "no_copy": 0, + "options": "\nDraft\nSubmitted\nCancelled\nCompleted", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "amended_from", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Amended From", + "length": 0, + "no_copy": 1, + "options": "Subscription", + "permlevel": 0, + "print_hide": 1, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + } + ], + "has_web_view": 0, + "hide_heading": 0, + "hide_toolbar": 0, + "idx": 0, + "image_view": 0, + "in_create": 0, + "is_submittable": 1, + "issingle": 0, + "istable": 0, + "max_attachments": 0, + "modified": "2017-08-29 15:45:16.157643", + "modified_by": "Administrator", + "module": "Subscription", + "name": "Subscription", + "name_case": "", + "owner": "Administrator", + "permissions": [ + { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "if_owner": 0, + "import": 0, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "set_user_permissions": 0, + "share": 1, + "submit": 1, + "write": 1 + } + ], + "quick_entry": 0, + "read_only": 0, + "read_only_onload": 0, + "search_fields": "reference_document", + "show_name_in_global_search": 0, + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "reference_document", + "track_changes": 1, + "track_seen": 0 +} \ No newline at end of file diff --git a/erpnext/subscription/doctype/subscription/subscription.py b/erpnext/subscription/doctype/subscription/subscription.py new file mode 100644 index 00000000000..be36211ec27 --- /dev/null +++ b/erpnext/subscription/doctype/subscription/subscription.py @@ -0,0 +1,207 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +import calendar +from frappe import _ +from frappe.desk.form import assign_to +from dateutil.relativedelta import relativedelta +from frappe.utils.user import get_system_managers +from frappe.utils import cstr, getdate, split_emails, add_days, today +from frappe.model.document import Document + +month_map = {'Monthly': 1, 'Quarterly': 3, 'Half-yearly': 6, 'Yearly': 12} +class Subscription(Document): + def validate(self): + self.update_status() + self.validate_dates() + self.validate_next_schedule_date() + self.validate_email_id() + + def before_submit(self): + self.set_next_schedule_date() + + def on_submit(self): + self.update_subscription_id() + + def on_update_after_submit(self): + self.validate_dates() + self.set_next_schedule_date() + + def validate_dates(self): + if self.end_date and getdate(self.start_date) > getdate(self.end_date): + frappe.throw(_("End date must be greater than start date")) + + def validate_next_schedule_date(self): + if self.repeat_on_day and self.next_schedule_date: + next_date = getdate(self.next_schedule_date) + if next_date.day != self.repeat_on_day: + # if the repeat day is the last day of the month (31) + # and the current month does not have as many days, + # then the last day of the current month is a valid date + lastday = calendar.monthrange(next_date.year, next_date.month)[1] + if self.repeat_on_day < lastday: + + # the specified day of the month is not same as the day specified + # or the last day of the month + frappe.throw(_("Next Date's day and Repeat on Day of Month must be equal")) + + def validate_email_id(self): + if self.notify_by_email: + if self.recipients: + email_list = split_emails(self.recipients.replace("\n", "")) + + from frappe.utils import validate_email_add + for email in email_list: + if not validate_email_add(email): + frappe.throw(_("{0} is an invalid email address in 'Recipients'").format(email)) + else: + frappe.throw(_("'Recipients' not specified")) + + def set_next_schedule_date(self): + self.next_schedule_date = get_next_schedule_date(self.start_date, + self.frequency, self.repeat_on_day) + + def update_subscription_id(self): + doc = frappe.get_doc(self.reference_doctype, self.reference_document) + if not doc.meta.get_field('subscription'): + frappe.throw(_("Add custom field Subscription Id in the doctype {0}").format(self.reference_doctype)) + + doc.db_set('subscription', self.name) + + def update_status(self): + self.status = { + '0': 'Draft', + '1': 'Submitted', + '2': 'Cancelled' + }[cstr(self.docstatus or 0)] + +def get_next_schedule_date(start_date, frequency, repeat_on_day): + mcount = month_map.get(frequency) + if mcount: + next_date = get_next_date(start_date, mcount, repeat_on_day) + else: + days = 7 if frequency == 'Weekly' else 1 + next_date = add_days(start_date, days) + return next_date + +def make_subscription_entry(date=None): + date = date or today() + for data in get_subscription_entries(date): + schedule_date = getdate(data.next_schedule_date) + while schedule_date <= getdate(today()): + create_documents(data, schedule_date) + + schedule_date = get_next_schedule_date(schedule_date, + data.frequency, data.repeat_on_day) + + if schedule_date: + frappe.db.set_value('Subscription', data.name, 'next_schedule_date', schedule_date) + +def get_subscription_entries(date): + return frappe.db.sql(""" select * from `tabSubscription` + where docstatus = 1 and next_schedule_date <=%s + and reference_document is not null and reference_document != '' + and next_schedule_date <= ifnull(end_date, '2199-12-31') + and ifnull(disabled, 0) = 0""", (date), as_dict=1) + +def create_documents(data, schedule_date): + try: + doc = make_new_document(data, schedule_date) + if data.notify_by_email: + send_notification(doc, data.print_format, data.recipients) + + frappe.db.commit() + except Exception: + frappe.db.rollback() + frappe.db.begin() + frappe.log_error(frappe.get_traceback()) + frappe.db.commit() + if data.reference_document and not frappe.flags.in_test: + notify_error_to_user(data) + +def notify_error_to_user(data): + party = '' + party_type = '' + + if data.reference_doctype in ['Sales Order', 'Sales Invoice', 'Delivery Note']: + party_type = 'customer' + elif data.reference_doctype in ['Purchase Order', 'Purchase Invoice', 'Purchase Receipt']: + party_type = 'supplier' + + if party_type: + party = frappe.db.get_value(data.reference_doctype, data.reference_document, party_type) + + notify_errors(data.reference_document, data.reference_doctype, party, data.owner) + +def make_new_document(args, schedule_date): + doc = frappe.get_doc(args.reference_doctype, args.reference_document) + new_doc = frappe.copy_doc(doc, ignore_no_copy=False) + update_doc(new_doc, doc , args, schedule_date) + new_doc.insert(ignore_permissions=True) + + if args.submit_on_creation: + new_doc.submit() + + return new_doc + +def update_doc(new_document, reference_doc, args, schedule_date): + new_document.docstatus = 0 + if new_document.meta.get_field('set_posting_time'): + new_document.set('set_posting_time', 1) + + if new_document.meta.get_field('subscription'): + new_document.set('subscription', args.name) + + new_document.run_method("on_recurring", reference_doc=reference_doc, subscription_doc=args) + for data in new_document.meta.fields: + if data.fieldtype == 'Date' and data.reqd: + new_document.set(data.fieldname, schedule_date) + +def get_next_date(dt, mcount, day=None): + dt = getdate(dt) + dt += relativedelta(months=mcount, day=day) + + return dt + +def send_notification(new_rv, print_format='Standard', recipients=None): + """Notify concerned persons about recurring document generation""" + recipients = recipients or new_rv.notification_email_address + print_format = print_format or new_rv.recurring_print_format + + frappe.sendmail(recipients, + subject= _("New {0}: #{1}").format(new_rv.doctype, new_rv.name), + message = _("Please find attached {0} #{1}").format(new_rv.doctype, new_rv.name), + attachments = [frappe.attach_print(new_rv.doctype, new_rv.name, file_name=new_rv.name, print_format=print_format)]) + +def notify_errors(doc, doctype, party, owner): + recipients = get_system_managers(only_name=True) + frappe.sendmail(recipients + [frappe.db.get_value("User", owner, "email")], + subject="[Urgent] Error while creating recurring %s for %s" % (doctype, doc), + message = frappe.get_template("templates/emails/recurring_document_failed.html").render({ + "type": doctype, + "name": doc, + "party": party or "" + })) + + assign_task_to_owner(doc, doctype, "Recurring Invoice Failed", recipients) + +def assign_task_to_owner(doc, doctype, msg, users): + for d in users: + args = { + 'assign_to' : d, + 'doctype' : doctype, + 'name' : doc, + 'description' : msg, + 'priority' : 'High' + } + assign_to.add(args) + +@frappe.whitelist() +def make_subscription(doctype, docname): + doc = frappe.new_doc('Subscription') + doc.reference_doctype = doctype + doc.reference_document = docname + return doc diff --git a/erpnext/subscription/doctype/subscription/subscription_list.js b/erpnext/subscription/doctype/subscription/subscription_list.js new file mode 100644 index 00000000000..6a33638b391 --- /dev/null +++ b/erpnext/subscription/doctype/subscription/subscription_list.js @@ -0,0 +1,12 @@ +frappe.listview_settings['Subscription'] = { + add_fields: ["next_schedule_date"], + get_indicator: function(doc) { + if(doc.next_schedule_date >= frappe.datetime.get_today() ) { + return [__("Active"), "green"]; + } else if(doc.docstatus === 0) { + return [__("Draft"), "red", "docstatus,=,0"]; + } else { + return [__("Expired"), "darkgrey"]; + } + } +}; \ No newline at end of file diff --git a/erpnext/subscription/doctype/subscription/test_subscription.js b/erpnext/subscription/doctype/subscription/test_subscription.js new file mode 100644 index 00000000000..2872a2147fb --- /dev/null +++ b/erpnext/subscription/doctype/subscription/test_subscription.js @@ -0,0 +1,32 @@ +/* eslint-disable */ +// rename this file from _test_[name] to test_[name] to activate +// and remove above this line + +QUnit.test("test: Subscription", function (assert) { + assert.expect(4); + let done = assert.async(); + frappe.run_serially([ + // insert a new Subscription + () => { + return frappe.tests.make("Subscription", [ + {reference_doctype: 'Sales Invoice'}, + {reference_document: 'SINV-00004'}, + {start_date: frappe.datetime.month_start()}, + {end_date: frappe.datetime.month_end()}, + {frequency: 'Weekly'} + ]); + }, + () => cur_frm.savesubmit(), + () => frappe.timeout(1), + () => frappe.click_button('Yes'), + () => frappe.timeout(2), + () => { + assert.ok(cur_frm.doc.frequency.includes("Weekly"), "Set frequency Weekly"); + assert.ok(cur_frm.doc.reference_doctype.includes("Sales Invoice"), "Set base doctype Sales Invoice"); + assert.equal(cur_frm.doc.docstatus, 1, "Submitted subscription"); + assert.equal(cur_frm.doc.next_schedule_date, + frappe.datetime.add_days(frappe.datetime.get_today(), 7), "Set schedule date"); + }, + () => done() + ]); +}); diff --git a/erpnext/subscription/doctype/subscription/test_subscription.py b/erpnext/subscription/doctype/subscription/test_subscription.py new file mode 100644 index 00000000000..28f8be7257d --- /dev/null +++ b/erpnext/subscription/doctype/subscription/test_subscription.py @@ -0,0 +1,93 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +import frappe +import unittest +from frappe.utils import today, add_days, getdate +from erpnext.accounts.utils import get_fiscal_year +from erpnext.accounts.report.financial_statements import get_months +from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice +from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order +from erpnext.subscription.doctype.subscription.subscription import make_subscription_entry + +class TestSubscription(unittest.TestCase): + def test_daily_subscription(self): + qo = frappe.copy_doc(quotation_records[0]) + qo.submit() + + doc = make_subscription(reference_document=qo.name) + self.assertEquals(doc.next_schedule_date, today()) + make_subscription_entry() + frappe.db.commit() + + quotation = frappe.get_doc(doc.reference_doctype, doc.reference_document) + self.assertEquals(quotation.subscription, doc.name) + + new_quotation = frappe.db.get_value('Quotation', + {'subscription': doc.name, 'name': ('!=', quotation.name)}, 'name') + + new_quotation = frappe.get_doc('Quotation', new_quotation) + + for fieldname in ['customer', 'company', 'order_type', 'total', 'grand_total']: + self.assertEquals(quotation.get(fieldname), new_quotation.get(fieldname)) + + for fieldname in ['item_code', 'qty', 'rate', 'amount']: + self.assertEquals(quotation.items[0].get(fieldname), + new_quotation.items[0].get(fieldname)) + + def test_monthly_subscription_for_so(self): + current_fiscal_year = get_fiscal_year(today(), as_dict=True) + start_date = current_fiscal_year.year_start_date + end_date = current_fiscal_year.year_end_date + + for doctype in ['Sales Order', 'Sales Invoice']: + if doctype == 'Sales Invoice': + docname = create_sales_invoice(posting_date=start_date) + else: + docname = make_sales_order() + + self.monthly_subscription(doctype, docname.name, start_date, end_date) + + def monthly_subscription(self, doctype, docname, start_date, end_date): + doc = make_subscription(reference_doctype=doctype, frequency = 'Monthly', + reference_document = docname, start_date=start_date, end_date=end_date) + + doc.disabled = 1 + doc.save() + frappe.db.commit() + + make_subscription_entry() + docnames = frappe.get_all(doc.reference_doctype, {'subscription': doc.name}) + self.assertEquals(len(docnames), 1) + + doc = frappe.get_doc('Subscription', doc.name) + doc.disabled = 0 + doc.save() + + months = get_months(getdate(start_date), getdate(today())) + make_subscription_entry() + + docnames = frappe.get_all(doc.reference_doctype, {'subscription': doc.name}) + self.assertEquals(len(docnames), months) + +quotation_records = frappe.get_test_records('Quotation') + +def make_subscription(**args): + args = frappe._dict(args) + doc = frappe.get_doc({ + 'doctype': 'Subscription', + 'reference_doctype': args.reference_doctype or 'Quotation', + 'reference_document': args.reference_document or \ + frappe.db.get_value('Quotation', {'docstatus': 1}, 'name'), + 'frequency': args.frequency or 'Daily', + 'start_date': args.start_date or add_days(today(), -1), + 'end_date': args.end_date or add_days(today(), 1), + 'submit_on_creation': args.submit_on_creation or 0 + }).insert(ignore_permissions=True) + + if not args.do_not_submit: + doc.submit() + + return doc \ No newline at end of file diff --git a/erpnext/templates/emails/training_event.html b/erpnext/templates/emails/training_event.html new file mode 100644 index 00000000000..51c232d8e87 --- /dev/null +++ b/erpnext/templates/emails/training_event.html @@ -0,0 +1,21 @@ +

{{_("Training Event")}}

+

{{ message }}

+ +

{{_("Details")}}

+{{_("Event Name")}}: {{ name }} +
{{_("Event Location")}}: {{ location }} +
{{_("Start Time")}}: {{ start_time }} +
{{_("End Time")}}: {{ end_time }} +
{{_("Attendance")}}: {{ attendance }} + +

{{_("Update Response")}}

+{% if not self_study %} +

{{_("Please update your status for this training event")}}:

+
+
+{% else %} +

{{_("Please confirm once you have completed your training")}}:

+
+{% endif %} +

{{_("Thank you")}},
+{{ user_fullname }}

diff --git a/erpnext/templates/pages/demo.html b/erpnext/templates/pages/demo.html index f94a7c4591a..10743b82319 100644 --- a/erpnext/templates/pages/demo.html +++ b/erpnext/templates/pages/demo.html @@ -60,7 +60,7 @@ $(document).ready(function() {
-

Sign up for a Free ERPNext.com account here +

Start a free 30-day trial