From a57d13e1e9538e56b401d651c52d2b9c5b84250f Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 29 Jul 2021 19:46:17 +0530 Subject: [PATCH 01/65] fix: Multiple fixes in payment entry --- .../doctype/payment_entry/payment_entry.js | 4 ++-- .../doctype/payment_entry/payment_entry.py | 15 +++++++++++++-- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index 439b1edbce..d96bc271ef 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -533,8 +533,8 @@ frappe.ui.form.on('Payment Entry', { source_exchange_rate: function(frm) { if (frm.doc.paid_amount) { frm.set_value("base_paid_amount", flt(frm.doc.paid_amount) * flt(frm.doc.source_exchange_rate)); - if(!frm.set_paid_amount_based_on_received_amount && - (frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency)) { + // target exchange rate should always be same as source if both account currencies is same + if(frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency) { frm.set_value("target_exchange_rate", frm.doc.source_exchange_rate); frm.set_value("base_received_amount", frm.doc.base_paid_amount); } diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 46904f7c57..a131a810e1 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -55,8 +55,9 @@ class PaymentEntry(AccountsController): self.validate_mandatory() self.validate_reference_documents() self.set_tax_withholding() - self.apply_taxes() self.set_amounts() + self.validate_amounts() + self.apply_taxes() self.clear_unallocated_reference_document_rows() self.validate_payment_against_negative_invoice() self.validate_transaction_reference() @@ -236,7 +237,9 @@ class PaymentEntry(AccountsController): self.company_currency, self.posting_date) def set_target_exchange_rate(self, ref_doc=None): - if self.paid_to and not self.target_exchange_rate: + if self.paid_from_account_currency == self.paid_to_account_currency: + self.target_exchange_rate = self.source_exchange_rate + elif self.paid_to and not self.target_exchange_rate: if ref_doc: if self.paid_to_account_currency == ref_doc.currency: self.target_exchange_rate = ref_doc.get("exchange_rate") @@ -473,6 +476,14 @@ class PaymentEntry(AccountsController): self.set_unallocated_amount() self.set_difference_amount() + def validate_amounts(self): + self.validate_received_amount() + + def validate_received_amount(self): + if self.paid_from_account_currency == self.paid_to_account_currency: + if self.paid_amount != self.received_amount: + frappe.throw(_("Received Amount cannot be greater than Paid Amount")) + def set_received_amount(self): self.base_received_amount = self.base_paid_amount From c5276f3fd36764e5e90b0b068d0d9937a88e4447 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 1 Aug 2021 17:48:50 +0530 Subject: [PATCH 02/65] fix: Multiple fixes in payment entry --- .../accounts/doctype/payment_entry/payment_entry.py | 13 ++++++++----- erpnext/accounts/utils.py | 12 ++++++++---- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index a131a810e1..2231b47d9c 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -45,7 +45,7 @@ class PaymentEntry(AccountsController): self.party_account = self.paid_to self.party_account_currency = self.paid_to_account_currency - def validate(self): + def validate(self, on_reference_unlink=False): self.setup_party_account_field() self.set_missing_values() self.validate_payment_type() @@ -64,8 +64,9 @@ class PaymentEntry(AccountsController): self.set_title() self.set_remarks() self.validate_duplicate_entry() - self.validate_allocated_amount() - self.validate_paid_invoices() + if not on_reference_unlink: + self.validate_allocated_amount() + self.validate_paid_invoices() self.ensure_supplier_is_not_blocked() self.set_status() @@ -529,8 +530,10 @@ class PaymentEntry(AccountsController): base_total_allocated_amount += flt(flt(d.allocated_amount) * flt(d.exchange_rate), self.precision("base_paid_amount")) - self.total_allocated_amount = abs(total_allocated_amount) - self.base_total_allocated_amount = abs(base_total_allocated_amount) + # Do not use absolute values as only credit notes could be allocated + # and total allocated should be negative in that scenario + self.total_allocated_amount = total_allocated_amount + self.base_total_allocated_amount = base_total_allocated_amount def set_unallocated_amount(self): self.unallocated_amount = 0 diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 9272bc4fce..11e113d000 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -553,10 +553,14 @@ def remove_ref_doc_link_from_pe(ref_type, ref_no): and docstatus < 2""", (now(), frappe.session.user, ref_type, ref_no)) for pe in linked_pe: - pe_doc = frappe.get_doc("Payment Entry", pe) - pe_doc.set_total_allocated_amount() - pe_doc.set_unallocated_amount() - pe_doc.clear_unallocated_reference_document_rows() + try: + pe_doc = frappe.get_doc("Payment Entry", pe) + pe_doc.validate(on_reference_unlink=True) + except Exception as e: + msg = _("There were issues unlinking payment entry {0}.").format(pe_doc.name) + msg += '
' + msg += _("Please cancel payment entry manually first and then resubmit") + frappe.throw(msg, title=_("Payment Unlink Error")) frappe.db.sql("""update `tabPayment Entry` set total_allocated_amount=%s, base_total_allocated_amount=%s, unallocated_amount=%s, modified=%s, modified_by=%s From 188bba8feb64519de2c1ded0d5432308c397f04a Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 10 Aug 2021 14:04:31 +0530 Subject: [PATCH 03/65] fix: Validation for receivingfrom customer against negative outstanding --- .../accounts/doctype/payment_entry/payment_entry.py | 12 ++++++++---- erpnext/accounts/utils.py | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 2231b47d9c..66b46675dc 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -64,6 +64,7 @@ class PaymentEntry(AccountsController): self.set_title() self.set_remarks() self.validate_duplicate_entry() + self.validate_payment_type_with_outstanding() if not on_reference_unlink: self.validate_allocated_amount() self.validate_paid_invoices() @@ -120,6 +121,11 @@ class PaymentEntry(AccountsController): if not self.get(field): self.set(field, bank_data.account) + def validate_payment_type_with_outstanding(self): + total_outstanding = sum(d.allocated_amount for d in self.get('references')) + if total_outstanding < 0 and self.party_type == 'Customer' and self.payment_type == 'Receive': + frappe.throw(_("Cannot receive from customer against negative outstanding"), title=_("Incorrect Payment Type")) + def validate_allocated_amount(self): for d in self.get("references"): if (flt(d.allocated_amount))> 0: @@ -530,10 +536,8 @@ class PaymentEntry(AccountsController): base_total_allocated_amount += flt(flt(d.allocated_amount) * flt(d.exchange_rate), self.precision("base_paid_amount")) - # Do not use absolute values as only credit notes could be allocated - # and total allocated should be negative in that scenario - self.total_allocated_amount = total_allocated_amount - self.base_total_allocated_amount = base_total_allocated_amount + self.total_allocated_amount = abs(total_allocated_amount) + self.base_total_allocated_amount = abs(base_total_allocated_amount) def set_unallocated_amount(self): self.unallocated_amount = 0 diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 11e113d000..9d84d94074 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -559,7 +559,7 @@ def remove_ref_doc_link_from_pe(ref_type, ref_no): except Exception as e: msg = _("There were issues unlinking payment entry {0}.").format(pe_doc.name) msg += '
' - msg += _("Please cancel payment entry manually first and then resubmit") + msg += _("Please cancel payment entry manually first") frappe.throw(msg, title=_("Payment Unlink Error")) frappe.db.sql("""update `tabPayment Entry` set total_allocated_amount=%s, From b5162390e5881a110ca460ad4720de40ff7d4e1d Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 10 Aug 2021 14:52:24 +0530 Subject: [PATCH 04/65] fix: Only do specific validations on reference unlink --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 7 +++---- erpnext/accounts/utils.py | 5 ++++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 66b46675dc..16b4720b37 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -45,7 +45,7 @@ class PaymentEntry(AccountsController): self.party_account = self.paid_to self.party_account_currency = self.paid_to_account_currency - def validate(self, on_reference_unlink=False): + def validate(self): self.setup_party_account_field() self.set_missing_values() self.validate_payment_type() @@ -65,9 +65,8 @@ class PaymentEntry(AccountsController): self.set_remarks() self.validate_duplicate_entry() self.validate_payment_type_with_outstanding() - if not on_reference_unlink: - self.validate_allocated_amount() - self.validate_paid_invoices() + self.validate_allocated_amount() + self.validate_paid_invoices() self.ensure_supplier_is_not_blocked() self.set_status() diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 9d84d94074..0d3aa8f0ef 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -555,7 +555,10 @@ def remove_ref_doc_link_from_pe(ref_type, ref_no): for pe in linked_pe: try: pe_doc = frappe.get_doc("Payment Entry", pe) - pe_doc.validate(on_reference_unlink=True) + pe_doc.set_total_allocated_amount() + pe_doc.set_unallocated_amount() + pe_doc.clear_unallocated_reference_document_rows() + pe_doc.validate_payment_type_with_outstanding() except Exception as e: msg = _("There were issues unlinking payment entry {0}.").format(pe_doc.name) msg += '
' From 7141860c04fd1541f8d346a48e3e058f4ad443c9 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 10 Aug 2021 22:21:28 +0530 Subject: [PATCH 05/65] test: Update exchange rate in test cases --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 4 +++- .../accounts/doctype/payment_entry/test_payment_entry.py | 6 +++--- erpnext/accounts/utils.py | 6 +++--- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 16b4720b37..a991c0679b 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -58,6 +58,7 @@ class PaymentEntry(AccountsController): self.set_amounts() self.validate_amounts() self.apply_taxes() + self.set_amounts_after_tax() self.clear_unallocated_reference_document_rows() self.validate_payment_against_negative_invoice() self.validate_transaction_reference() @@ -477,7 +478,6 @@ class PaymentEntry(AccountsController): def set_amounts(self): self.set_received_amount() self.set_amounts_in_company_currency() - self.set_amounts_after_tax() self.set_total_allocated_amount() self.set_unallocated_amount() self.set_difference_amount() @@ -492,6 +492,8 @@ class PaymentEntry(AccountsController): def set_received_amount(self): self.base_received_amount = self.base_paid_amount + if self.paid_from_account_currency == self.paid_to_account_currency: + self.received_amount = self.paid_amount def set_amounts_after_tax(self): applicable_tax = 0 diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py index d1302f5ae7..420e8583ea 100644 --- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py @@ -107,7 +107,7 @@ class TestPaymentEntry(unittest.TestCase): 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 = 50 + pe.source_exchange_rate = 50 pe.insert() pe.submit() @@ -154,7 +154,7 @@ class TestPaymentEntry(unittest.TestCase): 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 = 50 + pe.source_exchange_rate = 50 pe.insert() pe.submit() @@ -463,7 +463,7 @@ class TestPaymentEntry(unittest.TestCase): 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.source_exchange_rate = 55 pe.append("deductions", { "account": "_Test Exchange Gain/Loss - _TC", diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 0d3aa8f0ef..73e2c2ea86 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -19,6 +19,7 @@ from erpnext.stock import get_warehouse_account_map class StockValueAndAccountBalanceOutOfSync(frappe.ValidationError): pass class FiscalYearError(frappe.ValidationError): pass +class PaymentEntryUnlinkError(frappe.ValidationError): pass @frappe.whitelist() def get_fiscal_year(date=None, fiscal_year=None, label="Date", verbose=1, company=None, as_dict=False): @@ -555,15 +556,14 @@ def remove_ref_doc_link_from_pe(ref_type, ref_no): for pe in linked_pe: try: pe_doc = frappe.get_doc("Payment Entry", pe) - pe_doc.set_total_allocated_amount() - pe_doc.set_unallocated_amount() + pe_doc.set_amounts() pe_doc.clear_unallocated_reference_document_rows() pe_doc.validate_payment_type_with_outstanding() except Exception as e: msg = _("There were issues unlinking payment entry {0}.").format(pe_doc.name) msg += '
' msg += _("Please cancel payment entry manually first") - frappe.throw(msg, title=_("Payment Unlink Error")) + frappe.throw(msg, exc=PaymentEntryUnlinkError, title=_("Payment Unlink Error")) frappe.db.sql("""update `tabPayment Entry` set total_allocated_amount=%s, base_total_allocated_amount=%s, unallocated_amount=%s, modified=%s, modified_by=%s From 02f44528f2b43c16fc8660b3e353533ba1bc3976 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 10 Aug 2021 22:21:52 +0530 Subject: [PATCH 06/65] test: Add test case for payment entry unlink --- .../sales_invoice/test_sales_invoice.py | 41 ++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index be20b18bea..623ab3f6f2 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -25,6 +25,7 @@ from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_invoice from erpnext.stock.utils import get_incoming_rate +from erpnext.accounts.utils import PaymentEntryUnlinkError class TestSalesInvoice(unittest.TestCase): def make(self): @@ -135,7 +136,7 @@ class TestSalesInvoice(unittest.TestCase): pe.paid_to_account_currency = si.currency pe.source_exchange_rate = 1 pe.target_exchange_rate = 1 - pe.paid_amount = si.grand_total + pe.paid_amount = si.outstanding_amount pe.insert() pe.submit() @@ -144,6 +145,44 @@ class TestSalesInvoice(unittest.TestCase): self.assertRaises(frappe.LinkExistsError, si.cancel) unlink_payment_on_cancel_of_invoice() + def test_payment_entry_unlink_against_standalone_credit_note(self): + from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry + si1 = create_sales_invoice(rate=1000) + si2 = create_sales_invoice(rate=300) + si3 = create_sales_invoice(qty=-1, rate=300, is_return=1) + + + pe = get_payment_entry("Sales Invoice", si1.name, bank_account="_Test Bank - _TC") + pe.append('references', { + 'reference_doctype': 'Sales Invoice', + 'reference_name': si2.name, + 'total_amount': si2.grand_total, + 'outstanding_amount': si2.outstanding_amount, + 'allocated_amount': si2.outstanding_amount + }) + + pe.append('references', { + 'reference_doctype': 'Sales Invoice', + 'reference_name': si3.name, + 'total_amount': si3.grand_total, + 'outstanding_amount': si3.outstanding_amount, + 'allocated_amount': si3.outstanding_amount + }) + + pe.reference_no = 'Test001' + pe.reference_date = nowdate() + pe.save() + pe.submit() + + unlink_payment_on_cancel_of_invoice() + si2.load_from_db() + si2.cancel() + + si1.load_from_db() + self.assertRaises(PaymentEntryUnlinkError, si1.cancel) + unlink_payment_on_cancel_of_invoice(0) + + def test_sales_invoice_calculation_export_currency(self): si = frappe.copy_doc(test_records[2]) si.currency = "USD" From a1f0cebda5f21bf4d1dba4214fd77e0dec210578 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 11 Aug 2021 11:36:49 +0530 Subject: [PATCH 07/65] fix: Do not update settings for test --- erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 623ab3f6f2..92bf746ad8 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -174,13 +174,11 @@ class TestSalesInvoice(unittest.TestCase): pe.save() pe.submit() - unlink_payment_on_cancel_of_invoice() si2.load_from_db() si2.cancel() si1.load_from_db() self.assertRaises(PaymentEntryUnlinkError, si1.cancel) - unlink_payment_on_cancel_of_invoice(0) def test_sales_invoice_calculation_export_currency(self): From 23e1d2b536b6da805eb305c70c6878374d07aaa6 Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Tue, 18 May 2021 17:11:47 +0530 Subject: [PATCH 08/65] refactor: update stock onboarding --- .../stock/module_onboarding/stock/stock.json | 16 +++++++++---- .../create_a_delivery_note.json | 20 ++++++++++++++++ .../create_a_purchase_receipt.json | 5 ++-- .../create_a_stock_entry.json | 5 ++-- .../create_a_supplier/create_a_supplier.json | 7 +++--- .../create_a_warehouse.json | 20 ++++++++++++++++ .../create_an_item/create_an_item.json | 22 ++++++++++++++++++ .../introduction_to_stock_entry.json | 5 ++-- .../setup_your_warehouse.json | 4 ++-- .../stock_settings/stock_settings.json | 5 ++-- .../view_stock_ledger/view_stock_ledger.json | 23 +++++++++++++++++++ .../view_warehouses/view_warehouses.json | 20 ++++++++++++++++ 12 files changed, 134 insertions(+), 18 deletions(-) create mode 100644 erpnext/stock/onboarding_step/create_a_delivery_note/create_a_delivery_note.json create mode 100644 erpnext/stock/onboarding_step/create_a_warehouse/create_a_warehouse.json create mode 100644 erpnext/stock/onboarding_step/create_an_item/create_an_item.json create mode 100644 erpnext/stock/onboarding_step/view_stock_ledger/view_stock_ledger.json create mode 100644 erpnext/stock/onboarding_step/view_warehouses/view_warehouses.json diff --git a/erpnext/stock/module_onboarding/stock/stock.json b/erpnext/stock/module_onboarding/stock/stock.json index 847464822b..6cc8ec6b05 100644 --- a/erpnext/stock/module_onboarding/stock/stock.json +++ b/erpnext/stock/module_onboarding/stock/stock.json @@ -19,20 +19,23 @@ "documentation_url": "https://docs.erpnext.com/docs/user/manual/en/stock", "idx": 0, "is_complete": 0, - "modified": "2020-10-14 14:54:42.741971", + "modified": "2021-05-18 17:01:36.137180", "modified_by": "Administrator", "module": "Stock", "name": "Stock", "owner": "Administrator", "steps": [ { - "step": "Setup your Warehouse" + "step": "Create an Item" }, { - "step": "Create a Product" + "step": "View Warehouses" }, { - "step": "Create a Supplier" + "step": "Create a Warehouse" + }, + { + "step": "Stock Settings" }, { "step": "Introduction to Stock Entry" @@ -40,11 +43,14 @@ { "step": "Create a Stock Entry" }, + { + "step": "View Stock Ledger" + }, { "step": "Create a Purchase Receipt" }, { - "step": "Stock Settings" + "step": "Create a Delivery Note" } ], "subtitle": "Inventory, Warehouses, Analysis, and more.", diff --git a/erpnext/stock/onboarding_step/create_a_delivery_note/create_a_delivery_note.json b/erpnext/stock/onboarding_step/create_a_delivery_note/create_a_delivery_note.json new file mode 100644 index 0000000000..7b702f013b --- /dev/null +++ b/erpnext/stock/onboarding_step/create_a_delivery_note/create_a_delivery_note.json @@ -0,0 +1,20 @@ +{ + "action": "Create Entry", + "creation": "2021-05-17 16:15:11.745214", + "description": "# Create a Delivery Note\nA [**Delivery Note**](https://docs.erpnext.com/docs/user/manual/en/stock/delivery-note) is made when a shipment is shipped from the company\u2019s *Warehouse* to the *Customer*. \n\nIn this step we will create a *Delivery Note*.", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2021-05-18 15:33:47.452298", + "modified_by": "Administrator", + "name": "Create a Delivery Note", + "owner": "Administrator", + "reference_document": "Delivery Note", + "show_form_tour": 1, + "show_full_form": 1, + "title": "Create a Delivery Note", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/stock/onboarding_step/create_a_purchase_receipt/create_a_purchase_receipt.json b/erpnext/stock/onboarding_step/create_a_purchase_receipt/create_a_purchase_receipt.json index 9012493f57..549fe26c25 100644 --- a/erpnext/stock/onboarding_step/create_a_purchase_receipt/create_a_purchase_receipt.json +++ b/erpnext/stock/onboarding_step/create_a_purchase_receipt/create_a_purchase_receipt.json @@ -1,18 +1,19 @@ { "action": "Create Entry", "creation": "2020-05-19 18:59:13.266713", + "description": "# Create a Purchase Entry\nA [**Purchase Receipt**](https://docs.erpnext.com/docs/user/manual/en/stock/purchase-receipt) is made when an *Item* is received from a *Supplier*. \n\nIn this step we will create a *Purchase Receipt*.", "docstatus": 0, "doctype": "Onboarding Step", "idx": 0, "is_complete": 0, - "is_mandatory": 0, "is_single": 0, "is_skipped": 0, - "modified": "2020-10-14 14:53:25.618434", + "modified": "2021-05-18 15:17:44.204445", "modified_by": "Administrator", "name": "Create a Purchase Receipt", "owner": "Administrator", "reference_document": "Purchase Receipt", + "show_form_tour": 1, "show_full_form": 1, "title": "Create a Purchase Receipt", "validate_action": 1 diff --git a/erpnext/stock/onboarding_step/create_a_stock_entry/create_a_stock_entry.json b/erpnext/stock/onboarding_step/create_a_stock_entry/create_a_stock_entry.json index 09902b8844..2244c13d5d 100644 --- a/erpnext/stock/onboarding_step/create_a_stock_entry/create_a_stock_entry.json +++ b/erpnext/stock/onboarding_step/create_a_stock_entry/create_a_stock_entry.json @@ -1,18 +1,19 @@ { "action": "Create Entry", "creation": "2020-05-15 03:20:16.277043", + "description": "# Create a Stock Entry\nA [**Stock Entry**](https://docs.erpnext.com/docs/user/manual/en/stock/stock-entry) allows you to record the movement of *Items* between *Warehouses*. \n\nIn this step we will create *Stock Entry*.", "docstatus": 0, "doctype": "Onboarding Step", "idx": 0, "is_complete": 0, - "is_mandatory": 0, "is_single": 0, "is_skipped": 0, - "modified": "2020-10-14 14:53:00.105905", + "modified": "2021-05-18 15:15:26.355884", "modified_by": "Administrator", "name": "Create a Stock Entry", "owner": "Administrator", "reference_document": "Stock Entry", + "show_form_tour": 1, "show_full_form": 1, "title": "Create a Stock Entry", "validate_action": 1 diff --git a/erpnext/stock/onboarding_step/create_a_supplier/create_a_supplier.json b/erpnext/stock/onboarding_step/create_a_supplier/create_a_supplier.json index ef61fa3b2e..49efe578a2 100644 --- a/erpnext/stock/onboarding_step/create_a_supplier/create_a_supplier.json +++ b/erpnext/stock/onboarding_step/create_a_supplier/create_a_supplier.json @@ -1,18 +1,19 @@ { - "action": "Create Entry", + "action": "Show Form Tour", "creation": "2020-05-14 22:09:10.043554", + "description": "# Create a Supplier\nIn this step we will create a **Supplier**. If you have already created a **Supplier** you can skip this step.", "docstatus": 0, "doctype": "Onboarding Step", "idx": 0, "is_complete": 0, - "is_mandatory": 0, "is_single": 0, "is_skipped": 0, - "modified": "2020-10-14 14:53:00.120455", + "modified": "2021-05-17 16:37:37.697077", "modified_by": "Administrator", "name": "Create a Supplier", "owner": "Administrator", "reference_document": "Supplier", + "show_form_tour": 0, "show_full_form": 0, "title": "Create a Supplier", "validate_action": 1 diff --git a/erpnext/stock/onboarding_step/create_a_warehouse/create_a_warehouse.json b/erpnext/stock/onboarding_step/create_a_warehouse/create_a_warehouse.json new file mode 100644 index 0000000000..b31b0d03d7 --- /dev/null +++ b/erpnext/stock/onboarding_step/create_a_warehouse/create_a_warehouse.json @@ -0,0 +1,20 @@ +{ + "action": "Create Entry", + "creation": "2021-05-17 16:13:19.297789", + "description": "# Create a Warehouse\nIn this step we will create a [**Warehouse**](https://docs.erpnext.com/docs/user/manual/en/stock/warehouse).", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2021-05-18 15:00:36.896793", + "modified_by": "Administrator", + "name": "Create a Warehouse", + "owner": "Administrator", + "reference_document": "Warehouse", + "show_form_tour": 1, + "show_full_form": 1, + "title": "Create a Warehouse", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/stock/onboarding_step/create_an_item/create_an_item.json b/erpnext/stock/onboarding_step/create_an_item/create_an_item.json new file mode 100644 index 0000000000..016cbd566d --- /dev/null +++ b/erpnext/stock/onboarding_step/create_an_item/create_an_item.json @@ -0,0 +1,22 @@ +{ + "action": "Create Entry", + "action_label": "", + "creation": "2021-05-17 13:47:18.515052", + "description": "# Create an Item\nThe Stock module deals with the movement of items.\n\nIn this step we will create an [**Item**](https://docs.erpnext.com/docs/user/manual/en/stock/item).", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "intro_video_url": "", + "is_complete": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2021-05-18 16:15:20.695028", + "modified_by": "Administrator", + "name": "Create an Item", + "owner": "Administrator", + "reference_document": "Item", + "show_form_tour": 1, + "show_full_form": 1, + "title": "Create an Item", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/stock/onboarding_step/introduction_to_stock_entry/introduction_to_stock_entry.json b/erpnext/stock/onboarding_step/introduction_to_stock_entry/introduction_to_stock_entry.json index 212e5055ed..384950e8b9 100644 --- a/erpnext/stock/onboarding_step/introduction_to_stock_entry/introduction_to_stock_entry.json +++ b/erpnext/stock/onboarding_step/introduction_to_stock_entry/introduction_to_stock_entry.json @@ -1,17 +1,18 @@ { "action": "Watch Video", "creation": "2020-05-15 02:47:17.958806", + "description": "# Introduction to Stock Entry\nThis video will give a quick introduction to [**Stock Entry**](https://docs.erpnext.com/docs/user/manual/en/stock/stock-entry).", "docstatus": 0, "doctype": "Onboarding Step", "idx": 0, "is_complete": 0, - "is_mandatory": 0, "is_single": 0, "is_skipped": 0, - "modified": "2020-10-14 14:53:00.075177", + "modified": "2021-05-18 15:13:43.306064", "modified_by": "Administrator", "name": "Introduction to Stock Entry", "owner": "Administrator", + "show_form_tour": 0, "show_full_form": 0, "title": "Introduction to Stock Entry", "validate_action": 1, diff --git a/erpnext/stock/onboarding_step/setup_your_warehouse/setup_your_warehouse.json b/erpnext/stock/onboarding_step/setup_your_warehouse/setup_your_warehouse.json index 75940ed2a6..5d33a64910 100644 --- a/erpnext/stock/onboarding_step/setup_your_warehouse/setup_your_warehouse.json +++ b/erpnext/stock/onboarding_step/setup_your_warehouse/setup_your_warehouse.json @@ -5,15 +5,15 @@ "doctype": "Onboarding Step", "idx": 0, "is_complete": 0, - "is_mandatory": 0, "is_single": 0, "is_skipped": 0, - "modified": "2020-10-14 14:53:25.538900", + "modified": "2021-05-17 13:53:06.936579", "modified_by": "Administrator", "name": "Setup your Warehouse", "owner": "Administrator", "path": "Tree/Warehouse", "reference_document": "Warehouse", + "show_form_tour": 0, "show_full_form": 0, "title": "Set up your Warehouse", "validate_action": 1 diff --git a/erpnext/stock/onboarding_step/stock_settings/stock_settings.json b/erpnext/stock/onboarding_step/stock_settings/stock_settings.json index ae34afa695..096a3d2b47 100644 --- a/erpnext/stock/onboarding_step/stock_settings/stock_settings.json +++ b/erpnext/stock/onboarding_step/stock_settings/stock_settings.json @@ -1,18 +1,19 @@ { "action": "Show Form Tour", "creation": "2020-05-15 02:53:57.209967", + "description": "# Stock Settings\nIn this step we will view the [**Stock Settings**](https://docs.erpnext.com/docs/user/manual/en/stock/stock-settings) page, here you can set the default settings for your stock related transactions.", "docstatus": 0, "doctype": "Onboarding Step", "idx": 0, "is_complete": 0, - "is_mandatory": 0, "is_single": 1, "is_skipped": 0, - "modified": "2020-10-14 14:53:00.092504", + "modified": "2021-05-18 16:44:58.084849", "modified_by": "Administrator", "name": "Stock Settings", "owner": "Administrator", "reference_document": "Stock Settings", + "show_form_tour": 0, "show_full_form": 0, "title": "Explore Stock Settings", "validate_action": 1 diff --git a/erpnext/stock/onboarding_step/view_stock_ledger/view_stock_ledger.json b/erpnext/stock/onboarding_step/view_stock_ledger/view_stock_ledger.json new file mode 100644 index 0000000000..13661bc9fb --- /dev/null +++ b/erpnext/stock/onboarding_step/view_stock_ledger/view_stock_ledger.json @@ -0,0 +1,23 @@ +{ + "action": "View Report", + "creation": "2021-05-17 16:16:10.727959", + "description": "# View Stock Ledger\nThe [**Stock Ledger**](https://docs.erpnext.com/docs/user/manual/en/stock/stock-ledger) is a detailed record that keeps track of stock movements for a company. \n\nIn this step we will view the *Stock Ledger*.", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2021-05-18 16:16:38.150079", + "modified_by": "Administrator", + "name": "View Stock Ledger", + "owner": "Administrator", + "reference_report": "Stock Ledger", + "report_description": "You can set the filters to see entries specific to the previous steps.", + "report_reference_doctype": "Stock Ledger Entry", + "report_type": "Script Report", + "show_form_tour": 0, + "show_full_form": 0, + "title": "View Stock Ledger", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/stock/onboarding_step/view_warehouses/view_warehouses.json b/erpnext/stock/onboarding_step/view_warehouses/view_warehouses.json new file mode 100644 index 0000000000..c46c4bdab8 --- /dev/null +++ b/erpnext/stock/onboarding_step/view_warehouses/view_warehouses.json @@ -0,0 +1,20 @@ +{ + "action": "Go to Page", + "creation": "2021-05-17 16:12:43.427579", + "description": "# View Warehouse\nIn ERPNext the term 'warehouse' can be thought of as a storage location.\n\nWarehouses are arranged in ERPNext in a tree like structure, where multiple sub-warehouses can be grouped under a single warehouse.\n\nIn this step we will view the [**Warehouse Tree**](https://docs.erpnext.com/docs/user/manual/en/stock/warehouse#21-tree-view) to view the [**Warehouses**](https://docs.erpnext.com/docs/user/manual/en/stock/warehouse) that are set by default.", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2021-05-18 15:04:41.198413", + "modified_by": "Administrator", + "name": "View Warehouses", + "owner": "Administrator", + "path": "Tree/Warehouse", + "show_form_tour": 0, + "show_full_form": 0, + "title": "View Warehouses", + "validate_action": 1 +} \ No newline at end of file From 0b669b3d7e0e079a4029ec5898d8de0e38333648 Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Tue, 18 May 2021 17:18:04 +0530 Subject: [PATCH 09/65] refactor: add form tour for stock module onboarding --- .../doctype/delivery_note/delivery_note.js | 20 ++++++++++ erpnext/stock/doctype/item/item.js | 39 +++++++++++++++++++ .../purchase_receipt/purchase_receipt.js | 20 ++++++++++ .../stock/doctype/stock_entry/stock_entry.js | 20 ++++++++++ erpnext/stock/doctype/warehouse/warehouse.js | 25 +++++++++++- 5 files changed, 123 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.js b/erpnext/stock/doctype/delivery_note/delivery_note.js index 36dfa6d795..b73ebd6c3b 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.js +++ b/erpnext/stock/doctype/delivery_note/delivery_note.js @@ -356,3 +356,23 @@ erpnext.stock.delivery_note.set_print_hide = function(doc, cdt, cdn){ dn_fields['taxes'].print_hide = 0; } } + + +frappe.tour['Delivery Note'] = [ + { + fieldname: "customer", + title: __("Customer"), + description: __("This field is used to set the 'Customer'.") + }, + { + fieldname: "items", + title: __("Items"), + description: __("This table is used to set details about the 'Item', 'Qty', 'Basic Rate', etc. ") + + __("Different 'Source Warehouse' and 'Target Warehouse' can be set for each row.") + }, + { + fieldname: "set_posting_time", + title: __("Edit Posting Date and Time"), + description: __("This option can be checked to edit the 'Posting Date' and 'Posting Time' fields.") + } +] diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index c5bc9f14fb..6af14c95a4 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -793,3 +793,42 @@ frappe.ui.form.on("UOM Conversion Detail", { } } }) + + +frappe.tour['Item'] = [ + { + fieldname: "item_name", + title: __("Item Name"), + description: __("This is the actual name of the product or service.") + }, + { + fieldname: "item_code", + title: __("Item Code"), + description: __("The Item Code can be thought of as a short-form that refers to the item. ") + + __("For more information click here: ") + + "Item Codification" + + __("."), + }, + { + fieldname: "item_group", + title: __("Item Group"), + description: __("This is used for categorizing items under a common criteria. ") + + __("For more information click here: ") + + "Item Group" + + __("."), + }, + { + fieldname: "stock_uom", + title: __("Default Unit of Measure"), + description: __("The Item Code can be thought of as a short-form that refers to the item. ") + + __("For more information click here: ") + + "Unit of Measure" + + __("."), + }, + { + fieldname: "is_stock_item", + title: __("Maintain Stock"), + description: __("Selecting this creates a stock item and 'Stock Ledger' entries are created for all transcations. ") + + __("Unselect this for a non-stock item such as a service.") + }, +] diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js index 0182ed55a1..fd4be86ae9 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js @@ -347,3 +347,23 @@ var validate_sample_quantity = function(frm, cdt, cdn) { }); } }; + + +frappe.tour['Purchase Receipt'] = [ + { + fieldname: "supplier", + title: __("Supplier"), + description: __("This field is used to set the 'Supplier'.") + }, + { + fieldname: "items", + title: __("Items"), + description: __("This table is used to set details about the 'Item', 'Qty', 'Basic Rate', etc. ") + + __("Different 'Source Warehouse' and 'Target Warehouse' can be set for each row.") + }, + { + fieldname: "set_posting_time", + title: __("Edit Posting Date and Time"), + description: __("This option can be checked to edit the 'Posting Date' and 'Posting Time' fields.") + } +] diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 908020d02b..8616a4cd3f 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -1101,3 +1101,23 @@ function check_should_not_attach_bom_items(bom_no) { } $.extend(cur_frm.cscript, new erpnext.stock.StockEntry({frm: cur_frm})); + +frappe.tour['Stock Entry'] = [ + { + fieldname: "stock_entry_type", + title: __("Stock Entry Type"), + description: __("There are multiple 'Stock Entry Type's you can even create your own. ") + + __("For moving material between warehouses you can select 'Material Transfer'") + }, + { + fieldname: "items", + title: __("Items"), + description: __("This table is used to set details about the 'Item', 'Qty', 'Basic Rate', etc. ") + + __("Different 'Source Warehouse' and 'Target Warehouse' can be set for each row.") + }, + { + fieldname: "set_posting_time", + title: __("Edit Posting Date and Time"), + description: __("This option can be checked to edit the 'Posting Date' and 'Posting Time' fields.") + } +] diff --git a/erpnext/stock/doctype/warehouse/warehouse.js b/erpnext/stock/doctype/warehouse/warehouse.js index 1f172504a7..9b5f2bc0b1 100644 --- a/erpnext/stock/doctype/warehouse/warehouse.js +++ b/erpnext/stock/doctype/warehouse/warehouse.js @@ -85,4 +85,27 @@ function convert_to_group_or_ledger(frm){ } }) -} \ No newline at end of file +} + +frappe.tour['Warehouse'] = [ + { + fieldname: "warehouse_name", + title: __("Warehouse Name"), + description: __("A 'Warehouse Name' can be assigned to the warehouse. This should reflect the type of goods it contains.") + }, + { + fieldname: "warehouse_type", + title: __("Warehouse Type"), + description: __("A 'Warehouse Type' can be set to classify warehouses.") + }, + { + fieldname: "is_group", + title: __("Is Group"), + description: __("If you check 'Is Group', you can group warehouses under this warehouse.") + }, + { + fieldname: "account", + title: __("Account"), + description: __("This field can be used to set a default account for all transactions with this warehouse.") + }, +] From 1851232e3cc13676c131de99cc2df0b6fc5526ee Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Tue, 18 May 2021 17:30:23 +0530 Subject: [PATCH 10/65] refactor: move trailing whitespace out of translate func --- erpnext/stock/doctype/delivery_note/delivery_note.js | 2 +- erpnext/stock/doctype/item/item.js | 12 ++++++------ .../doctype/purchase_receipt/purchase_receipt.js | 2 +- erpnext/stock/doctype/stock_entry/stock_entry.js | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.js b/erpnext/stock/doctype/delivery_note/delivery_note.js index b73ebd6c3b..706ca36598 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.js +++ b/erpnext/stock/doctype/delivery_note/delivery_note.js @@ -367,7 +367,7 @@ frappe.tour['Delivery Note'] = [ { fieldname: "items", title: __("Items"), - description: __("This table is used to set details about the 'Item', 'Qty', 'Basic Rate', etc. ") + + description: __("This table is used to set details about the 'Item', 'Qty', 'Basic Rate', etc.") + " " + __("Different 'Source Warehouse' and 'Target Warehouse' can be set for each row.") }, { diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index 6af14c95a4..e664e9c1cd 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -804,7 +804,7 @@ frappe.tour['Item'] = [ { fieldname: "item_code", title: __("Item Code"), - description: __("The Item Code can be thought of as a short-form that refers to the item. ") + + description: __("The Item Code can be thought of as a short-form that refers to the item.") + " " + __("For more information click here: ") + "Item Codification" + __("."), @@ -812,23 +812,23 @@ frappe.tour['Item'] = [ { fieldname: "item_group", title: __("Item Group"), - description: __("This is used for categorizing items under a common criteria. ") + - __("For more information click here: ") + + description: __("This is used for categorizing items under a common criteria.") + " " + + __("For more information click here:") + " " + "Item Group" + __("."), }, { fieldname: "stock_uom", title: __("Default Unit of Measure"), - description: __("The Item Code can be thought of as a short-form that refers to the item. ") + - __("For more information click here: ") + + description: __("The Item Code can be thought of as a short-form that refers to the item.") + " " + + __("For more information click here:") + " " + "Unit of Measure" + __("."), }, { fieldname: "is_stock_item", title: __("Maintain Stock"), - description: __("Selecting this creates a stock item and 'Stock Ledger' entries are created for all transcations. ") + + description: __("Selecting this creates a stock item and 'Stock Ledger' entries are created for all transcations.") + " " + __("Unselect this for a non-stock item such as a service.") }, ] diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js index fd4be86ae9..9f002f459f 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js @@ -358,7 +358,7 @@ frappe.tour['Purchase Receipt'] = [ { fieldname: "items", title: __("Items"), - description: __("This table is used to set details about the 'Item', 'Qty', 'Basic Rate', etc. ") + + description: __("This table is used to set details about the 'Item', 'Qty', 'Basic Rate', etc.") + " " + __("Different 'Source Warehouse' and 'Target Warehouse' can be set for each row.") }, { diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 8616a4cd3f..30d0750959 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -1106,13 +1106,13 @@ frappe.tour['Stock Entry'] = [ { fieldname: "stock_entry_type", title: __("Stock Entry Type"), - description: __("There are multiple 'Stock Entry Type's you can even create your own. ") + + description: __("There are multiple 'Stock Entry Type's you can even create your own.") + " " + __("For moving material between warehouses you can select 'Material Transfer'") }, { fieldname: "items", title: __("Items"), - description: __("This table is used to set details about the 'Item', 'Qty', 'Basic Rate', etc. ") + + description: __("This table is used to set details about the 'Item', 'Qty', 'Basic Rate', etc.") + " " + __("Different 'Source Warehouse' and 'Target Warehouse' can be set for each row.") }, { From 51d2317bb5339022040d2e177156c30157a6c20e Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Tue, 18 May 2021 17:50:59 +0530 Subject: [PATCH 11/65] refactor: sider/semgrep --- erpnext/stock/doctype/item/item.js | 6 +++--- erpnext/stock/doctype/warehouse/warehouse.js | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index e664e9c1cd..3144f1c12a 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -792,7 +792,7 @@ frappe.ui.form.on("UOM Conversion Detail", { }); } } -}) +}); frappe.tour['Item'] = [ @@ -805,7 +805,7 @@ frappe.tour['Item'] = [ fieldname: "item_code", title: __("Item Code"), description: __("The Item Code can be thought of as a short-form that refers to the item.") + " " + - __("For more information click here: ") + + __("For more information click here:") + " " + "Item Codification" + __("."), }, @@ -831,4 +831,4 @@ frappe.tour['Item'] = [ description: __("Selecting this creates a stock item and 'Stock Ledger' entries are created for all transcations.") + " " + __("Unselect this for a non-stock item such as a service.") }, -] +]; diff --git a/erpnext/stock/doctype/warehouse/warehouse.js b/erpnext/stock/doctype/warehouse/warehouse.js index 9b5f2bc0b1..af9a515247 100644 --- a/erpnext/stock/doctype/warehouse/warehouse.js +++ b/erpnext/stock/doctype/warehouse/warehouse.js @@ -108,4 +108,4 @@ frappe.tour['Warehouse'] = [ title: __("Account"), description: __("This field can be used to set a default account for all transactions with this warehouse.") }, -] +]; From 1d67d20185e48e537a1841d5bc73ceb56ad41cfd Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Thu, 20 May 2021 13:04:59 +0530 Subject: [PATCH 12/65] refactor: remove DN, PR; change wording, add/remove steps in tour --- erpnext/stock/doctype/item/item.js | 20 +++++++++--------- .../stock/doctype/stock_entry/stock_entry.js | 21 ++++++++++++------- erpnext/stock/doctype/warehouse/warehouse.js | 8 +++---- .../create_a_delivery_note.json | 20 ------------------ .../create_a_purchase_receipt.json | 20 ------------------ 5 files changed, 27 insertions(+), 62 deletions(-) delete mode 100644 erpnext/stock/onboarding_step/create_a_delivery_note/create_a_delivery_note.json delete mode 100644 erpnext/stock/onboarding_step/create_a_purchase_receipt/create_a_purchase_receipt.json diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index 3144f1c12a..5c632fc7d5 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -796,23 +796,23 @@ frappe.ui.form.on("UOM Conversion Detail", { frappe.tour['Item'] = [ - { - fieldname: "item_name", - title: __("Item Name"), - description: __("This is the actual name of the product or service.") - }, { fieldname: "item_code", title: __("Item Code"), - description: __("The Item Code can be thought of as a short-form that refers to the item.") + " " + + description: __("Select an item code, this can be an SKU or a product code.") + " " + __("For more information click here:") + " " + "Item Codification" + __("."), }, + { + fieldname: "item_name", + title: __("Item Name"), + description: __("Select an item name, this is the actual name of the item. You will be able to search and select an item with its name as well.") + }, { fieldname: "item_group", title: __("Item Group"), - description: __("This is used for categorizing items under a common criteria.") + " " + + description: __("Select an item group to categorizing items under a common criteria. You can even create custom Item Groups.") + " " + __("For more information click here:") + " " + "Item Group" + __("."), @@ -820,7 +820,7 @@ frappe.tour['Item'] = [ { fieldname: "stock_uom", title: __("Default Unit of Measure"), - description: __("The Item Code can be thought of as a short-form that refers to the item.") + " " + + description: __("This decides the unit of measure in which stock balance will be maintained for this item.") + " " + __("For more information click here:") + " " + "Unit of Measure" + __("."), @@ -828,7 +828,7 @@ frappe.tour['Item'] = [ { fieldname: "is_stock_item", title: __("Maintain Stock"), - description: __("Selecting this creates a stock item and 'Stock Ledger' entries are created for all transcations.") + " " + - __("Unselect this for a non-stock item such as a service.") + description: __("Check this field to maintain stock for this item.") + " " + + __("Uncheck this field for non-stock items such as a service.") }, ]; diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 30d0750959..e7bf0f3bf9 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -1106,18 +1106,23 @@ frappe.tour['Stock Entry'] = [ { fieldname: "stock_entry_type", title: __("Stock Entry Type"), - description: __("There are multiple 'Stock Entry Type's you can even create your own.") + " " + - __("For moving material between warehouses you can select 'Material Transfer'") + description: __("Select the type of Stock Entry to be made.") + " " + + __("For now, to move stock between warehouses select Material Transfer.") + }, + { + fieldname: "from_warehouse", + title: __("Default Source Warehouse"), + description: __("Select a source warehouse, where you have stock available.") + }, + { + fieldname: "to_warehouse", + title: __("Default Target Warehouse"), + description: __("Select a target warehouse, where stock needs to be transferred.") }, { fieldname: "items", title: __("Items"), - description: __("This table is used to set details about the 'Item', 'Qty', 'Basic Rate', etc.") + " " + + description: __("Select an item and entry quantity to be delivered.") + " " + __("Different 'Source Warehouse' and 'Target Warehouse' can be set for each row.") }, - { - fieldname: "set_posting_time", - title: __("Edit Posting Date and Time"), - description: __("This option can be checked to edit the 'Posting Date' and 'Posting Time' fields.") - } ] diff --git a/erpnext/stock/doctype/warehouse/warehouse.js b/erpnext/stock/doctype/warehouse/warehouse.js index af9a515247..b29c6fdde3 100644 --- a/erpnext/stock/doctype/warehouse/warehouse.js +++ b/erpnext/stock/doctype/warehouse/warehouse.js @@ -91,21 +91,21 @@ frappe.tour['Warehouse'] = [ { fieldname: "warehouse_name", title: __("Warehouse Name"), - description: __("A 'Warehouse Name' can be assigned to the warehouse. This should reflect the type of goods it contains.") + description: __("Select a name for the warehouse. This should reflect it's location or purpose.") }, { fieldname: "warehouse_type", title: __("Warehouse Type"), - description: __("A 'Warehouse Type' can be set to classify warehouses.") + description: __("Select a warehouse type to categorize the warehouse into a sub-group.") }, { fieldname: "is_group", title: __("Is Group"), - description: __("If you check 'Is Group', you can group warehouses under this warehouse.") + description: __("Check this field to group warehouses under this warehouse.") }, { fieldname: "account", title: __("Account"), - description: __("This field can be used to set a default account for all transactions with this warehouse.") + description: __("Select an account to set a default account for all transactions with this warehouse.") }, ]; diff --git a/erpnext/stock/onboarding_step/create_a_delivery_note/create_a_delivery_note.json b/erpnext/stock/onboarding_step/create_a_delivery_note/create_a_delivery_note.json deleted file mode 100644 index 7b702f013b..0000000000 --- a/erpnext/stock/onboarding_step/create_a_delivery_note/create_a_delivery_note.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "action": "Create Entry", - "creation": "2021-05-17 16:15:11.745214", - "description": "# Create a Delivery Note\nA [**Delivery Note**](https://docs.erpnext.com/docs/user/manual/en/stock/delivery-note) is made when a shipment is shipped from the company\u2019s *Warehouse* to the *Customer*. \n\nIn this step we will create a *Delivery Note*.", - "docstatus": 0, - "doctype": "Onboarding Step", - "idx": 0, - "is_complete": 0, - "is_single": 0, - "is_skipped": 0, - "modified": "2021-05-18 15:33:47.452298", - "modified_by": "Administrator", - "name": "Create a Delivery Note", - "owner": "Administrator", - "reference_document": "Delivery Note", - "show_form_tour": 1, - "show_full_form": 1, - "title": "Create a Delivery Note", - "validate_action": 1 -} \ No newline at end of file diff --git a/erpnext/stock/onboarding_step/create_a_purchase_receipt/create_a_purchase_receipt.json b/erpnext/stock/onboarding_step/create_a_purchase_receipt/create_a_purchase_receipt.json deleted file mode 100644 index 549fe26c25..0000000000 --- a/erpnext/stock/onboarding_step/create_a_purchase_receipt/create_a_purchase_receipt.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "action": "Create Entry", - "creation": "2020-05-19 18:59:13.266713", - "description": "# Create a Purchase Entry\nA [**Purchase Receipt**](https://docs.erpnext.com/docs/user/manual/en/stock/purchase-receipt) is made when an *Item* is received from a *Supplier*. \n\nIn this step we will create a *Purchase Receipt*.", - "docstatus": 0, - "doctype": "Onboarding Step", - "idx": 0, - "is_complete": 0, - "is_single": 0, - "is_skipped": 0, - "modified": "2021-05-18 15:17:44.204445", - "modified_by": "Administrator", - "name": "Create a Purchase Receipt", - "owner": "Administrator", - "reference_document": "Purchase Receipt", - "show_form_tour": 1, - "show_full_form": 1, - "title": "Create a Purchase Receipt", - "validate_action": 1 -} \ No newline at end of file From 9581836885bd37e577e5c70fadf72738ecb0a7a3 Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Thu, 20 May 2021 15:23:17 +0530 Subject: [PATCH 13/65] refactor: add watch video step for stock opening balance --- .../stock/module_onboarding/stock/stock.json | 7 ++----- .../stock_opening_balance.json | 21 +++++++++++++++++++ 2 files changed, 23 insertions(+), 5 deletions(-) create mode 100644 erpnext/stock/onboarding_step/stock_opening_balance/stock_opening_balance.json diff --git a/erpnext/stock/module_onboarding/stock/stock.json b/erpnext/stock/module_onboarding/stock/stock.json index 6cc8ec6b05..9e91090d5b 100644 --- a/erpnext/stock/module_onboarding/stock/stock.json +++ b/erpnext/stock/module_onboarding/stock/stock.json @@ -19,7 +19,7 @@ "documentation_url": "https://docs.erpnext.com/docs/user/manual/en/stock", "idx": 0, "is_complete": 0, - "modified": "2021-05-18 17:01:36.137180", + "modified": "2021-05-20 15:08:11.987145", "modified_by": "Administrator", "module": "Stock", "name": "Stock", @@ -47,10 +47,7 @@ "step": "View Stock Ledger" }, { - "step": "Create a Purchase Receipt" - }, - { - "step": "Create a Delivery Note" + "step": "Stock Opening Balance" } ], "subtitle": "Inventory, Warehouses, Analysis, and more.", diff --git a/erpnext/stock/onboarding_step/stock_opening_balance/stock_opening_balance.json b/erpnext/stock/onboarding_step/stock_opening_balance/stock_opening_balance.json new file mode 100644 index 0000000000..19d6a37ba4 --- /dev/null +++ b/erpnext/stock/onboarding_step/stock_opening_balance/stock_opening_balance.json @@ -0,0 +1,21 @@ +{ + "action": "Watch Video", + "creation": "2021-05-17 16:13:47.511883", + "description": "# Stock Opening Balance\nTo set the Stock Opening Balance, you can create a [**Stock Reconciliation**](https://docs.erpnext.com/docs/user/manual/en/stock/stock-reconciliation) entry with _Purpose_ set to _Opening Balance_.\n\nFor more information you can watch the video.", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2021-05-20 15:07:19.545934", + "modified_by": "Administrator", + "name": "Stock Opening Balance", + "owner": "Administrator", + "reference_document": "Stock Reconciliation", + "show_form_tour": 1, + "show_full_form": 1, + "title": "Stock Opening Balance", + "validate_action": 1, + "video_url": "https://www.youtube.com/watch?v=nlHX0ZZ84Lw" +} \ No newline at end of file From 37466631f20b16e94253ac2115717aed2ceeb9b8 Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Thu, 20 May 2021 15:28:08 +0530 Subject: [PATCH 14/65] refactor: reorder steps according to stock settings refactor --- erpnext/stock/doctype/stock_settings/stock_settings.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.js b/erpnext/stock/doctype/stock_settings/stock_settings.js index 48624e0f25..e2f1f951ec 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.js +++ b/erpnext/stock/doctype/stock_settings/stock_settings.js @@ -27,17 +27,17 @@ frappe.tour['Stock Settings'] = [ title: __("Default Warehouse"), description: __("Set a Default Warehouse for Inventory Transactions. This will be fetched into the Default Warehouse in the Item master.") }, + { + fieldname: "valuation_method", + title: __("Valuation Method"), + description: __("Choose between FIFO and Moving Average Valuation Methods. Click ") + "here" + __(" to know more about them.") + }, { fieldname: "allow_negative_stock", title: __("Allow Negative Stock"), description: __("This will allow stock items to be displayed in negative values. Using this option depends on your use case. With this option unchecked, the system warns before obstructing a transaction that is causing negative stock.") }, - { - fieldname: "valuation_method", - title: __("Valuation Method"), - description: __("Choose between FIFO and Moving Average Valuation Methods. Click ") + "here" + __(" to know more about them.") - }, { fieldname: "show_barcode_field", title: __("Show Barcode Field"), From 95992a7ea54de397c200c3be4925e7c0a5fa6482 Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Thu, 20 May 2021 15:29:24 +0530 Subject: [PATCH 15/65] refactor: fix typo, remove target warehouse cause SE Type dependency --- erpnext/stock/doctype/stock_entry/stock_entry.js | 5 ----- erpnext/stock/doctype/warehouse/warehouse.js | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index e7bf0f3bf9..ecd3fee828 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -1114,11 +1114,6 @@ frappe.tour['Stock Entry'] = [ title: __("Default Source Warehouse"), description: __("Select a source warehouse, where you have stock available.") }, - { - fieldname: "to_warehouse", - title: __("Default Target Warehouse"), - description: __("Select a target warehouse, where stock needs to be transferred.") - }, { fieldname: "items", title: __("Items"), diff --git a/erpnext/stock/doctype/warehouse/warehouse.js b/erpnext/stock/doctype/warehouse/warehouse.js index b29c6fdde3..30575a7c93 100644 --- a/erpnext/stock/doctype/warehouse/warehouse.js +++ b/erpnext/stock/doctype/warehouse/warehouse.js @@ -91,7 +91,7 @@ frappe.tour['Warehouse'] = [ { fieldname: "warehouse_name", title: __("Warehouse Name"), - description: __("Select a name for the warehouse. This should reflect it's location or purpose.") + description: __("Select a name for the warehouse. This should reflect its location or purpose.") }, { fieldname: "warehouse_type", From 599c1024a221624f83f50a7295768292aebce48a Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Thu, 20 May 2021 15:49:26 +0530 Subject: [PATCH 16/65] fix: semgrep, remove trailing and leading whitespaces --- erpnext/stock/doctype/stock_settings/stock_settings.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.js b/erpnext/stock/doctype/stock_settings/stock_settings.js index e2f1f951ec..6a919309f5 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.js +++ b/erpnext/stock/doctype/stock_settings/stock_settings.js @@ -20,7 +20,9 @@ frappe.tour['Stock Settings'] = [ { fieldname: "item_naming_by", title: __("Item Naming By"), - description: __("By default, the Item Name is set as per the Item Code entered. If you want Items to be named by a ") + "Naming Series" + __(" choose the 'Naming Series' option."), + description: __("By default, the Item Name is set as per the Item Code entered. If you want Items to be named by a") + " " + + "Naming Series" + " " + + __("choose the 'Naming Series' option."), }, { fieldname: "default_warehouse", @@ -30,7 +32,9 @@ frappe.tour['Stock Settings'] = [ { fieldname: "valuation_method", title: __("Valuation Method"), - description: __("Choose between FIFO and Moving Average Valuation Methods. Click ") + "here" + __(" to know more about them.") + description: __("Choose between FIFO and Moving Average Valuation Methods. Click") + " " + + "here" + " " + + __("to know more about them.") }, { fieldname: "allow_negative_stock", From f7b747e19c322df60f7cc31ac3cf0c75bc9e29aa Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Fri, 18 Jun 2021 14:43:56 +0530 Subject: [PATCH 17/65] refactor: reduce steps, reword cards --- erpnext/stock/doctype/item/item.js | 39 ------------------- .../purchase_receipt/purchase_receipt.js | 20 ---------- .../stock_reconciliation.js | 13 +++++++ .../stock/module_onboarding/stock/stock.json | 17 ++------ .../create_a_stock_entry.json | 7 ++-- .../create_a_warehouse.json | 7 ++-- .../stock_opening_balance.json | 9 +++-- .../stock_settings/stock_settings.json | 7 ++-- .../view_stock_balance.json | 24 ++++++++++++ .../view_stock_ledger/view_stock_ledger.json | 7 ++-- 10 files changed, 62 insertions(+), 88 deletions(-) create mode 100644 erpnext/stock/onboarding_step/view_stock_balance/view_stock_balance.json diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index 5c632fc7d5..c587dd5c7e 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -793,42 +793,3 @@ frappe.ui.form.on("UOM Conversion Detail", { } } }); - - -frappe.tour['Item'] = [ - { - fieldname: "item_code", - title: __("Item Code"), - description: __("Select an item code, this can be an SKU or a product code.") + " " + - __("For more information click here:") + " " + - "Item Codification" + - __("."), - }, - { - fieldname: "item_name", - title: __("Item Name"), - description: __("Select an item name, this is the actual name of the item. You will be able to search and select an item with its name as well.") - }, - { - fieldname: "item_group", - title: __("Item Group"), - description: __("Select an item group to categorizing items under a common criteria. You can even create custom Item Groups.") + " " + - __("For more information click here:") + " " + - "Item Group" + - __("."), - }, - { - fieldname: "stock_uom", - title: __("Default Unit of Measure"), - description: __("This decides the unit of measure in which stock balance will be maintained for this item.") + " " + - __("For more information click here:") + " " + - "Unit of Measure" + - __("."), - }, - { - fieldname: "is_stock_item", - title: __("Maintain Stock"), - description: __("Check this field to maintain stock for this item.") + " " + - __("Uncheck this field for non-stock items such as a service.") - }, -]; diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js index 9f002f459f..0182ed55a1 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js @@ -347,23 +347,3 @@ var validate_sample_quantity = function(frm, cdt, cdn) { }); } }; - - -frappe.tour['Purchase Receipt'] = [ - { - fieldname: "supplier", - title: __("Supplier"), - description: __("This field is used to set the 'Supplier'.") - }, - { - fieldname: "items", - title: __("Items"), - description: __("This table is used to set details about the 'Item', 'Qty', 'Basic Rate', etc.") + " " + - __("Different 'Source Warehouse' and 'Target Warehouse' can be set for each row.") - }, - { - fieldname: "set_posting_time", - title: __("Edit Posting Date and Time"), - description: __("This option can be checked to edit the 'Posting Date' and 'Posting Time' fields.") - } -] diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js index 84f65a077e..49c85401ac 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js @@ -302,3 +302,16 @@ erpnext.stock.StockReconciliation = class StockReconciliation extends erpnext.st }; cur_frm.cscript = new erpnext.stock.StockReconciliation({frm: cur_frm}); + +frappe.tour['Stock Reconciliation'] = [ + { + fieldname: "purpose", + title: __("Purpose"), + description: __("Set Purpose to Opening Stock to set the stock opening balance.") + }, + { + fieldname: "items", + title: __("Items"), + description: __("Select the items for which the opening stock has to be set.") + }, +]; diff --git a/erpnext/stock/module_onboarding/stock/stock.json b/erpnext/stock/module_onboarding/stock/stock.json index 9e91090d5b..4503f534c7 100644 --- a/erpnext/stock/module_onboarding/stock/stock.json +++ b/erpnext/stock/module_onboarding/stock/stock.json @@ -19,35 +19,26 @@ "documentation_url": "https://docs.erpnext.com/docs/user/manual/en/stock", "idx": 0, "is_complete": 0, - "modified": "2021-05-20 15:08:11.987145", + "modified": "2021-06-18 14:41:24.286683", "modified_by": "Administrator", "module": "Stock", "name": "Stock", "owner": "Administrator", "steps": [ { - "step": "Create an Item" - }, - { - "step": "View Warehouses" + "step": "Stock Settings" }, { "step": "Create a Warehouse" }, - { - "step": "Stock Settings" - }, - { - "step": "Introduction to Stock Entry" - }, { "step": "Create a Stock Entry" }, { - "step": "View Stock Ledger" + "step": "Stock Opening Balance" }, { - "step": "Stock Opening Balance" + "step": "View Stock Balance" } ], "subtitle": "Inventory, Warehouses, Analysis, and more.", diff --git a/erpnext/stock/onboarding_step/create_a_stock_entry/create_a_stock_entry.json b/erpnext/stock/onboarding_step/create_a_stock_entry/create_a_stock_entry.json index 2244c13d5d..3cb522c893 100644 --- a/erpnext/stock/onboarding_step/create_a_stock_entry/create_a_stock_entry.json +++ b/erpnext/stock/onboarding_step/create_a_stock_entry/create_a_stock_entry.json @@ -1,20 +1,21 @@ { "action": "Create Entry", + "action_label": "Create a Material Transfer Entry", "creation": "2020-05-15 03:20:16.277043", - "description": "# Create a Stock Entry\nA [**Stock Entry**](https://docs.erpnext.com/docs/user/manual/en/stock/stock-entry) allows you to record the movement of *Items* between *Warehouses*. \n\nIn this step we will create *Stock Entry*.", + "description": "# Manage Stock Movements\nStock entry allows you to register the movement of stock for various purposes like transfer, received, issues, repacked, etc. To address issues related to theft and pilferages, you can always ensure that the movement of goods happens against a document reference Stock Entry in ERPNext.\n\nLet\u2019s get a quick walk-through on the various scenarios covered in Stock Entry by watching [*this video*](https://www.youtube.com/watch?v=Njt107hlY3I).", "docstatus": 0, "doctype": "Onboarding Step", "idx": 0, "is_complete": 0, "is_single": 0, "is_skipped": 0, - "modified": "2021-05-18 15:15:26.355884", + "modified": "2021-06-18 13:57:11.434063", "modified_by": "Administrator", "name": "Create a Stock Entry", "owner": "Administrator", "reference_document": "Stock Entry", "show_form_tour": 1, "show_full_form": 1, - "title": "Create a Stock Entry", + "title": "Manage Stock Movements", "validate_action": 1 } \ No newline at end of file diff --git a/erpnext/stock/onboarding_step/create_a_warehouse/create_a_warehouse.json b/erpnext/stock/onboarding_step/create_a_warehouse/create_a_warehouse.json index b31b0d03d7..0ef6c3615c 100644 --- a/erpnext/stock/onboarding_step/create_a_warehouse/create_a_warehouse.json +++ b/erpnext/stock/onboarding_step/create_a_warehouse/create_a_warehouse.json @@ -1,20 +1,21 @@ { "action": "Create Entry", + "action_label": "Let\u2019s create your first warehouse ", "creation": "2021-05-17 16:13:19.297789", - "description": "# Create a Warehouse\nIn this step we will create a [**Warehouse**](https://docs.erpnext.com/docs/user/manual/en/stock/warehouse).", + "description": "# Setup a Warehouse\nThe warehouse can be your location/godown/store where you maintain the item's inventory, and receive/deliver them to various parties.\n\nIn ERPNext, you can maintain a Warehouse in the tree structure, so that location and sub-location of an item can be tracked. Also, you can link a Warehouse to a specific Accounting ledger, where the real-time stock value of that warehouse\u2019s item will be reflected.", "docstatus": 0, "doctype": "Onboarding Step", "idx": 0, "is_complete": 0, "is_single": 0, "is_skipped": 0, - "modified": "2021-05-18 15:00:36.896793", + "modified": "2021-06-18 13:52:51.126984", "modified_by": "Administrator", "name": "Create a Warehouse", "owner": "Administrator", "reference_document": "Warehouse", "show_form_tour": 1, "show_full_form": 1, - "title": "Create a Warehouse", + "title": "Setup a Warehouse", "validate_action": 1 } \ No newline at end of file diff --git a/erpnext/stock/onboarding_step/stock_opening_balance/stock_opening_balance.json b/erpnext/stock/onboarding_step/stock_opening_balance/stock_opening_balance.json index 19d6a37ba4..48fd1fddee 100644 --- a/erpnext/stock/onboarding_step/stock_opening_balance/stock_opening_balance.json +++ b/erpnext/stock/onboarding_step/stock_opening_balance/stock_opening_balance.json @@ -1,21 +1,22 @@ { - "action": "Watch Video", + "action": "Create Entry", + "action_label": "Let\u2019s create a stock opening entry", "creation": "2021-05-17 16:13:47.511883", - "description": "# Stock Opening Balance\nTo set the Stock Opening Balance, you can create a [**Stock Reconciliation**](https://docs.erpnext.com/docs/user/manual/en/stock/stock-reconciliation) entry with _Purpose_ set to _Opening Balance_.\n\nFor more information you can watch the video.", + "description": "# Update Stock Opening Balance\nIt\u2019s an entry to update the stock balance of an item, in a warehouse, on a date and time you are going live on ERPNext.\n\nOnce opening stocks are updated, you can create transactions like manufacturing and stock deliveries, where this opening stock will be consumed.", "docstatus": 0, "doctype": "Onboarding Step", "idx": 0, "is_complete": 0, "is_single": 0, "is_skipped": 0, - "modified": "2021-05-20 15:07:19.545934", + "modified": "2021-06-18 13:59:36.021097", "modified_by": "Administrator", "name": "Stock Opening Balance", "owner": "Administrator", "reference_document": "Stock Reconciliation", "show_form_tour": 1, "show_full_form": 1, - "title": "Stock Opening Balance", + "title": "Update Stock Opening Balance", "validate_action": 1, "video_url": "https://www.youtube.com/watch?v=nlHX0ZZ84Lw" } \ No newline at end of file diff --git a/erpnext/stock/onboarding_step/stock_settings/stock_settings.json b/erpnext/stock/onboarding_step/stock_settings/stock_settings.json index 096a3d2b47..f7238da334 100644 --- a/erpnext/stock/onboarding_step/stock_settings/stock_settings.json +++ b/erpnext/stock/onboarding_step/stock_settings/stock_settings.json @@ -1,20 +1,21 @@ { "action": "Show Form Tour", + "action_label": "Take a walk through Stock Settings", "creation": "2020-05-15 02:53:57.209967", - "description": "# Stock Settings\nIn this step we will view the [**Stock Settings**](https://docs.erpnext.com/docs/user/manual/en/stock/stock-settings) page, here you can set the default settings for your stock related transactions.", + "description": "# Review Stock Settings\n\nIn ERPNext, the Stock module\u2019s features are configurable as per your business needs. Stock Settings is the place where you can set your preferences for:\n- Default values for Item and Pricing\n- Default valuation method for inventory valuation\n- Set preference for serialization and batching of item\n- Set tolerance for over-receipt and delivery of items", "docstatus": 0, "doctype": "Onboarding Step", "idx": 0, "is_complete": 0, "is_single": 1, "is_skipped": 0, - "modified": "2021-05-18 16:44:58.084849", + "modified": "2021-06-18 14:13:12.678178", "modified_by": "Administrator", "name": "Stock Settings", "owner": "Administrator", "reference_document": "Stock Settings", "show_form_tour": 0, "show_full_form": 0, - "title": "Explore Stock Settings", + "title": "Review Stock Settings", "validate_action": 1 } \ No newline at end of file diff --git a/erpnext/stock/onboarding_step/view_stock_balance/view_stock_balance.json b/erpnext/stock/onboarding_step/view_stock_balance/view_stock_balance.json new file mode 100644 index 0000000000..ed5fe36945 --- /dev/null +++ b/erpnext/stock/onboarding_step/view_stock_balance/view_stock_balance.json @@ -0,0 +1,24 @@ +{ + "action": "View Report", + "action_label": "Check Stock Balance", + "creation": "2021-05-17 16:15:54.617572", + "description": "# Check Stock Reports\nBased on the various stock transactions, you can get a host of one-click Stock Reports in ERPNext like Stock Ledger, Stock Balance, Projected Quantity, and Ageing analysis.", + "docstatus": 0, + "doctype": "Onboarding Step", + "idx": 0, + "is_complete": 0, + "is_single": 0, + "is_skipped": 0, + "modified": "2021-06-18 14:41:18.499215", + "modified_by": "Administrator", + "name": "View Stock Balance", + "owner": "Administrator", + "reference_report": "Stock Balance", + "report_description": "You can set the filters to narrow the results, then click on Generate New Report to see the updated report.", + "report_reference_doctype": "Stock Ledger Entry", + "report_type": "Script Report", + "show_form_tour": 0, + "show_full_form": 0, + "title": "Check Stock Reports", + "validate_action": 1 +} \ No newline at end of file diff --git a/erpnext/stock/onboarding_step/view_stock_ledger/view_stock_ledger.json b/erpnext/stock/onboarding_step/view_stock_ledger/view_stock_ledger.json index 13661bc9fb..7a5f164e01 100644 --- a/erpnext/stock/onboarding_step/view_stock_ledger/view_stock_ledger.json +++ b/erpnext/stock/onboarding_step/view_stock_ledger/view_stock_ledger.json @@ -1,14 +1,15 @@ { "action": "View Report", + "action_label": "Check Stock Ledger", "creation": "2021-05-17 16:16:10.727959", - "description": "# View Stock Ledger\nThe [**Stock Ledger**](https://docs.erpnext.com/docs/user/manual/en/stock/stock-ledger) is a detailed record that keeps track of stock movements for a company. \n\nIn this step we will view the *Stock Ledger*.", + "description": "# Check Stock Reports\nBased on the various stock transactions, you can get a host of one-click Stock Reports in ERPNext like Stock Ledger, Stock Balance, Projected Quantity, and Ageing analysis.", "docstatus": 0, "doctype": "Onboarding Step", "idx": 0, "is_complete": 0, "is_single": 0, "is_skipped": 0, - "modified": "2021-05-18 16:16:38.150079", + "modified": "2021-06-18 14:09:39.399311", "modified_by": "Administrator", "name": "View Stock Ledger", "owner": "Administrator", @@ -18,6 +19,6 @@ "report_type": "Script Report", "show_form_tour": 0, "show_full_form": 0, - "title": "View Stock Ledger", + "title": "Check Stock Reports", "validate_action": 1 } \ No newline at end of file From 6ef16ee4fb3d07526dcb81e6277bc9f5d5e9911e Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Fri, 20 Aug 2021 12:24:13 +0530 Subject: [PATCH 18/65] fix: update scrap table item details; typo --- erpnext/manufacturing/doctype/bom/bom.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 0ba85078ea..eb1dfc8cae 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -148,6 +148,7 @@ class BOM(WebsiteGenerator): self.set_plc_conversion_rate() self.validate_uom_is_interger() self.set_bom_material_details() + self.set_bom_scrap_items_detail() self.validate_materials() self.set_routing_operations() self.validate_operations() @@ -200,7 +201,7 @@ class BOM(WebsiteGenerator): def set_bom_material_details(self): for item in self.get("items"): - self.validate_bom_currecny(item) + self.validate_bom_currency(item) ret = self.get_bom_material_detail({ "company": self.company, @@ -219,6 +220,19 @@ class BOM(WebsiteGenerator): if not item.get(r): item.set(r, ret[r]) + def set_bom_scrap_items_detail(self): + for item in self.get("scrap_items"): + args = { + "item_code": item.item_code, + "company": self.company, + "scrap_items": True, + "bom_no": '', + } + ret = self.get_bom_material_detail(args) + for key, value in ret.items(): + if not item.get(key): + item.set(key, value) + @frappe.whitelist() def get_bom_material_detail(self, args=None): """ Get raw material details like uom, desc and rate""" @@ -255,7 +269,7 @@ class BOM(WebsiteGenerator): return ret_item - def validate_bom_currecny(self, item): + def validate_bom_currency(self, item): if item.get('bom_no') and frappe.db.get_value('BOM', item.get('bom_no'), 'currency') != self.currency: frappe.throw(_("Row {0}: Currency of the BOM #{1} should be equal to the selected currency {2}") .format(item.idx, item.bom_no, self.currency)) From 77ebbdd172c355f8598dcdc92cd1340cb8ad1f29 Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Fri, 20 Aug 2021 16:21:33 +0530 Subject: [PATCH 19/65] fix: minor changes - remove Is Group from warehouse - change stock entry type - link to stock entry type - add posting date to stock reco - change report to Stock Projected Qty - highlight quality inspection action - remove allow neg highlight --- .../stock/doctype/stock_entry/stock_entry.js | 12 ++++------ .../stock_reconciliation.js | 5 ++++ .../doctype/stock_settings/stock_settings.js | 12 +++++----- erpnext/stock/doctype/warehouse/warehouse.js | 8 ++----- .../stock/module_onboarding/stock/stock.json | 4 ++-- .../create_a_warehouse.json | 2 +- .../stock_settings/stock_settings.json | 2 +- .../view_stock_ledger/view_stock_ledger.json | 24 ------------------- .../view_stock_projected_qty.json} | 14 +++++------ 9 files changed, 29 insertions(+), 54 deletions(-) delete mode 100644 erpnext/stock/onboarding_step/view_stock_ledger/view_stock_ledger.json rename erpnext/stock/onboarding_step/{view_stock_balance/view_stock_balance.json => view_stock_projected_qty/view_stock_projected_qty.json} (69%) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index ecd3fee828..5ea733c66d 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -1106,18 +1106,16 @@ frappe.tour['Stock Entry'] = [ { fieldname: "stock_entry_type", title: __("Stock Entry Type"), - description: __("Select the type of Stock Entry to be made.") + " " + - __("For now, to move stock between warehouses select Material Transfer.") + description: __("Select the type of Stock Entry to be made. For now, to receive stock into a warehouses select") + ' ' + __("Material Receipt.") + "" }, { - fieldname: "from_warehouse", - title: __("Default Source Warehouse"), - description: __("Select a source warehouse, where you have stock available.") + fieldname: "to_warehouse", + title: __("Default Target Warehouse"), + description: __("Select a target warehouse where the stock will be received.") }, { fieldname: "items", title: __("Items"), - description: __("Select an item and entry quantity to be delivered.") + " " + - __("Different 'Source Warehouse' and 'Target Warehouse' can be set for each row.") + description: __("Select an item and entry quantity to be delivered.") }, ] diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js index 49c85401ac..f91d0a740d 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js @@ -309,6 +309,11 @@ frappe.tour['Stock Reconciliation'] = [ title: __("Purpose"), description: __("Set Purpose to Opening Stock to set the stock opening balance.") }, + { + fieldname: "posting_date", + title: __("Posting Date"), + description: __("Edit the Posting Date by clicking on the Edit Posting Date and Time checkbox below.") + }, { fieldname: "items", title: __("Items"), diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.js b/erpnext/stock/doctype/stock_settings/stock_settings.js index 6a919309f5..08606fdcaa 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.js +++ b/erpnext/stock/doctype/stock_settings/stock_settings.js @@ -36,17 +36,17 @@ frappe.tour['Stock Settings'] = [ "here" + " " + __("to know more about them.") }, - { - fieldname: "allow_negative_stock", - title: __("Allow Negative Stock"), - description: __("This will allow stock items to be displayed in negative values. Using this option depends on your use case. With this option unchecked, the system warns before obstructing a transaction that is causing negative stock.") - - }, { fieldname: "show_barcode_field", title: __("Show Barcode Field"), description: __("Show 'Scan Barcode' field above every child table to insert Items with ease.") }, + { + fieldname: "action_if_quality_inspection_is_not_submitted", + title: __("Action if Quality Inspection Is Not Submitted"), + description: __("Quality inspection is performed on the inward and outward movement of goods. Receipt and delivery transactions will be stopped or the user will be warned if the quality inspection is not performed.") + + }, { fieldname: "automatically_set_serial_nos_based_on_fifo", title: __("Automatically Set Serial Nos based on FIFO"), diff --git a/erpnext/stock/doctype/warehouse/warehouse.js b/erpnext/stock/doctype/warehouse/warehouse.js index 30575a7c93..8b925bebf9 100644 --- a/erpnext/stock/doctype/warehouse/warehouse.js +++ b/erpnext/stock/doctype/warehouse/warehouse.js @@ -91,21 +91,17 @@ frappe.tour['Warehouse'] = [ { fieldname: "warehouse_name", title: __("Warehouse Name"), - description: __("Select a name for the warehouse. This should reflect its location or purpose.") + description: __("Select name for the warehouse. This should reflect its location or purpose.") }, { fieldname: "warehouse_type", title: __("Warehouse Type"), description: __("Select a warehouse type to categorize the warehouse into a sub-group.") }, - { - fieldname: "is_group", - title: __("Is Group"), - description: __("Check this field to group warehouses under this warehouse.") - }, { fieldname: "account", title: __("Account"), description: __("Select an account to set a default account for all transactions with this warehouse.") }, ]; + diff --git a/erpnext/stock/module_onboarding/stock/stock.json b/erpnext/stock/module_onboarding/stock/stock.json index 4503f534c7..c246747a5b 100644 --- a/erpnext/stock/module_onboarding/stock/stock.json +++ b/erpnext/stock/module_onboarding/stock/stock.json @@ -19,7 +19,7 @@ "documentation_url": "https://docs.erpnext.com/docs/user/manual/en/stock", "idx": 0, "is_complete": 0, - "modified": "2021-06-18 14:41:24.286683", + "modified": "2021-08-20 14:38:55.570067", "modified_by": "Administrator", "module": "Stock", "name": "Stock", @@ -38,7 +38,7 @@ "step": "Stock Opening Balance" }, { - "step": "View Stock Balance" + "step": "View Stock Projected Qty" } ], "subtitle": "Inventory, Warehouses, Analysis, and more.", diff --git a/erpnext/stock/onboarding_step/create_a_warehouse/create_a_warehouse.json b/erpnext/stock/onboarding_step/create_a_warehouse/create_a_warehouse.json index 0ef6c3615c..22c88bf10e 100644 --- a/erpnext/stock/onboarding_step/create_a_warehouse/create_a_warehouse.json +++ b/erpnext/stock/onboarding_step/create_a_warehouse/create_a_warehouse.json @@ -9,7 +9,7 @@ "is_complete": 0, "is_single": 0, "is_skipped": 0, - "modified": "2021-06-18 13:52:51.126984", + "modified": "2021-08-18 12:23:36.675572", "modified_by": "Administrator", "name": "Create a Warehouse", "owner": "Administrator", diff --git a/erpnext/stock/onboarding_step/stock_settings/stock_settings.json b/erpnext/stock/onboarding_step/stock_settings/stock_settings.json index f7238da334..2cf90e806c 100644 --- a/erpnext/stock/onboarding_step/stock_settings/stock_settings.json +++ b/erpnext/stock/onboarding_step/stock_settings/stock_settings.json @@ -9,7 +9,7 @@ "is_complete": 0, "is_single": 1, "is_skipped": 0, - "modified": "2021-06-18 14:13:12.678178", + "modified": "2021-08-18 12:06:51.139387", "modified_by": "Administrator", "name": "Stock Settings", "owner": "Administrator", diff --git a/erpnext/stock/onboarding_step/view_stock_ledger/view_stock_ledger.json b/erpnext/stock/onboarding_step/view_stock_ledger/view_stock_ledger.json deleted file mode 100644 index 7a5f164e01..0000000000 --- a/erpnext/stock/onboarding_step/view_stock_ledger/view_stock_ledger.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "action": "View Report", - "action_label": "Check Stock Ledger", - "creation": "2021-05-17 16:16:10.727959", - "description": "# Check Stock Reports\nBased on the various stock transactions, you can get a host of one-click Stock Reports in ERPNext like Stock Ledger, Stock Balance, Projected Quantity, and Ageing analysis.", - "docstatus": 0, - "doctype": "Onboarding Step", - "idx": 0, - "is_complete": 0, - "is_single": 0, - "is_skipped": 0, - "modified": "2021-06-18 14:09:39.399311", - "modified_by": "Administrator", - "name": "View Stock Ledger", - "owner": "Administrator", - "reference_report": "Stock Ledger", - "report_description": "You can set the filters to see entries specific to the previous steps.", - "report_reference_doctype": "Stock Ledger Entry", - "report_type": "Script Report", - "show_form_tour": 0, - "show_full_form": 0, - "title": "Check Stock Reports", - "validate_action": 1 -} \ No newline at end of file diff --git a/erpnext/stock/onboarding_step/view_stock_balance/view_stock_balance.json b/erpnext/stock/onboarding_step/view_stock_projected_qty/view_stock_projected_qty.json similarity index 69% rename from erpnext/stock/onboarding_step/view_stock_balance/view_stock_balance.json rename to erpnext/stock/onboarding_step/view_stock_projected_qty/view_stock_projected_qty.json index ed5fe36945..e684780751 100644 --- a/erpnext/stock/onboarding_step/view_stock_balance/view_stock_balance.json +++ b/erpnext/stock/onboarding_step/view_stock_projected_qty/view_stock_projected_qty.json @@ -1,7 +1,7 @@ { "action": "View Report", - "action_label": "Check Stock Balance", - "creation": "2021-05-17 16:15:54.617572", + "action_label": "Check Stock Projected Qty", + "creation": "2021-08-20 14:38:41.649103", "description": "# Check Stock Reports\nBased on the various stock transactions, you can get a host of one-click Stock Reports in ERPNext like Stock Ledger, Stock Balance, Projected Quantity, and Ageing analysis.", "docstatus": 0, "doctype": "Onboarding Step", @@ -9,16 +9,16 @@ "is_complete": 0, "is_single": 0, "is_skipped": 0, - "modified": "2021-06-18 14:41:18.499215", + "modified": "2021-08-20 14:38:41.649103", "modified_by": "Administrator", - "name": "View Stock Balance", + "name": "View Stock Projected Qty", "owner": "Administrator", - "reference_report": "Stock Balance", + "reference_report": "Stock Projected Qty", "report_description": "You can set the filters to narrow the results, then click on Generate New Report to see the updated report.", - "report_reference_doctype": "Stock Ledger Entry", + "report_reference_doctype": "Item", "report_type": "Script Report", "show_form_tour": 0, "show_full_form": 0, - "title": "Check Stock Reports", + "title": "Check Stock Projected Qty", "validate_action": 1 } \ No newline at end of file From f4487c3ae540c7afa790a0a906e63ca95e736fde Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Fri, 20 Aug 2021 16:25:20 +0530 Subject: [PATCH 20/65] refactor: use Form Tour doc instead of controller form tour note - keeping controller form tours as a fallback, new form tours seem to work only for Stock Settings --- .../create_a_stock_entry.json | 56 ++++++++++++ .../create_a_warehouse.json | 54 +++++++++++ .../create_warehouse/create_warehouse.json | 54 +++++++++++ .../stock_opening_balance.json | 55 ++++++++++++ .../stock_settings/stock_settings.json | 89 +++++++++++++++++++ 5 files changed, 308 insertions(+) create mode 100644 erpnext/stock/form_tour/create_a_stock_entry/create_a_stock_entry.json create mode 100644 erpnext/stock/form_tour/create_a_warehouse/create_a_warehouse.json create mode 100644 erpnext/stock/form_tour/create_warehouse/create_warehouse.json create mode 100644 erpnext/stock/form_tour/stock_opening_balance/stock_opening_balance.json create mode 100644 erpnext/stock/form_tour/stock_settings/stock_settings.json diff --git a/erpnext/stock/form_tour/create_a_stock_entry/create_a_stock_entry.json b/erpnext/stock/form_tour/create_a_stock_entry/create_a_stock_entry.json new file mode 100644 index 0000000000..fbc4b558bc --- /dev/null +++ b/erpnext/stock/form_tour/create_a_stock_entry/create_a_stock_entry.json @@ -0,0 +1,56 @@ +{ + "creation": "2021-08-20 16:02:59.314742", + "docstatus": 0, + "doctype": "Form Tour", + "idx": 0, + "is_standard": 1, + "modified": "2021-08-20 16:05:40.593997", + "modified_by": "Administrator", + "module": "Stock", + "name": "Create a Stock Entry", + "owner": "Administrator", + "reference_doctype": "Stock Entry", + "save_on_complete": 1, + "steps": [ + { + "description": "Select the type of Stock Entry to be made. For now, to receive stock into a warehouses select Material Receipt.", + "field": "", + "fieldname": "stock_entry_type", + "fieldtype": "Link", + "has_next_condition": 1, + "is_table_field": 0, + "label": "Stock Entry Type", + "next_step_condition": "eval: doc.stock_entry_type === \"Material Receipt\"", + "parent_field": "", + "position": "Top", + "title": "Stock Entry Type" + }, + { + "description": "Select a target warehouse where the stock will be received.", + "field": "", + "fieldname": "to_warehouse", + "fieldtype": "Link", + "has_next_condition": 1, + "is_table_field": 0, + "label": "Default Target Warehouse", + "next_step_condition": "eval: doc.to_warehouse", + "parent_field": "", + "position": "Top", + "title": "Default Target Warehouse" + }, + { + "description": "Select an item and entry quantity to be delivered.", + "field": "", + "fieldname": "items", + "fieldtype": "Table", + "has_next_condition": 1, + "is_table_field": 0, + "label": "Items", + "next_step_condition": "eval: doc.items[0]?.item_code", + "parent_field": "", + "position": "Bottom", + "title": "Items" + } + ], + "title": "Create a Stock Entry" +} \ No newline at end of file diff --git a/erpnext/stock/form_tour/create_a_warehouse/create_a_warehouse.json b/erpnext/stock/form_tour/create_a_warehouse/create_a_warehouse.json new file mode 100644 index 0000000000..e9a598b4b1 --- /dev/null +++ b/erpnext/stock/form_tour/create_a_warehouse/create_a_warehouse.json @@ -0,0 +1,54 @@ +{ + "creation": "2021-08-20 15:56:10.157109", + "docstatus": 0, + "doctype": "Form Tour", + "idx": 0, + "is_standard": 1, + "modified": "2021-08-20 15:56:10.157109", + "modified_by": "Administrator", + "module": "Stock", + "name": "Create a Warehouse", + "owner": "Administrator", + "reference_doctype": "Warehouse", + "save_on_complete": 1, + "steps": [ + { + "description": "Select a name for the warehouse. This should reflect its location or purpose.", + "field": "", + "fieldname": "warehouse_name", + "fieldtype": "Data", + "has_next_condition": 1, + "is_table_field": 0, + "label": "Warehouse Name", + "next_step_condition": "eval: doc.warehouse_name", + "parent_field": "", + "position": "Bottom", + "title": "Warehouse Name" + }, + { + "description": "Select a warehouse type to categorize the warehouse into a sub-group.", + "field": "", + "fieldname": "warehouse_type", + "fieldtype": "Link", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Warehouse Type", + "parent_field": "", + "position": "Top", + "title": "Warehouse Type" + }, + { + "description": "Select an account to set a default account for all transactions with this warehouse.", + "field": "", + "fieldname": "account", + "fieldtype": "Link", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Account", + "parent_field": "", + "position": "Top", + "title": "Account" + } + ], + "title": "Create a Warehouse" +} \ No newline at end of file diff --git a/erpnext/stock/form_tour/create_warehouse/create_warehouse.json b/erpnext/stock/form_tour/create_warehouse/create_warehouse.json new file mode 100644 index 0000000000..f25e3a1cf3 --- /dev/null +++ b/erpnext/stock/form_tour/create_warehouse/create_warehouse.json @@ -0,0 +1,54 @@ +{ + "creation": "2021-08-20 15:42:47.059290", + "docstatus": 0, + "doctype": "Form Tour", + "idx": 0, + "is_standard": 1, + "modified": "2021-08-20 15:54:58.440497", + "modified_by": "Administrator", + "module": "Stock", + "name": "Create Warehouse", + "owner": "Administrator", + "reference_doctype": "Warehouse", + "save_on_complete": 1, + "steps": [ + { + "description": "Select a name for the warehouse. This should reflect its location or purpose.", + "field": "", + "fieldname": "warehouse_name", + "fieldtype": "Data", + "has_next_condition": 1, + "is_table_field": 0, + "label": "Warehouse Name", + "next_step_condition": "eval: doc.warehouse_name", + "parent_field": "", + "position": "Bottom", + "title": "Warehouse Name" + }, + { + "description": "Select a warehouse type to categorize the warehouse into a sub-group.", + "field": "", + "fieldname": "warehouse_type", + "fieldtype": "Link", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Warehouse Type", + "parent_field": "", + "position": "Top", + "title": "Warehouse Type" + }, + { + "description": "Select an account to set a default account for all transactions with this warehouse.", + "field": "", + "fieldname": "account", + "fieldtype": "Link", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Account", + "parent_field": "", + "position": "Top", + "title": "Account" + } + ], + "title": "Create Warehouse" +} \ No newline at end of file diff --git a/erpnext/stock/form_tour/stock_opening_balance/stock_opening_balance.json b/erpnext/stock/form_tour/stock_opening_balance/stock_opening_balance.json new file mode 100644 index 0000000000..94bdbbc442 --- /dev/null +++ b/erpnext/stock/form_tour/stock_opening_balance/stock_opening_balance.json @@ -0,0 +1,55 @@ +{ + "creation": "2021-08-20 16:08:39.298267", + "docstatus": 0, + "doctype": "Form Tour", + "idx": 0, + "is_standard": 1, + "modified": "2021-08-20 16:08:39.298267", + "modified_by": "Administrator", + "module": "Stock", + "name": "Stock Opening Balance", + "owner": "Administrator", + "reference_doctype": "Stock Reconciliation", + "save_on_complete": 1, + "steps": [ + { + "description": "Set Purpose to Opening Stock to set the stock opening balance.", + "field": "", + "fieldname": "purpose", + "fieldtype": "Select", + "has_next_condition": 1, + "is_table_field": 0, + "label": "Purpose", + "next_step_condition": "eval: doc.purpose === \"Opening Stock\"", + "parent_field": "", + "position": "Top", + "title": "Purpose" + }, + { + "description": "Edit the Posting Date by clicking on the Edit Posting Date and Time checkbox below.", + "field": "", + "fieldname": "posting_date", + "fieldtype": "Date", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Posting Date", + "parent_field": "", + "position": "Bottom", + "title": "Posting Date" + }, + { + "description": "Select the items for which the opening stock has to be set.", + "field": "", + "fieldname": "items", + "fieldtype": "Table", + "has_next_condition": 1, + "is_table_field": 0, + "label": "Items", + "next_step_condition": "eval: doc.items[0]?.item_code", + "parent_field": "", + "position": "Bottom", + "title": "Items" + } + ], + "title": "Stock Opening Balance" +} \ No newline at end of file diff --git a/erpnext/stock/form_tour/stock_settings/stock_settings.json b/erpnext/stock/form_tour/stock_settings/stock_settings.json new file mode 100644 index 0000000000..282b87c21f --- /dev/null +++ b/erpnext/stock/form_tour/stock_settings/stock_settings.json @@ -0,0 +1,89 @@ +{ + "creation": "2021-08-20 15:20:59.336585", + "docstatus": 0, + "doctype": "Form Tour", + "idx": 0, + "is_standard": 1, + "modified": "2021-08-20 15:37:36.151783", + "modified_by": "Administrator", + "module": "Stock", + "name": "Stock Settings", + "owner": "Administrator", + "reference_doctype": "Stock Settings", + "save_on_complete": 1, + "steps": [ + { + "description": "By default, the Item Name is set as per the Item Code entered. If you want Items to be named by a Naming Series choose the 'Naming Series' option.", + "field": "", + "fieldname": "item_naming_by", + "fieldtype": "Select", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Item Naming By", + "parent_field": "", + "position": "Bottom", + "title": "Item Naming By" + }, + { + "description": "Set a Default Warehouse for Inventory Transactions. This will be fetched into the Default Warehouse in the Item master.", + "field": "", + "fieldname": "default_warehouse", + "fieldtype": "Link", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Default Warehouse", + "parent_field": "", + "position": "Bottom", + "title": "Default Warehouse" + }, + { + "description": "Choose between FIFO and Moving Average Valuation Methods. Click here to know more about them.", + "field": "", + "fieldname": "valuation_method", + "fieldtype": "Select", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Default Valuation Method", + "parent_field": "", + "position": "Bottom", + "title": "Default Valuation Method" + }, + { + "description": "Show 'Scan Barcode' field above every child table to insert Items with ease.", + "field": "", + "fieldname": "show_barcode_field", + "fieldtype": "Check", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Show Barcode Field in Stock Transactions", + "parent_field": "", + "position": "Bottom", + "title": "Show Barcode Field" + }, + { + "description": "Quality inspection is performed on the inward and outward movement of goods. Receipt and delivery transactions will be stopped or the user will be warned if the quality inspection is not performed.", + "field": "", + "fieldname": "action_if_quality_inspection_is_not_submitted", + "fieldtype": "Select", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Action If Quality Inspection Is Not Submitted", + "parent_field": "", + "position": "Bottom", + "title": "Action if Quality Inspection Is Not Submitted" + }, + { + "description": "Serial numbers for stock will be set automatically based on the Items entered based on first in first out in transactions like Purchase/Sales Invoices, Delivery Notes, etc.", + "field": "", + "fieldname": "automatically_set_serial_nos_based_on_fifo", + "fieldtype": "Check", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Automatically Set Serial Nos Based on FIFO", + "parent_field": "", + "position": "Bottom", + "title": "Automatically Set Serial Nos based on FIFO" + } + ], + "title": "Stock Settings" +} \ No newline at end of file From 496bff5136c7a72c3346361af7b7e41e787cff1b Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Sun, 22 Aug 2021 18:10:51 +0530 Subject: [PATCH 21/65] feat: Column for total amount due in Accounts Receivable/Payable Summary (#27069) --- .../report/accounts_receivable/accounts_receivable.py | 2 ++ .../accounts_receivable_summary.py | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index b54646fd27..cedfc0f58b 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -535,6 +535,8 @@ class ReceivablePayableReport(object): if getdate(entry_date) > getdate(self.filters.report_date): row.range1 = row.range2 = row.range3 = row.range4 = row.range5 = 0.0 + row.total_due = row.range1 + row.range2 + row.range3 + row.range4 + row.range5 + def get_ageing_data(self, entry_date, row): # [0-30, 30-60, 60-90, 90-120, 120-above] row.range1 = row.range2 = row.range3 = row.range4 = row.range5 = 0.0 diff --git a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py index e94b30921f..4bfb022c4e 100644 --- a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py +++ b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py @@ -82,6 +82,7 @@ class AccountsReceivableSummary(ReceivablePayableReport): "range3": 0.0, "range4": 0.0, "range5": 0.0, + "total_due": 0.0, "sales_person": [] })) @@ -135,3 +136,6 @@ class AccountsReceivableSummary(ReceivablePayableReport): "{range3}-{range4}".format(range3=cint(self.filters["range3"])+ 1, range4=self.filters["range4"]), "{range4}-{above}".format(range4=cint(self.filters["range4"])+ 1, above=_("Above"))]): self.add_column(label=label, fieldname='range' + str(i+1)) + + # Add column for total due amount + self.add_column(label="Total Amount Due", fieldname='total_due') From 7d627df4dbe15b3db16da031945ab98ccaae71ea Mon Sep 17 00:00:00 2001 From: Subin Tom <36098155+nemesis189@users.noreply.github.com> Date: Mon, 23 Aug 2021 11:05:07 +0530 Subject: [PATCH 22/65] fix: pos return payment mode issue (#26872) --- erpnext/controllers/taxes_and_totals.py | 9 ++++----- erpnext/public/js/controllers/taxes_and_totals.js | 2 -- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 05edb2530c..7c6d3552f1 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -595,7 +595,8 @@ class calculate_taxes_and_totals(object): self.doc.precision("outstanding_amount")) if self.doc.doctype == 'Sales Invoice' and self.doc.get('is_pos') and self.doc.get('is_return'): - self.update_paid_amount_for_return(total_amount_to_pay) + self.set_total_amount_to_default_mop(total_amount_to_pay) + self.calculate_paid_amount() def calculate_paid_amount(self): @@ -675,7 +676,7 @@ class calculate_taxes_and_totals(object): def set_item_wise_tax_breakup(self): self.doc.other_charges_calculation = get_itemised_tax_breakup_html(self.doc) - def update_paid_amount_for_return(self, total_amount_to_pay): + def set_total_amount_to_default_mop(self, total_amount_to_pay): default_mode_of_payment = frappe.db.get_value('POS Payment Method', {'parent': self.doc.pos_profile, 'default': 1}, ['mode_of_payment'], as_dict=1) @@ -685,9 +686,7 @@ class calculate_taxes_and_totals(object): 'mode_of_payment': default_mode_of_payment.mode_of_payment, 'amount': total_amount_to_pay, 'default': 1 - }) - - self.calculate_paid_amount() + }) def get_itemised_tax_breakup_html(doc): if not doc.taxes: diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index e8f31225ba..702064fe55 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -754,8 +754,6 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { } }); this.frm.refresh_fields(); - - this.calculate_paid_amount(); } set_default_payment(total_amount_to_pay, update_paid_amount) { From 8b2fe9e793e226339bbcec554d8c57a09f87b5c5 Mon Sep 17 00:00:00 2001 From: Subin Tom <36098155+nemesis189@users.noreply.github.com> Date: Mon, 23 Aug 2021 11:17:31 +0530 Subject: [PATCH 23/65] fix: eway bill version changed to 1.0.0421 (#27044) --- erpnext/regional/india/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 4e4dcf8585..ce5aa10902 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -475,7 +475,7 @@ def get_ewb_data(dt, dn): ewaybills.append(data) data = { - 'version': '1.0.1118', + 'version': '1.0.0421', 'billLists': ewaybills } From 098d349bf4181100111e2b01025ce7d939b96392 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 23 Aug 2021 13:36:13 +0530 Subject: [PATCH 24/65] fix: stock ledger report not working if include uom selected in filter --- erpnext/stock/report/stock_ledger/stock_ledger.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.py b/erpnext/stock/report/stock_ledger/stock_ledger.py index 8909f217f4..b6923e97c4 100644 --- a/erpnext/stock/report/stock_ledger/stock_ledger.py +++ b/erpnext/stock/report/stock_ledger/stock_ledger.py @@ -23,6 +23,7 @@ def execute(filters=None): conversion_factors = [] if opening_row: data.append(opening_row) + conversion_factors.append(0) actual_qty = stock_value = 0 From e1f070437ae5869ac43896730cccb4b4c904e7de Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 23 Aug 2021 14:27:55 +0530 Subject: [PATCH 25/65] fix: selected batch no changed on updation of qty --- erpnext/selling/sales_common.js | 4 ++++ erpnext/stock/get_item_details.py | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/erpnext/selling/sales_common.js b/erpnext/selling/sales_common.js index 22bf3fc94f..2de57c87f1 100644 --- a/erpnext/selling/sales_common.js +++ b/erpnext/selling/sales_common.js @@ -394,6 +394,10 @@ erpnext.selling.SellingController = class SellingController extends erpnext.Tran } _set_batch_number(doc) { + if (doc.batch_no) { + return + } + let args = {'item_code': doc.item_code, 'warehouse': doc.warehouse, 'qty': flt(doc.qty) * flt(doc.conversion_factor)}; if (doc.has_serial_no && doc.serial_no) { args['serial_no'] = doc.serial_no diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index a0fbcecc5d..c72073c614 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -278,6 +278,10 @@ def get_basic_details(args, item, overwrite_warehouse=True): else: args.uom = item.stock_uom + if (args.get("batch_no") and + item.name != frappe.get_cached_value('Batch', args.get("batch_no"), 'item')): + args['batch_no'] = '' + out = frappe._dict({ "item_code": item.name, "item_name": item.item_name, From 47b63a627d7d8b6d32cc5ff5713af9eb973001d3 Mon Sep 17 00:00:00 2001 From: Subin Tom <36098155+nemesis189@users.noreply.github.com> Date: Mon, 23 Aug 2021 15:38:26 +0530 Subject: [PATCH 26/65] fix: Eway bill test update to check ver 1.0.0421 (#27083) --- erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 984a652248..01a6b02732 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -2014,7 +2014,7 @@ class TestSalesInvoice(unittest.TestCase): data = get_ewb_data("Sales Invoice", [si.name]) - self.assertEqual(data['version'], '1.0.1118') + self.assertEqual(data['version'], '1.0.0421') self.assertEqual(data['billLists'][0]['fromGstin'], '27AAECE4835E1ZR') self.assertEqual(data['billLists'][0]['fromTrdName'], '_Test Company') self.assertEqual(data['billLists'][0]['toTrdName'], '_Test Customer') From bd709f8ab07529b3cb8fec35655040e619f11c9e Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 23 Aug 2021 19:05:52 +0530 Subject: [PATCH 27/65] fix: Ignore due date validations if payment terms are copied from orders/receipts --- erpnext/controllers/accounts_controller.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 8addbeb98f..e710a65891 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -159,7 +159,8 @@ class AccountsController(TransactionBase): self.set_due_date() self.set_payment_schedule() self.validate_payment_schedule_amount() - self.validate_due_date() + if not self.get('ignore_default_payment_terms_template'): + self.validate_due_date() self.validate_advance_entries() def validate_non_invoice_documents_schedule(self): From 49e0a4f28714b468ba5edad296b30f22991210f3 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Tue, 24 Aug 2021 08:33:57 +0530 Subject: [PATCH 28/65] Revert "fix: Salary component account filter (#26604)" This reverts commit ae9d1d9617015b6e2714a9fcd027a2957053d99a. --- .../doctype/salary_component/salary_component.js | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/erpnext/payroll/doctype/salary_component/salary_component.js b/erpnext/payroll/doctype/salary_component/salary_component.js index e9e6f81862..dbf75140ac 100644 --- a/erpnext/payroll/doctype/salary_component/salary_component.js +++ b/erpnext/payroll/doctype/salary_component/salary_component.js @@ -4,18 +4,11 @@ frappe.ui.form.on('Salary Component', { setup: function(frm) { frm.set_query("account", "accounts", function(doc, cdt, cdn) { - let d = frappe.get_doc(cdt, cdn); - - let root_type = "Liability"; - if (frm.doc.type == "Deduction") { - root_type = "Expense"; - } - + var d = locals[cdt][cdn]; return { filters: { "is_group": 0, - "company": d.company, - "root_type": root_type + "company": d.company } }; }); From 332ac105b5ffec04009539eba94b153a8b93c0c4 Mon Sep 17 00:00:00 2001 From: Pruthvi Patel Date: Tue, 24 Aug 2021 12:07:38 +0530 Subject: [PATCH 29/65] refactor: use `read_only_depends_on` instead of code (#27008) --- erpnext/accounts/doctype/pos_invoice/pos_invoice.js | 6 +----- erpnext/accounts/doctype/pos_invoice/pos_invoice.json | 5 +++-- erpnext/accounts/doctype/sales_invoice/sales_invoice.js | 8 +------- erpnext/accounts/doctype/sales_invoice/sales_invoice.json | 5 +++-- 4 files changed, 8 insertions(+), 16 deletions(-) diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.js b/erpnext/accounts/doctype/pos_invoice/pos_invoice.js index 181e9f8ec0..e317546481 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.js +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.js @@ -111,16 +111,12 @@ erpnext.selling.POSInvoiceController = class POSInvoiceController extends erpnex } write_off_outstanding_amount_automatically() { - if(cint(this.frm.doc.write_off_outstanding_amount_automatically)) { + if (cint(this.frm.doc.write_off_outstanding_amount_automatically)) { frappe.model.round_floats_in(this.frm.doc, ["grand_total", "paid_amount"]); // this will make outstanding amount 0 this.frm.set_value("write_off_amount", flt(this.frm.doc.grand_total - this.frm.doc.paid_amount - this.frm.doc.total_advance, precision("write_off_amount")) ); - this.frm.toggle_enable("write_off_amount", false); - - } else { - this.frm.toggle_enable("write_off_amount", true); } this.calculate_outstanding_amount(false); diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json index fcccb39b70..3e22b9e00a 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json @@ -1183,7 +1183,8 @@ "label": "Write Off Amount", "no_copy": 1, "options": "currency", - "print_hide": 1 + "print_hide": 1, + "read_only_depends_on": "eval: doc.write_off_outstanding_amount_automatically" }, { "fieldname": "base_write_off_amount", @@ -1554,7 +1555,7 @@ "icon": "fa fa-file-text", "is_submittable": 1, "links": [], - "modified": "2021-08-17 20:13:44.255437", + "modified": "2021-08-18 16:13:52.080543", "modified_by": "Administrator", "module": "Accounts", "name": "POS Invoice", diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 8d65101b3b..2071827d99 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -324,16 +324,12 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e } write_off_outstanding_amount_automatically() { - if(cint(this.frm.doc.write_off_outstanding_amount_automatically)) { + if (cint(this.frm.doc.write_off_outstanding_amount_automatically)) { frappe.model.round_floats_in(this.frm.doc, ["grand_total", "paid_amount"]); // this will make outstanding amount 0 this.frm.set_value("write_off_amount", flt(this.frm.doc.grand_total - this.frm.doc.paid_amount - this.frm.doc.total_advance, precision("write_off_amount")) ); - this.frm.toggle_enable("write_off_amount", false); - - } else { - this.frm.toggle_enable("write_off_amount", true); } this.calculate_outstanding_amount(false); @@ -787,8 +783,6 @@ frappe.ui.form.on('Sales Invoice', { if (frappe.boot.sysdefaults.country == 'India') unhide_field(['c_form_applicable', 'c_form_no']); else hide_field(['c_form_applicable', 'c_form_no']); - frm.toggle_enable("write_off_amount", !!!cint(doc.write_off_outstanding_amount_automatically)); - frm.refresh_fields(); }, diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index e317443b91..5023c9c61a 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -1444,7 +1444,8 @@ "label": "Write Off Amount", "no_copy": 1, "options": "currency", - "print_hide": 1 + "print_hide": 1, + "read_only_depends_on": "eval:doc.write_off_outstanding_amount_automatically" }, { "fieldname": "base_write_off_amount", @@ -2014,7 +2015,7 @@ "link_fieldname": "consolidated_invoice" } ], - "modified": "2021-08-17 20:16:12.737743", + "modified": "2021-08-18 16:07:45.122570", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice", From 0dff0beabaa937e397eae2f930bae337cfe8af82 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 24 Aug 2021 12:16:46 +0530 Subject: [PATCH 30/65] fix: stock analytics report date range issues and add company filter (#27014) * test: tests for correct get_period_date_ranges * fix: stock analytics report date range issues - Upon selecting second half of month with Monthly filter, data from that period was missing. - Solution: "round down" the date as per expected frequency. * chore: drop py2 and fix misleading docstring * test: fix test to avoid FY clash * feat: add company filter in stock analytics report [skip ci] Co-authored-by: Marica --- .../report/stock_analytics/stock_analytics.js | 16 +++++++- .../report/stock_analytics/stock_analytics.py | 37 ++++++++++++++++--- .../stock_analytics/test_stock_analytics.py | 35 ++++++++++++++++++ 3 files changed, 82 insertions(+), 6 deletions(-) create mode 100644 erpnext/stock/report/stock_analytics/test_stock_analytics.py diff --git a/erpnext/stock/report/stock_analytics/stock_analytics.js b/erpnext/stock/report/stock_analytics/stock_analytics.js index 6b384e2861..78afe6d264 100644 --- a/erpnext/stock/report/stock_analytics/stock_analytics.js +++ b/erpnext/stock/report/stock_analytics/stock_analytics.js @@ -36,12 +36,26 @@ frappe.query_reports["Stock Analytics"] = { options:"Brand", default: "", }, + { + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_user_default("Company"), + reqd: 1, + }, { fieldname: "warehouse", label: __("Warehouse"), fieldtype: "Link", - options:"Warehouse", + options: "Warehouse", default: "", + get_query: function() { + const company = frappe.query_report.get_filter_value('company'); + return { + filters: { 'company': company } + } + } }, { fieldname: "from_date", diff --git a/erpnext/stock/report/stock_analytics/stock_analytics.py b/erpnext/stock/report/stock_analytics/stock_analytics.py index d62abed91f..a1e1e7fce7 100644 --- a/erpnext/stock/report/stock_analytics/stock_analytics.py +++ b/erpnext/stock/report/stock_analytics/stock_analytics.py @@ -1,14 +1,15 @@ # Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt +import datetime -from __future__ import unicode_literals import frappe from frappe import _, scrub -from frappe.utils import getdate, flt +from frappe.utils import getdate, get_quarter_start, get_first_day_of_week +from frappe.utils import get_first_day as get_first_day_of_month + from erpnext.stock.report.stock_balance.stock_balance import (get_items, get_stock_ledger_entries, get_item_details) from erpnext.accounts.utils import get_fiscal_year from erpnext.stock.utils import is_reposting_item_valuation_in_progress -from six import iteritems def execute(filters=None): is_reposting_item_valuation_in_progress() @@ -71,7 +72,8 @@ def get_columns(filters): def get_period_date_ranges(filters): from dateutil.relativedelta import relativedelta - from_date, to_date = getdate(filters.from_date), getdate(filters.to_date) + from_date = round_down_to_nearest_frequency(filters.from_date, filters.range) + to_date = getdate(filters.to_date) increment = { "Monthly": 1, @@ -97,6 +99,31 @@ def get_period_date_ranges(filters): return periodic_daterange + +def round_down_to_nearest_frequency(date: str, frequency: str) -> datetime.datetime: + """Rounds down the date to nearest frequency unit. + example: + + >>> round_down_to_nearest_frequency("2021-02-21", "Monthly") + datetime.datetime(2021, 2, 1) + + >>> round_down_to_nearest_frequency("2021-08-21", "Yearly") + datetime.datetime(2021, 1, 1) + """ + + def _get_first_day_of_fiscal_year(date): + fiscal_year = get_fiscal_year(date) + return fiscal_year and fiscal_year[1] or date + + round_down_function = { + "Monthly": get_first_day_of_month, + "Quarterly": get_quarter_start, + "Weekly": get_first_day_of_week, + "Yearly": _get_first_day_of_fiscal_year, + }.get(frequency, getdate) + return round_down_function(date) + + def get_period(posting_date, filters): months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] @@ -177,7 +204,7 @@ def get_data(filters): periodic_data = get_periodic_data(sle, filters) ranges = get_period_date_ranges(filters) - for dummy, item_data in iteritems(item_details): + for dummy, item_data in item_details.items(): row = { "name": item_data.name, "item_name": item_data.item_name, diff --git a/erpnext/stock/report/stock_analytics/test_stock_analytics.py b/erpnext/stock/report/stock_analytics/test_stock_analytics.py new file mode 100644 index 0000000000..00e268b4e0 --- /dev/null +++ b/erpnext/stock/report/stock_analytics/test_stock_analytics.py @@ -0,0 +1,35 @@ +import datetime +import unittest + +from frappe import _dict +from erpnext.accounts.utils import get_fiscal_year + +from erpnext.stock.report.stock_analytics.stock_analytics import get_period_date_ranges + + +class TestStockAnalyticsReport(unittest.TestCase): + def test_get_period_date_ranges(self): + + filters = _dict(range="Monthly", from_date="2020-12-28", to_date="2021-02-06") + + ranges = get_period_date_ranges(filters) + + expected_ranges = [ + [datetime.date(2020, 12, 1), datetime.date(2020, 12, 31)], + [datetime.date(2021, 1, 1), datetime.date(2021, 1, 31)], + [datetime.date(2021, 2, 1), datetime.date(2021, 2, 6)], + ] + + self.assertEqual(ranges, expected_ranges) + + def test_get_period_date_ranges_yearly(self): + + filters = _dict(range="Yearly", from_date="2021-01-28", to_date="2021-02-06") + + ranges = get_period_date_ranges(filters) + first_date = get_fiscal_year("2021-01-28")[1] + expected_ranges = [ + [first_date, datetime.date(2021, 2, 6)], + ] + + self.assertEqual(ranges, expected_ranges) From 6de7b8ea93e3ffe621976ebf3b613406a379216d Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 24 Aug 2021 12:18:40 +0530 Subject: [PATCH 31/65] fix: discard empty rows from update items (#27021) --- erpnext/controllers/accounts_controller.py | 5 +++++ erpnext/public/js/utils.js | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 8addbeb98f..01d354df81 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1836,6 +1836,11 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil for d in data: new_child_flag = False + + if not d.get("item_code"): + # ignore empty rows + continue + if not d.get("docname"): new_child_flag = True check_doc_permissions(parent, 'create') diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index f1b9235fe3..f240b8ca91 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -563,7 +563,7 @@ erpnext.utils.update_child_items = function(opts) { }, ], primary_action: function() { - const trans_items = this.get_values()["trans_items"]; + const trans_items = this.get_values()["trans_items"].filter((item) => !!item.item_code); frappe.call({ method: 'erpnext.controllers.accounts_controller.update_child_qty_rate', freeze: true, From c09d8a28098ca2963122373ae18f0c8c1954fcf3 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 24 Aug 2021 12:20:34 +0530 Subject: [PATCH 32/65] fix(ux): keep stock entry title & purpose in sync (#27043) Co-authored-by: rohitwaghchaure --- erpnext/stock/doctype/stock_entry/stock_entry.json | 4 +--- erpnext/stock/doctype/stock_entry/stock_entry.py | 9 +++++++++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.json b/erpnext/stock/doctype/stock_entry/stock_entry.json index e6ce3c851f..2f37778896 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.json +++ b/erpnext/stock/doctype/stock_entry/stock_entry.json @@ -84,8 +84,6 @@ "oldfieldtype": "Section Break" }, { - "allow_on_submit": 1, - "default": "{purpose}", "fieldname": "title", "fieldtype": "Data", "hidden": 1, @@ -630,7 +628,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-08-17 20:16:12.737743", + "modified": "2021-08-20 19:19:31.514846", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry", diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 90a33d3617..0b4592c12f 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -58,6 +58,7 @@ class StockEntry(StockController): self.validate_posting_time() self.validate_purpose() + self.set_title() self.validate_item() self.validate_customer_provided_item() self.validate_qty() @@ -1608,6 +1609,14 @@ class StockEntry(StockController): return sorted(list(set(get_serial_nos(self.pro_doc.serial_no)) - set(used_serial_nos))) + def set_title(self): + if frappe.flags.in_import and self.title: + # Allow updating title during data import/update + return + + self.title = self.purpose + + @frappe.whitelist() def move_sample_to_retention_warehouse(company, items): if isinstance(items, string_types): From 1e05c9467bc5fa9315a67c7f8ed9ff5b17baa3e6 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Tue, 24 Aug 2021 14:21:14 +0530 Subject: [PATCH 33/65] fix: Use remove_all from file_manager --- erpnext/regional/italy/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/regional/italy/utils.py b/erpnext/regional/italy/utils.py index ba1aeafc3e..024f820568 100644 --- a/erpnext/regional/italy/utils.py +++ b/erpnext/regional/italy/utils.py @@ -6,7 +6,7 @@ import frappe from frappe.utils import flt, cstr from erpnext.controllers.taxes_and_totals import get_itemised_tax from frappe import _ -from frappe.core.doctype.file.file import remove_file +from frappe.utils.file_manager import remove_file from six import string_types from frappe.desk.form.load import get_attachments from erpnext.regional.italy import state_codes From ad735522cb777431c9e85a900a24d38b6633a377 Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Tue, 24 Aug 2021 14:54:38 +0530 Subject: [PATCH 34/65] fix: rename form tours to doctype names, remove tours from js controllers --- .../stock/doctype/stock_entry/stock_entry.js | 17 ------ .../stock_reconciliation.js | 17 ------ .../doctype/stock_settings/stock_settings.js | 37 ------------- erpnext/stock/doctype/warehouse/warehouse.js | 18 ------- .../create_a_warehouse.json | 54 ------------------- .../stock_entry.json} | 8 +-- .../stock_reconciliation.json} | 8 +-- .../warehouse.json} | 8 +-- 8 files changed, 12 insertions(+), 155 deletions(-) delete mode 100644 erpnext/stock/form_tour/create_a_warehouse/create_a_warehouse.json rename erpnext/stock/form_tour/{create_a_stock_entry/create_a_stock_entry.json => stock_entry/stock_entry.json} (91%) rename erpnext/stock/form_tour/{stock_opening_balance/stock_opening_balance.json => stock_reconciliation/stock_reconciliation.json} (89%) rename erpnext/stock/form_tour/{create_warehouse/create_warehouse.json => warehouse/warehouse.json} (89%) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 5ea733c66d..8f34794db9 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -1102,20 +1102,3 @@ function check_should_not_attach_bom_items(bom_no) { $.extend(cur_frm.cscript, new erpnext.stock.StockEntry({frm: cur_frm})); -frappe.tour['Stock Entry'] = [ - { - fieldname: "stock_entry_type", - title: __("Stock Entry Type"), - description: __("Select the type of Stock Entry to be made. For now, to receive stock into a warehouses select") + ' ' + __("Material Receipt.") + "" - }, - { - fieldname: "to_warehouse", - title: __("Default Target Warehouse"), - description: __("Select a target warehouse where the stock will be received.") - }, - { - fieldname: "items", - title: __("Items"), - description: __("Select an item and entry quantity to be delivered.") - }, -] diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js index f91d0a740d..aa502a432d 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js @@ -303,20 +303,3 @@ erpnext.stock.StockReconciliation = class StockReconciliation extends erpnext.st cur_frm.cscript = new erpnext.stock.StockReconciliation({frm: cur_frm}); -frappe.tour['Stock Reconciliation'] = [ - { - fieldname: "purpose", - title: __("Purpose"), - description: __("Set Purpose to Opening Stock to set the stock opening balance.") - }, - { - fieldname: "posting_date", - title: __("Posting Date"), - description: __("Edit the Posting Date by clicking on the Edit Posting Date and Time checkbox below.") - }, - { - fieldname: "items", - title: __("Items"), - description: __("Select the items for which the opening stock has to be set.") - }, -]; diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.js b/erpnext/stock/doctype/stock_settings/stock_settings.js index 08606fdcaa..6167becdaa 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.js +++ b/erpnext/stock/doctype/stock_settings/stock_settings.js @@ -16,40 +16,3 @@ frappe.ui.form.on('Stock Settings', { } }); -frappe.tour['Stock Settings'] = [ - { - fieldname: "item_naming_by", - title: __("Item Naming By"), - description: __("By default, the Item Name is set as per the Item Code entered. If you want Items to be named by a") + " " + - "Naming Series" + " " + - __("choose the 'Naming Series' option."), - }, - { - fieldname: "default_warehouse", - title: __("Default Warehouse"), - description: __("Set a Default Warehouse for Inventory Transactions. This will be fetched into the Default Warehouse in the Item master.") - }, - { - fieldname: "valuation_method", - title: __("Valuation Method"), - description: __("Choose between FIFO and Moving Average Valuation Methods. Click") + " " + - "here" + " " + - __("to know more about them.") - }, - { - fieldname: "show_barcode_field", - title: __("Show Barcode Field"), - description: __("Show 'Scan Barcode' field above every child table to insert Items with ease.") - }, - { - fieldname: "action_if_quality_inspection_is_not_submitted", - title: __("Action if Quality Inspection Is Not Submitted"), - description: __("Quality inspection is performed on the inward and outward movement of goods. Receipt and delivery transactions will be stopped or the user will be warned if the quality inspection is not performed.") - - }, - { - fieldname: "automatically_set_serial_nos_based_on_fifo", - title: __("Automatically Set Serial Nos based on FIFO"), - description: __("Serial numbers for stock will be set automatically based on the Items entered based on first in first out in transactions like Purchase/Sales Invoices, Delivery Notes, etc.") - } -]; diff --git a/erpnext/stock/doctype/warehouse/warehouse.js b/erpnext/stock/doctype/warehouse/warehouse.js index b0c1c033a3..4e1679c411 100644 --- a/erpnext/stock/doctype/warehouse/warehouse.js +++ b/erpnext/stock/doctype/warehouse/warehouse.js @@ -87,21 +87,3 @@ function convert_to_group_or_ledger(frm){ }) } -frappe.tour['Warehouse'] = [ - { - fieldname: "warehouse_name", - title: __("Warehouse Name"), - description: __("Select name for the warehouse. This should reflect its location or purpose.") - }, - { - fieldname: "warehouse_type", - title: __("Warehouse Type"), - description: __("Select a warehouse type to categorize the warehouse into a sub-group.") - }, - { - fieldname: "account", - title: __("Account"), - description: __("Select an account to set a default account for all transactions with this warehouse.") - }, -]; - diff --git a/erpnext/stock/form_tour/create_a_warehouse/create_a_warehouse.json b/erpnext/stock/form_tour/create_a_warehouse/create_a_warehouse.json deleted file mode 100644 index e9a598b4b1..0000000000 --- a/erpnext/stock/form_tour/create_a_warehouse/create_a_warehouse.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "creation": "2021-08-20 15:56:10.157109", - "docstatus": 0, - "doctype": "Form Tour", - "idx": 0, - "is_standard": 1, - "modified": "2021-08-20 15:56:10.157109", - "modified_by": "Administrator", - "module": "Stock", - "name": "Create a Warehouse", - "owner": "Administrator", - "reference_doctype": "Warehouse", - "save_on_complete": 1, - "steps": [ - { - "description": "Select a name for the warehouse. This should reflect its location or purpose.", - "field": "", - "fieldname": "warehouse_name", - "fieldtype": "Data", - "has_next_condition": 1, - "is_table_field": 0, - "label": "Warehouse Name", - "next_step_condition": "eval: doc.warehouse_name", - "parent_field": "", - "position": "Bottom", - "title": "Warehouse Name" - }, - { - "description": "Select a warehouse type to categorize the warehouse into a sub-group.", - "field": "", - "fieldname": "warehouse_type", - "fieldtype": "Link", - "has_next_condition": 0, - "is_table_field": 0, - "label": "Warehouse Type", - "parent_field": "", - "position": "Top", - "title": "Warehouse Type" - }, - { - "description": "Select an account to set a default account for all transactions with this warehouse.", - "field": "", - "fieldname": "account", - "fieldtype": "Link", - "has_next_condition": 0, - "is_table_field": 0, - "label": "Account", - "parent_field": "", - "position": "Top", - "title": "Account" - } - ], - "title": "Create a Warehouse" -} \ No newline at end of file diff --git a/erpnext/stock/form_tour/create_a_stock_entry/create_a_stock_entry.json b/erpnext/stock/form_tour/stock_entry/stock_entry.json similarity index 91% rename from erpnext/stock/form_tour/create_a_stock_entry/create_a_stock_entry.json rename to erpnext/stock/form_tour/stock_entry/stock_entry.json index fbc4b558bc..98c5d62f10 100644 --- a/erpnext/stock/form_tour/create_a_stock_entry/create_a_stock_entry.json +++ b/erpnext/stock/form_tour/stock_entry/stock_entry.json @@ -1,13 +1,13 @@ { - "creation": "2021-08-20 16:02:59.314742", + "creation": "2021-08-24 14:44:22.292652", "docstatus": 0, "doctype": "Form Tour", "idx": 0, "is_standard": 1, - "modified": "2021-08-20 16:05:40.593997", + "modified": "2021-08-24 14:50:48.017420", "modified_by": "Administrator", "module": "Stock", - "name": "Create a Stock Entry", + "name": "Stock Entry", "owner": "Administrator", "reference_doctype": "Stock Entry", "save_on_complete": 1, @@ -52,5 +52,5 @@ "title": "Items" } ], - "title": "Create a Stock Entry" + "title": "Stock Entry" } \ No newline at end of file diff --git a/erpnext/stock/form_tour/stock_opening_balance/stock_opening_balance.json b/erpnext/stock/form_tour/stock_reconciliation/stock_reconciliation.json similarity index 89% rename from erpnext/stock/form_tour/stock_opening_balance/stock_opening_balance.json rename to erpnext/stock/form_tour/stock_reconciliation/stock_reconciliation.json index 94bdbbc442..83083b639a 100644 --- a/erpnext/stock/form_tour/stock_opening_balance/stock_opening_balance.json +++ b/erpnext/stock/form_tour/stock_reconciliation/stock_reconciliation.json @@ -1,13 +1,13 @@ { - "creation": "2021-08-20 16:08:39.298267", + "creation": "2021-08-24 14:44:46.770952", "docstatus": 0, "doctype": "Form Tour", "idx": 0, "is_standard": 1, - "modified": "2021-08-20 16:08:39.298267", + "modified": "2021-08-24 14:50:41.512219", "modified_by": "Administrator", "module": "Stock", - "name": "Stock Opening Balance", + "name": "Stock Reconciliation", "owner": "Administrator", "reference_doctype": "Stock Reconciliation", "save_on_complete": 1, @@ -51,5 +51,5 @@ "title": "Items" } ], - "title": "Stock Opening Balance" + "title": "Stock Reconciliation" } \ No newline at end of file diff --git a/erpnext/stock/form_tour/create_warehouse/create_warehouse.json b/erpnext/stock/form_tour/warehouse/warehouse.json similarity index 89% rename from erpnext/stock/form_tour/create_warehouse/create_warehouse.json rename to erpnext/stock/form_tour/warehouse/warehouse.json index f25e3a1cf3..23ff2aebba 100644 --- a/erpnext/stock/form_tour/create_warehouse/create_warehouse.json +++ b/erpnext/stock/form_tour/warehouse/warehouse.json @@ -1,13 +1,13 @@ { - "creation": "2021-08-20 15:42:47.059290", + "creation": "2021-08-24 14:43:44.465237", "docstatus": 0, "doctype": "Form Tour", "idx": 0, "is_standard": 1, - "modified": "2021-08-20 15:54:58.440497", + "modified": "2021-08-24 14:50:31.988256", "modified_by": "Administrator", "module": "Stock", - "name": "Create Warehouse", + "name": "Warehouse", "owner": "Administrator", "reference_doctype": "Warehouse", "save_on_complete": 1, @@ -50,5 +50,5 @@ "title": "Account" } ], - "title": "Create Warehouse" + "title": "Warehouse" } \ No newline at end of file From 6cf9254ee5d6599f3dfe1b696dbb51edc33b965c Mon Sep 17 00:00:00 2001 From: Saqib Date: Tue, 24 Aug 2021 15:26:44 +0530 Subject: [PATCH 35/65] fix: invalid imports (#27101) --- erpnext/regional/italy/utils.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/erpnext/regional/italy/utils.py b/erpnext/regional/italy/utils.py index ba1aeafc3e..56f609eb23 100644 --- a/erpnext/regional/italy/utils.py +++ b/erpnext/regional/italy/utils.py @@ -6,9 +6,8 @@ import frappe from frappe.utils import flt, cstr from erpnext.controllers.taxes_and_totals import get_itemised_tax from frappe import _ -from frappe.core.doctype.file.file import remove_file +from frappe.utils.file_manager import remove_file from six import string_types -from frappe.desk.form.load import get_attachments from erpnext.regional.italy import state_codes From 9e82c24b277d09b61dc94f345b173603bee97a02 Mon Sep 17 00:00:00 2001 From: Saqib Date: Tue, 24 Aug 2021 15:37:00 +0530 Subject: [PATCH 36/65] fix: pos closing entry cancellation test (#27099) * fix: pos closing entry cancellation test * fix: invalid imports * fix: sider issue --- .../pos_closing_entry/test_pos_closing_entry.py | 10 ++++++++-- erpnext/accounts/doctype/pos_invoice/pos_invoice.js | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.py b/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.py index b596c0cf25..5b18ebb40d 100644 --- a/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.py +++ b/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.py @@ -85,9 +85,15 @@ class TestPOSClosingEntry(unittest.TestCase): pcv_doc.load_from_db() pcv_doc.cancel() - si_doc.load_from_db() + + cancelled_invoice = frappe.db.get_value( + 'POS Invoice Merge Log', {'pos_closing_entry': pcv_doc.name}, + 'consolidated_invoice' + ) + docstatus = frappe.db.get_value("Sales Invoice", cancelled_invoice, 'docstatus') + self.assertEqual(docstatus, 2) + pos_inv1.load_from_db() - self.assertEqual(si_doc.docstatus, 2) self.assertEqual(pos_inv1.status, 'Paid') diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.js b/erpnext/accounts/doctype/pos_invoice/pos_invoice.js index e317546481..15c292211c 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.js +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.js @@ -16,7 +16,7 @@ erpnext.selling.POSInvoiceController = class POSInvoiceController extends erpnex onload(doc) { super.onload(); - this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice Merge Log']; + this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice Merge Log', 'POS Closing Entry']; if(doc.__islocal && doc.is_pos && frappe.get_route_str() !== 'point-of-sale') { this.frm.script_manager.trigger("is_pos"); this.frm.refresh_fields(); From d4d5a4221a00771864d3b27eeb9a4f9580614c04 Mon Sep 17 00:00:00 2001 From: Subin Tom <36098155+nemesis189@users.noreply.github.com> Date: Tue, 24 Aug 2021 16:22:46 +0530 Subject: [PATCH 37/65] feat: coupon code discount in pos invoice (#27004) --- .../doctype/pos_invoice/pos_invoice.json | 9 +++++++++ .../doctype/pos_invoice/pos_invoice.py | 11 +++++++++++ .../doctype/pricing_rule/pricing_rule.py | 9 ++++++++- erpnext/public/js/controllers/transaction.js | 19 +++++++++++++------ erpnext/public/scss/point-of-sale.scss | 2 ++ 5 files changed, 43 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json index 3e22b9e00a..b819537400 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json @@ -99,6 +99,7 @@ "loyalty_redemption_account", "loyalty_redemption_cost_center", "section_break_49", + "coupon_code", "apply_discount_on", "base_discount_amount", "column_break_51", @@ -1550,6 +1551,14 @@ "no_copy": 1, "options": "Sales Invoice", "read_only": 1 + }, + { + "depends_on": "coupon_code", + "fieldname": "coupon_code", + "fieldtype": "Link", + "label": "Coupon Code", + "options": "Coupon Code", + "print_hide": 1 } ], "icon": "fa fa-file-text", diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index 8ec4ef224c..759cad53d4 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -44,6 +44,9 @@ class POSInvoice(SalesInvoice): self.validate_pos() self.validate_payment_amount() self.validate_loyalty_transaction() + if self.coupon_code: + from erpnext.accounts.doctype.pricing_rule.utils import validate_coupon_code + validate_coupon_code(self.coupon_code) def on_submit(self): # create the loyalty point ledger entry if the customer is enrolled in any loyalty program @@ -58,6 +61,10 @@ class POSInvoice(SalesInvoice): self.check_phone_payments() self.set_status(update=True) + if self.coupon_code: + from erpnext.accounts.doctype.pricing_rule.utils import update_coupon_code_count + update_coupon_code_count(self.coupon_code,'used') + def before_cancel(self): if self.consolidated_invoice and frappe.db.get_value('Sales Invoice', self.consolidated_invoice, 'docstatus') == 1: pos_closing_entry = frappe.get_all( @@ -84,6 +91,10 @@ class POSInvoice(SalesInvoice): against_psi_doc.delete_loyalty_point_entry() against_psi_doc.make_loyalty_point_entry() + if self.coupon_code: + from erpnext.accounts.doctype.pricing_rule.utils import update_coupon_code_count + update_coupon_code_count(self.coupon_code,'cancelled') + def check_phone_payments(self): for pay in self.payments: if pay.type == "Phone" and pay.amount >= 0: diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py index 556f49d34c..4903c50e17 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py @@ -198,12 +198,19 @@ def apply_pricing_rule(args, doc=None): set_serial_nos_based_on_fifo = frappe.db.get_single_value("Stock Settings", "automatically_set_serial_nos_based_on_fifo") + item_code_list = tuple(item.get('item_code') for item in item_list) + query_items = frappe.get_all('Item', fields=['item_code','has_serial_no'], filters=[['item_code','in',item_code_list]],as_list=1) + serialized_items = dict() + for item_code, val in query_items: + serialized_items.setdefault(item_code, val) + for item in item_list: args_copy = copy.deepcopy(args) args_copy.update(item) data = get_pricing_rule_for_item(args_copy, item.get('price_list_rate'), doc=doc) out.append(data) - if not item.get("serial_no") and set_serial_nos_based_on_fifo and not args.get('is_return'): + + if serialized_items.get(item.get('item_code')) and not item.get("serial_no") and set_serial_nos_based_on_fifo and not args.get('is_return'): out[0].update(get_serial_no_for_item(args_copy)) return out diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 9375e358a9..2538852bfa 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -2242,12 +2242,19 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe coupon_code() { var me = this; - frappe.run_serially([ - () => this.frm.doc.ignore_pricing_rule=1, - () => me.ignore_pricing_rule(), - () => this.frm.doc.ignore_pricing_rule=0, - () => me.apply_pricing_rule() - ]); + if (this.frm.doc.coupon_code) { + frappe.run_serially([ + () => this.frm.doc.ignore_pricing_rule=1, + () => me.ignore_pricing_rule(), + () => this.frm.doc.ignore_pricing_rule=0, + () => me.apply_pricing_rule() + ]); + } else { + frappe.run_serially([ + () => this.frm.doc.ignore_pricing_rule=1, + () => me.ignore_pricing_rule() + ]); + } } }; diff --git a/erpnext/public/scss/point-of-sale.scss b/erpnext/public/scss/point-of-sale.scss index c77b2ce3df..1677e9b3de 100644 --- a/erpnext/public/scss/point-of-sale.scss +++ b/erpnext/public/scss/point-of-sale.scss @@ -860,6 +860,8 @@ .invoice-fields { overflow-y: scroll; + height: 100%; + padding-right: var(--padding-sm); } } From ce129a141447f701240265e50c1de88b1ef46e12 Mon Sep 17 00:00:00 2001 From: Saqib Date: Tue, 24 Aug 2021 17:23:14 +0530 Subject: [PATCH 38/65] feat: re-arrange company doctype fields (#27091) --- erpnext/regional/india/setup.py | 2 + erpnext/setup/doctype/company/company.js | 78 +++++++------- erpnext/setup/doctype/company/company.json | 113 ++++++++++----------- 3 files changed, 99 insertions(+), 94 deletions(-) diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index a6ab6aba77..4db5551cb3 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -531,6 +531,7 @@ def make_custom_fields(update=True): fieldtype='Link', options='Salary Component', insert_after='hra_section'), dict(fieldname='hra_component', label='HRA Component', fieldtype='Link', options='Salary Component', insert_after='basic_component'), + dict(fieldname='hra_column_break', fieldtype='Column Break', insert_after='hra_component'), dict(fieldname='arrear_component', label='Arrear Component', fieldtype='Link', options='Salary Component', insert_after='hra_component'), dict(fieldname='non_profit_section', label='Non Profit Settings', @@ -539,6 +540,7 @@ def make_custom_fields(update=True): fieldtype='Data', insert_after='non_profit_section'), dict(fieldname='with_effect_from', label='80G With Effect From', fieldtype='Date', insert_after='company_80g_number'), + dict(fieldname='non_profit_column_break', fieldtype='Column Break', insert_after='with_effect_from'), dict(fieldname='pan_details', label='PAN Number', fieldtype='Data', insert_after='with_effect_from') ], diff --git a/erpnext/setup/doctype/company/company.js b/erpnext/setup/doctype/company/company.js index 8f83d3cd73..56700af79e 100644 --- a/erpnext/setup/doctype/company/company.js +++ b/erpnext/setup/doctype/company/company.js @@ -46,6 +46,43 @@ frappe.ui.form.on("Company", { }); }, + change_abbreviation(frm) { + var dialog = new frappe.ui.Dialog({ + title: "Replace Abbr", + fields: [ + {"fieldtype": "Data", "label": "New Abbreviation", "fieldname": "new_abbr", + "reqd": 1 }, + {"fieldtype": "Button", "label": "Update", "fieldname": "update"}, + ] + }); + + dialog.fields_dict.update.$input.click(function() { + var args = dialog.get_values(); + if (!args) return; + frappe.show_alert(__("Update in progress. It might take a while.")); + return frappe.call({ + method: "erpnext.setup.doctype.company.company.enqueue_replace_abbr", + args: { + "company": frm.doc.name, + "old": frm.doc.abbr, + "new": args.new_abbr + }, + callback: function(r) { + if (r.exc) { + frappe.msgprint(__("There were errors.")); + return; + } else { + frm.set_value("abbr", args.new_abbr); + } + dialog.hide(); + frm.refresh(); + }, + btn: this + }); + }); + dialog.show(); + }, + company_name: function(frm) { if(frm.doc.__islocal) { // add missing " " arg in split method @@ -127,6 +164,10 @@ frappe.ui.form.on("Company", { }, __('Manage')); } } + + frm.add_custom_button(__('Change Abbreviation'), () => { + frm.trigger('change_abbreviation'); + }, __('Manage')); } erpnext.company.set_chart_of_accounts_options(frm.doc); @@ -204,43 +245,6 @@ erpnext.company.set_chart_of_accounts_options = function(doc) { } } -cur_frm.cscript.change_abbr = function() { - var dialog = new frappe.ui.Dialog({ - title: "Replace Abbr", - fields: [ - {"fieldtype": "Data", "label": "New Abbreviation", "fieldname": "new_abbr", - "reqd": 1 }, - {"fieldtype": "Button", "label": "Update", "fieldname": "update"}, - ] - }); - - dialog.fields_dict.update.$input.click(function() { - var args = dialog.get_values(); - if(!args) return; - frappe.show_alert(__("Update in progress. It might take a while.")); - return frappe.call({ - method: "erpnext.setup.doctype.company.company.enqueue_replace_abbr", - args: { - "company": cur_frm.doc.name, - "old": cur_frm.doc.abbr, - "new": args.new_abbr - }, - callback: function(r) { - if(r.exc) { - frappe.msgprint(__("There were errors.")); - return; - } else { - cur_frm.set_value("abbr", args.new_abbr); - } - dialog.hide(); - cur_frm.refresh(); - }, - btn: this - }) - }); - dialog.show(); -} - erpnext.company.setup_queries = function(frm) { $.each([ ["default_bank_account", {"account_type": "Bank"}], diff --git a/erpnext/setup/doctype/company/company.json b/erpnext/setup/doctype/company/company.json index e6ec496a65..e4ee3ecea7 100644 --- a/erpnext/setup/doctype/company/company.json +++ b/erpnext/setup/doctype/company/company.json @@ -12,33 +12,48 @@ "details", "company_name", "abbr", - "change_abbr", + "default_currency", + "country", "is_group", "cb0", - "domain", - "parent_company", - "charts_section", - "default_currency", "default_letter_head", - "default_holiday_list", - "default_finance_book", - "default_selling_terms", - "default_buying_terms", - "default_warehouse_for_sales_return", - "default_in_transit_warehouse", - "column_break_10", - "country", - "create_chart_of_accounts_based_on", - "chart_of_accounts", - "existing_company", "tax_id", + "domain", "date_of_establishment", + "parent_company", + "company_info", + "company_logo", + "date_of_incorporation", + "phone_no", + "email", + "company_description", + "column_break1", + "date_of_commencement", + "fax", + "website", + "address_html", + "section_break_28", + "create_chart_of_accounts_based_on", + "existing_company", + "column_break_26", + "chart_of_accounts", + "charts_section", "sales_settings", - "monthly_sales_target", + "default_buying_terms", "sales_monthly_history", - "column_break_goals", - "transactions_annual_history", + "monthly_sales_target", "total_monthly_sales", + "column_break_goals", + "default_selling_terms", + "default_warehouse_for_sales_return", + "credit_limit", + "transactions_annual_history", + "hr_settings_section", + "default_holiday_list", + "default_expense_claim_payable_account", + "column_break_10", + "default_employee_advance_account", + "default_payroll_payable_account", "default_settings", "default_bank_account", "default_cash_account", @@ -52,24 +67,20 @@ "column_break0", "allow_account_creation_against_child_company", "default_payable_account", - "default_employee_advance_account", "default_expense_account", "default_income_account", "default_deferred_revenue_account", "default_deferred_expense_account", - "default_payroll_payable_account", - "default_expense_claim_payable_account", "default_discount_account", - "section_break_22", - "cost_center", - "column_break_26", - "credit_limit", "payment_terms", + "cost_center", + "default_finance_book", "auto_accounting_for_stock_settings", "enable_perpetual_inventory", "enable_perpetual_inventory_for_non_stock_items", "default_inventory_account", "stock_adjustment_account", + "default_in_transit_warehouse", "column_break_32", "stock_received_but_not_billed", "service_received_but_not_billed", @@ -79,25 +90,14 @@ "depreciation_expense_account", "series_for_depreciation_entry", "expenses_included_in_asset_valuation", + "repair_and_maintenance_account", "column_break_40", "disposal_account", "depreciation_cost_center", "capital_work_in_progress_account", - "repair_and_maintenance_account", "asset_received_but_not_billed", "budget_detail", "exception_budget_approver_role", - "company_info", - "company_logo", - "date_of_incorporation", - "address_html", - "date_of_commencement", - "phone_no", - "fax", - "email", - "website", - "column_break1", - "company_description", "registration_info", "registration_details", "lft", @@ -127,12 +127,6 @@ "oldfieldtype": "Data", "reqd": 1 }, - { - "depends_on": "eval:!doc.__islocal && in_list(frappe.user_roles, \"System Manager\")", - "fieldname": "change_abbr", - "fieldtype": "Button", - "label": "Change Abbreviation" - }, { "bold": 1, "default": "0", @@ -176,10 +170,9 @@ "label": "Company Description" }, { - "collapsible": 1, "fieldname": "sales_settings", "fieldtype": "Section Break", - "label": "Sales Settings" + "label": "Buying & Selling Settings" }, { "fieldname": "sales_monthly_history", @@ -442,10 +435,6 @@ "no_copy": 1, "options": "Account" }, - { - "fieldname": "section_break_22", - "fieldtype": "Section Break" - }, { "depends_on": "eval:!doc.__islocal", "fieldname": "cost_center", @@ -455,10 +444,6 @@ "no_copy": 1, "options": "Cost Center" }, - { - "fieldname": "column_break_26", - "fieldtype": "Column Break" - }, { "depends_on": "eval:!doc.__islocal", "fieldname": "credit_limit", @@ -589,10 +574,10 @@ }, { "collapsible": 1, - "description": "For reference only.", + "depends_on": "eval: doc.docstatus == 0 && doc.__islocal != 1", "fieldname": "company_info", "fieldtype": "Section Break", - "label": "Company Info" + "label": "Address & Contact" }, { "fieldname": "date_of_incorporation", @@ -741,6 +726,20 @@ "fieldtype": "Link", "label": "Repair and Maintenance Account", "options": "Account" + }, + { + "fieldname": "section_break_28", + "fieldtype": "Section Break", + "label": "Chart of Accounts" + }, + { + "fieldname": "hr_settings_section", + "fieldtype": "Section Break", + "label": "HR & Payroll Settings" + }, + { + "fieldname": "column_break_26", + "fieldtype": "Column Break" } ], "icon": "fa fa-building", @@ -748,7 +747,7 @@ "image_field": "company_logo", "is_tree": 1, "links": [], - "modified": "2021-05-12 16:51:08.187233", + "modified": "2021-07-12 11:27:06.353860", "modified_by": "Administrator", "module": "Setup", "name": "Company", From 2e5525aba9073010374e193ed2c6ac5e66d6ddc9 Mon Sep 17 00:00:00 2001 From: Subin Tom <36098155+nemesis189@users.noreply.github.com> Date: Tue, 24 Aug 2021 17:24:41 +0530 Subject: [PATCH 39/65] fix: Correct price list rate value in return si (#27097) --- erpnext/controllers/sales_and_purchase_return.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index 80ccc6d75b..5ee1f2f7fb 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -329,7 +329,6 @@ def make_return_doc(doctype, source_name, target_doc=None): target_doc.po_detail = source_doc.po_detail target_doc.pr_detail = source_doc.pr_detail target_doc.purchase_invoice_item = source_doc.name - target_doc.price_list_rate = 0 elif doctype == "Delivery Note": returned_qty_map = get_returned_qty_map_for_row(source_doc.name, doctype) @@ -360,7 +359,6 @@ def make_return_doc(doctype, source_name, target_doc=None): else: target_doc.pos_invoice_item = source_doc.name - target_doc.price_list_rate = 0 if default_warehouse_for_sales_return: target_doc.warehouse = default_warehouse_for_sales_return From 9198caa5e71ce06d8e4e4af4f006ef9520e50944 Mon Sep 17 00:00:00 2001 From: Saqib Date: Tue, 24 Aug 2021 17:26:23 +0530 Subject: [PATCH 40/65] fix: incorrect gl entry on period closing involving finance books (#26921) --- .../doctype/finance_book/test_finance_book.py | 24 ++++----- .../period_closing_voucher.py | 46 ++++++---------- .../test_period_closing_voucher.py | 53 +++++++++++++++++++ erpnext/accounts/general_ledger.py | 2 +- 4 files changed, 81 insertions(+), 44 deletions(-) diff --git a/erpnext/accounts/doctype/finance_book/test_finance_book.py b/erpnext/accounts/doctype/finance_book/test_finance_book.py index cd8e204f4c..2ba21397ad 100644 --- a/erpnext/accounts/doctype/finance_book/test_finance_book.py +++ b/erpnext/accounts/doctype/finance_book/test_finance_book.py @@ -9,19 +9,8 @@ import frappe import unittest class TestFinanceBook(unittest.TestCase): - def create_finance_book(self): - if not frappe.db.exists("Finance Book", "_Test Finance Book"): - finance_book = frappe.get_doc({ - "doctype": "Finance Book", - "finance_book_name": "_Test Finance Book" - }).insert() - else: - finance_book = frappe.get_doc("Finance Book", "_Test Finance Book") - - return finance_book - def test_finance_book(self): - finance_book = self.create_finance_book() + finance_book = create_finance_book() # create jv entry jv = make_journal_entry("_Test Bank - _TC", @@ -41,3 +30,14 @@ class TestFinanceBook(unittest.TestCase): for gl_entry in gl_entries: self.assertEqual(gl_entry.finance_book, finance_book.name) + +def create_finance_book(): + if not frappe.db.exists("Finance Book", "_Test Finance Book"): + finance_book = frappe.get_doc({ + "doctype": "Finance Book", + "finance_book_name": "_Test Finance Book" + }).insert() + else: + finance_book = frappe.get_doc("Finance Book", "_Test Finance Book") + + return finance_book \ No newline at end of file diff --git a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py index a6e3bd98e7..289278ea8d 100644 --- a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py +++ b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py @@ -50,9 +50,13 @@ class PeriodClosingVoucher(AccountsController): .format(pce[0][0], self.posting_date)) def make_gl_entries(self): + gl_entries = self.get_gl_entries() + if gl_entries: + from erpnext.accounts.general_ledger import make_gl_entries + make_gl_entries(gl_entries) + + def get_gl_entries(self): gl_entries = [] - net_pl_balance = 0 - pl_accounts = self.get_pl_balances() for acc in pl_accounts: @@ -60,6 +64,7 @@ class PeriodClosingVoucher(AccountsController): gl_entries.append(self.get_gl_dict({ "account": acc.account, "cost_center": acc.cost_center, + "finance_book": acc.finance_book, "account_currency": acc.account_currency, "debit_in_account_currency": abs(flt(acc.bal_in_account_currency)) if flt(acc.bal_in_account_currency) < 0 else 0, "debit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) < 0 else 0, @@ -67,35 +72,13 @@ class PeriodClosingVoucher(AccountsController): "credit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) > 0 else 0 }, item=acc)) - net_pl_balance += flt(acc.bal_in_company_currency) + if gl_entries: + gle_for_net_pl_bal = self.get_pnl_gl_entry(pl_accounts) + gl_entries += gle_for_net_pl_bal - if net_pl_balance: - if self.cost_center_wise_pnl: - costcenter_wise_gl_entries = self.get_costcenter_wise_pnl_gl_entries(pl_accounts) - gl_entries += costcenter_wise_gl_entries - else: - gl_entry = self.get_pnl_gl_entry(net_pl_balance) - gl_entries.append(gl_entry) - - from erpnext.accounts.general_ledger import make_gl_entries - make_gl_entries(gl_entries) - - def get_pnl_gl_entry(self, net_pl_balance): - cost_center = frappe.db.get_value("Company", self.company, "cost_center") - gl_entry = self.get_gl_dict({ - "account": self.closing_account_head, - "debit_in_account_currency": abs(net_pl_balance) if net_pl_balance > 0 else 0, - "debit": abs(net_pl_balance) if net_pl_balance > 0 else 0, - "credit_in_account_currency": abs(net_pl_balance) if net_pl_balance < 0 else 0, - "credit": abs(net_pl_balance) if net_pl_balance < 0 else 0, - "cost_center": cost_center - }) - - self.update_default_dimensions(gl_entry) - - return gl_entry - - def get_costcenter_wise_pnl_gl_entries(self, pl_accounts): + return gl_entries + + def get_pnl_gl_entry(self, pl_accounts): company_cost_center = frappe.db.get_value("Company", self.company, "cost_center") gl_entries = [] @@ -104,6 +87,7 @@ class PeriodClosingVoucher(AccountsController): gl_entry = self.get_gl_dict({ "account": self.closing_account_head, "cost_center": acc.cost_center or company_cost_center, + "finance_book": acc.finance_book, "account_currency": acc.account_currency, "debit_in_account_currency": abs(flt(acc.bal_in_account_currency)) if flt(acc.bal_in_account_currency) > 0 else 0, "debit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) > 0 else 0, @@ -130,7 +114,7 @@ class PeriodClosingVoucher(AccountsController): def get_pl_balances(self): """Get balance for dimension-wise pl accounts""" - dimension_fields = ['t1.cost_center'] + dimension_fields = ['t1.cost_center', 't1.finance_book'] self.accounting_dimensions = get_accounting_dimensions() for dimension in self.accounting_dimensions: diff --git a/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py b/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py index f17a5c51a0..2d1939131c 100644 --- a/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py +++ b/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py @@ -8,6 +8,7 @@ import frappe from frappe.utils import flt, today from erpnext.accounts.utils import get_fiscal_year, now from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry +from erpnext.accounts.doctype.finance_book.test_finance_book import create_finance_book from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice class TestPeriodClosingVoucher(unittest.TestCase): @@ -118,6 +119,58 @@ class TestPeriodClosingVoucher(unittest.TestCase): self.assertTrue(pcv_gle, expected_gle) + def test_period_closing_with_finance_book_entries(self): + frappe.db.sql("delete from `tabGL Entry` where company='Test PCV Company'") + + company = create_company() + surplus_account = create_account() + cost_center = create_cost_center("Test Cost Center 1") + + create_sales_invoice( + company=company, + income_account="Sales - TPC", + expense_account="Cost of Goods Sold - TPC", + cost_center=cost_center, + rate=400, + debit_to="Debtors - TPC" + ) + jv = make_journal_entry( + account1="Cash - TPC", + account2="Sales - TPC", + amount=400, + cost_center=cost_center, + posting_date=now() + ) + jv.company = company + jv.finance_book = create_finance_book().name + jv.save() + jv.submit() + + pcv = frappe.get_doc({ + "transaction_date": today(), + "posting_date": today(), + "fiscal_year": get_fiscal_year(today())[0], + "company": company, + "closing_account_head": surplus_account, + "remarks": "Test", + "doctype": "Period Closing Voucher" + }) + pcv.insert() + pcv.submit() + + expected_gle = ( + (surplus_account, 0.0, 400.0, ''), + (surplus_account, 0.0, 400.0, jv.finance_book), + ('Sales - TPC', 400.0, 0.0, ''), + ('Sales - TPC', 400.0, 0.0, jv.finance_book) + ) + + pcv_gle = frappe.db.sql(""" + select account, debit, credit, finance_book from `tabGL Entry` where voucher_no=%s + """, (pcv.name)) + + self.assertTrue(pcv_gle, expected_gle) + def make_period_closing_voucher(self): pcv = frappe.get_doc({ "doctype": "Period Closing Voucher", diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index 4c7c567b42..3126138408 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -101,7 +101,7 @@ def merge_similar_entries(gl_map, precision=None): def check_if_in_list(gle, gl_map, dimensions=None): account_head_fieldnames = ['voucher_detail_no', 'party', 'against_voucher', - 'cost_center', 'against_voucher_type', 'party_type', 'project'] + 'cost_center', 'against_voucher_type', 'party_type', 'project', 'finance_book'] if dimensions: account_head_fieldnames = account_head_fieldnames + dimensions From f47cbae5e07ba7f0e48e5fe11b807632f206bd45 Mon Sep 17 00:00:00 2001 From: Saqib Date: Tue, 24 Aug 2021 17:27:27 +0530 Subject: [PATCH 41/65] feat: allow draft pos invoices even if no stock available (#27078) --- erpnext/accounts/doctype/pos_invoice/pos_invoice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index 759cad53d4..034a217a26 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -138,7 +138,7 @@ class POSInvoice(SalesInvoice): .format(item.idx, bold_delivered_serial_nos), title=_("Item Unavailable")) def validate_stock_availablility(self): - if self.is_return: + if self.is_return or self.docstatus != 1: return allow_negative_stock = frappe.db.get_single_value('Stock Settings', 'allow_negative_stock') From ad06fb2179e69fc732e73f235a7f17b68134ef19 Mon Sep 17 00:00:00 2001 From: Afshan <33727827+AfshanKhan@users.noreply.github.com> Date: Tue, 24 Aug 2021 17:35:01 +0530 Subject: [PATCH 42/65] fix: calculation of gross profit percentage in Gross Profit Report (#27045) --- .../report/gross_profit/gross_profit.json | 8 +++-- .../report/gross_profit/gross_profit.py | 30 ++++++++++++++++--- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/report/gross_profit/gross_profit.json b/erpnext/accounts/report/gross_profit/gross_profit.json index cd6bac2d77..5fff3fdba7 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.json +++ b/erpnext/accounts/report/gross_profit/gross_profit.json @@ -1,16 +1,20 @@ { - "add_total_row": 1, + "add_total_row": 0, + "columns": [], "creation": "2013-02-25 17:03:34", + "disable_prepared_report": 0, "disabled": 0, "docstatus": 0, "doctype": "Report", + "filters": [], "idx": 3, "is_standard": "Yes", - "modified": "2020-08-13 11:26:39.112352", + "modified": "2021-08-19 18:57:07.468202", "modified_by": "Administrator", "module": "Accounts", "name": "Gross Profit", "owner": "Administrator", + "prepared_report": 0, "ref_doctype": "Sales Invoice", "report_name": "Gross Profit", "report_type": "Script Report", diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py index 6d8623c189..c949d9b74e 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -41,12 +41,14 @@ def execute(filters=None): columns = get_columns(group_wise_columns, filters) - for src in gross_profit_data.grouped_data: + for idx, src in enumerate(gross_profit_data.grouped_data): row = [] for col in group_wise_columns.get(scrub(filters.group_by)): row.append(src.get(col)) row.append(filters.currency) + if idx == len(gross_profit_data.grouped_data)-1: + row[0] = frappe.bold("Total") data.append(row) return columns, data @@ -154,6 +156,15 @@ class GrossProfitGenerator(object): def get_average_rate_based_on_group_by(self): # sum buying / selling totals for group + self.totals = frappe._dict( + qty=0, + base_amount=0, + buying_amount=0, + gross_profit=0, + gross_profit_percent=0, + base_rate=0, + buying_rate=0 + ) for key in list(self.grouped): if self.filters.get("group_by") != "Invoice": for i, row in enumerate(self.grouped[key]): @@ -165,6 +176,7 @@ class GrossProfitGenerator(object): new_row.base_amount += flt(row.base_amount, self.currency_precision) new_row = self.set_average_rate(new_row) self.grouped_data.append(new_row) + self.add_to_totals(new_row) else: for i, row in enumerate(self.grouped[key]): if row.parent in self.returned_invoices \ @@ -177,15 +189,25 @@ class GrossProfitGenerator(object): if row.qty or row.base_amount: row = self.set_average_rate(row) self.grouped_data.append(row) + self.add_to_totals(row) + self.set_average_gross_profit(self.totals) + self.grouped_data.append(self.totals) def set_average_rate(self, new_row): + self.set_average_gross_profit(new_row) + new_row.buying_rate = flt(new_row.buying_amount / new_row.qty, self.float_precision) if new_row.qty else 0 + new_row.base_rate = flt(new_row.base_amount / new_row.qty, self.float_precision) if new_row.qty else 0 + return new_row + + def set_average_gross_profit(self, new_row): new_row.gross_profit = flt(new_row.base_amount - new_row.buying_amount, self.currency_precision) new_row.gross_profit_percent = flt(((new_row.gross_profit / new_row.base_amount) * 100.0), self.currency_precision) \ if new_row.base_amount else 0 - new_row.buying_rate = flt(new_row.buying_amount / new_row.qty, self.float_precision) if new_row.qty else 0 - new_row.base_rate = flt(new_row.base_amount / new_row.qty, self.float_precision) if new_row.qty else 0 - return new_row + def add_to_totals(self, new_row): + for key in self.totals: + if new_row.get(key): + self.totals[key] += new_row[key] def get_returned_invoice_items(self): returned_invoices = frappe.db.sql(""" From 164acc845a0b1aa1473bfa475c6728834933c260 Mon Sep 17 00:00:00 2001 From: Anupam Kumar Date: Tue, 24 Aug 2021 19:15:56 +0530 Subject: [PATCH 43/65] refactor: social media post fixes (#24664) * fix: social media post fixes * feat: post metrics and some fixes * fix: sider issues * fix: sider issue * fix: reverting optional chaning statements * fix: sider issues * fix: review chnages * fix: text trigger check * fix: sider issue (cherry picked from commit f7e0edecc9ce45ba7baa4c17b2b35f487dffccf2) --- .../linkedin_settings/linkedin_settings.js | 11 +- .../linkedin_settings/linkedin_settings.json | 3 +- .../linkedin_settings/linkedin_settings.py | 112 +++++++--- .../social_media_post/social_media_post.js | 196 ++++++++++++------ .../social_media_post/social_media_post.json | 92 +++----- .../social_media_post/social_media_post.py | 54 +++-- .../social_media_post_list.js | 17 +- .../twitter_settings/twitter_settings.js | 13 +- .../twitter_settings/twitter_settings.json | 3 +- .../twitter_settings/twitter_settings.py | 50 +++-- 10 files changed, 349 insertions(+), 202 deletions(-) diff --git a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.js b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.js index 263005ef6c..7aa0b77759 100644 --- a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.js +++ b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.js @@ -2,8 +2,8 @@ // For license information, please see license.txt frappe.ui.form.on('LinkedIn Settings', { - onload: function(frm){ - if (frm.doc.session_status == 'Expired' && frm.doc.consumer_key && frm.doc.consumer_secret){ + onload: function(frm) { + if (frm.doc.session_status == 'Expired' && frm.doc.consumer_key && frm.doc.consumer_secret) { frappe.confirm( __('Session not valid, Do you want to login?'), function(){ @@ -14,8 +14,9 @@ frappe.ui.form.on('LinkedIn Settings', { } ); } + frm.dashboard.set_headline(__("For more information, {0}.", [`${__('Click here')}`])); }, - refresh: function(frm){ + refresh: function(frm) { if (frm.doc.session_status=="Expired"){ let msg = __("Session Not Active. Save doc to login."); frm.dashboard.set_headline_alert( @@ -53,7 +54,7 @@ frappe.ui.form.on('LinkedIn Settings', { ); } }, - login: function(frm){ + login: function(frm) { if (frm.doc.consumer_key && frm.doc.consumer_secret){ frappe.dom.freeze(); frappe.call({ @@ -67,7 +68,7 @@ frappe.ui.form.on('LinkedIn Settings', { }); } }, - after_save: function(frm){ + after_save: function(frm) { frm.trigger("login"); } }); diff --git a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.json b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.json index 9eacb0011c..f882e36c32 100644 --- a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.json +++ b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.json @@ -2,6 +2,7 @@ "actions": [], "creation": "2020-01-30 13:36:39.492931", "doctype": "DocType", + "documentation": "https://docs.erpnext.com/docs/user/manual/en/CRM/linkedin-settings", "editable_grid": 1, "engine": "InnoDB", "field_order": [ @@ -87,7 +88,7 @@ ], "issingle": 1, "links": [], - "modified": "2020-04-16 23:22:51.966397", + "modified": "2021-02-18 15:19:21.920725", "modified_by": "Administrator", "module": "CRM", "name": "LinkedIn Settings", diff --git a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py index d8c6fb4f90..9b88d78c1f 100644 --- a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py +++ b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py @@ -3,11 +3,12 @@ # For license information, please see license.txt from __future__ import unicode_literals -import frappe, requests, json +import frappe +import requests from frappe import _ -from frappe.utils import get_site_url, get_url_to_form, get_link_to_form +from frappe.utils import get_url_to_form from frappe.model.document import Document -from frappe.utils.file_manager import get_file, get_file_path +from frappe.utils.file_manager import get_file_path from six.moves.urllib.parse import urlencode class LinkedInSettings(Document): @@ -42,11 +43,7 @@ class LinkedInSettings(Document): self.db_set("access_token", response["access_token"]) def get_member_profile(self): - headers = { - "Authorization": "Bearer {}".format(self.access_token) - } - url = "https://api.linkedin.com/v2/me" - response = requests.get(url=url, headers=headers) + response = requests.get(url="https://api.linkedin.com/v2/me", headers=self.get_headers()) response = frappe.parse_json(response.content.decode()) frappe.db.set_value(self.doctype, self.name, { @@ -55,16 +52,16 @@ class LinkedInSettings(Document): "session_status": "Active" }) frappe.local.response["type"] = "redirect" - frappe.local.response["location"] = get_url_to_form("LinkedIn Settings","LinkedIn Settings") + frappe.local.response["location"] = get_url_to_form("LinkedIn Settings", "LinkedIn Settings") - def post(self, text, media=None): + def post(self, text, title, media=None): if not media: - return self.post_text(text) + return self.post_text(text, title) else: media_id = self.upload_image(media) if media_id: - return self.post_text(text, media_id=media_id) + return self.post_text(text, title, media_id=media_id) else: frappe.log_error("Failed to upload media.","LinkedIn Upload Error") @@ -82,9 +79,7 @@ class LinkedInSettings(Document): }] } } - headers = { - "Authorization": "Bearer {}".format(self.access_token) - } + headers = self.get_headers() response = self.http_post(url=register_url, body=body, headers=headers) if response.status_code == 200: @@ -100,24 +95,33 @@ class LinkedInSettings(Document): return None - def post_text(self, text, media_id=None): + def post_text(self, text, title, media_id=None): url = "https://api.linkedin.com/v2/shares" - headers = { - "X-Restli-Protocol-Version": "2.0.0", - "Authorization": "Bearer {}".format(self.access_token), - "Content-Type": "application/json; charset=UTF-8" - } + headers = self.get_headers() + headers["X-Restli-Protocol-Version"] = "2.0.0" + headers["Content-Type"] = "application/json; charset=UTF-8" + body = { "distribution": { "linkedInDistributionTarget": {} }, "owner":"urn:li:organization:{0}".format(self.company_id), - "subject": "Test Share Subject", + "subject": title, "text": { "text": text } } + reference_url = self.get_reference_url(text) + if reference_url: + body["content"] = { + "contentEntities": [ + { + "entityLocation": reference_url + } + ] + } + if media_id: body["content"]= { "contentEntities": [{ @@ -141,20 +145,60 @@ class LinkedInSettings(Document): raise except Exception as e: - content = json.loads(response.content) - - if response.status_code == 401: - self.db_set("session_status", "Expired") - frappe.db.commit() - frappe.throw(content["message"], title="LinkedIn Error - Unauthorized") - elif response.status_code == 403: - frappe.msgprint(_("You Didn't have permission to access this API")) - frappe.throw(content["message"], title="LinkedIn Error - Access Denied") - else: - frappe.throw(response.reason, title=response.status_code) - + self.api_error(response) + return response + def get_headers(self): + return { + "Authorization": "Bearer {}".format(self.access_token) + } + + def get_reference_url(self, text): + import re + regex_url = r"http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+" + urls = re.findall(regex_url, text) + if urls: + return urls[0] + + def delete_post(self, post_id): + try: + response = requests.delete(url="https://api.linkedin.com/v2/shares/urn:li:share:{0}".format(post_id), headers=self.get_headers()) + if response.status_code !=200: + raise + except Exception: + self.api_error(response) + + def get_post(self, post_id): + url = "https://api.linkedin.com/v2/organizationalEntityShareStatistics?q=organizationalEntity&organizationalEntity=urn:li:organization:{0}&shares[0]=urn:li:share:{1}".format(self.company_id, post_id) + + try: + response = requests.get(url=url, headers=self.get_headers()) + if response.status_code !=200: + raise + + except Exception: + self.api_error(response) + + response = frappe.parse_json(response.content.decode()) + if len(response.elements): + return response.elements[0] + + return None + + def api_error(self, response): + content = frappe.parse_json(response.content.decode()) + + if response.status_code == 401: + self.db_set("session_status", "Expired") + frappe.db.commit() + frappe.throw(content["message"], title=_("LinkedIn Error - Unauthorized")) + elif response.status_code == 403: + frappe.msgprint(_("You didn't have permission to access this API")) + frappe.throw(content["message"], title=_("LinkedIn Error - Access Denied")) + else: + frappe.throw(response.reason, title=response.status_code) + @frappe.whitelist(allow_guest=True) def callback(code=None, error=None, error_description=None): if not error: diff --git a/erpnext/crm/doctype/social_media_post/social_media_post.js b/erpnext/crm/doctype/social_media_post/social_media_post.js index 6fb0f975f4..a8f5deea53 100644 --- a/erpnext/crm/doctype/social_media_post/social_media_post.js +++ b/erpnext/crm/doctype/social_media_post/social_media_post.js @@ -1,67 +1,139 @@ // Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt frappe.ui.form.on('Social Media Post', { - validate: function(frm){ - if (frm.doc.twitter === 0 && frm.doc.linkedin === 0){ - frappe.throw(__("Select atleast one Social Media from Share on.")) - } - if (frm.doc.scheduled_time) { - let scheduled_time = new Date(frm.doc.scheduled_time); - let date_time = new Date(); - if (scheduled_time.getTime() < date_time.getTime()){ - frappe.throw(__("Invalid Scheduled Time")); - } - } - if (frm.doc.text?.length > 280){ - frappe.throw(__("Length Must be less than 280.")) - } - }, - refresh: function(frm){ - if (frm.doc.docstatus === 1){ - if (frm.doc.post_status != "Posted"){ - add_post_btn(frm); - } - else if (frm.doc.post_status == "Posted"){ - frm.set_df_property('sheduled_time', 'read_only', 1); - } + validate: function(frm) { + if (frm.doc.twitter === 0 && frm.doc.linkedin === 0) { + frappe.throw(__("Select atleast one Social Media Platform to Share on.")); + } + if (frm.doc.scheduled_time) { + let scheduled_time = new Date(frm.doc.scheduled_time); + let date_time = new Date(); + if (scheduled_time.getTime() < date_time.getTime()) { + frappe.throw(__("Scheduled Time must be a future time.")); + } + } + frm.trigger('validate_tweet_length'); + }, - let html=''; - if (frm.doc.twitter){ - let color = frm.doc.twitter_post_id ? "green" : "red"; - let status = frm.doc.twitter_post_id ? "Posted" : "Not Posted"; - html += `
- Twitter : ${status} -
` ; - } - if (frm.doc.linkedin){ - let color = frm.doc.linkedin_post_id ? "green" : "red"; - let status = frm.doc.linkedin_post_id ? "Posted" : "Not Posted"; - html += `
- LinkedIn : ${status} -
` ; - } - html = `
${html}
`; - frm.dashboard.set_headline_alert(html); - } - } + text: function(frm) { + if (frm.doc.text) { + frm.set_df_property('text', 'description', `${frm.doc.text.length}/280`); + frm.refresh_field('text'); + frm.trigger('validate_tweet_length'); + } + }, + + validate_tweet_length: function(frm) { + if (frm.doc.text && frm.doc.text.length > 280) { + frappe.throw(__("Tweet length Must be less than 280.")); + } + }, + + onload: function(frm) { + frm.trigger('make_dashboard'); + }, + + make_dashboard: function(frm) { + if (frm.doc.post_status == "Posted") { + frappe.call({ + doc: frm.doc, + method: 'get_post', + freeze: true, + callback: (r) => { + if (!r.message) { + return; + } + + let datasets = [], colors = []; + if (r.message && r.message.twitter) { + colors.push('#1DA1F2'); + datasets.push({ + name: 'Twitter', + values: [r.message.twitter.favorite_count, r.message.twitter.retweet_count] + }); + } + if (r.message && r.message.linkedin) { + colors.push('#0077b5'); + datasets.push({ + name: 'LinkedIn', + values: [r.message.linkedin.totalShareStatistics.likeCount, r.message.linkedin.totalShareStatistics.shareCount] + }); + } + + if (datasets.length) { + frm.dashboard.render_graph({ + data: { + labels: ['Likes', 'Retweets/Shares'], + datasets: datasets + }, + + title: __("Post Metrics"), + type: 'bar', + height: 300, + colors: colors + }); + } + } + }); + } + }, + + refresh: function(frm) { + frm.trigger('text'); + + if (frm.doc.docstatus === 1) { + if (!['Posted', 'Deleted'].includes(frm.doc.post_status)) { + frm.trigger('add_post_btn'); + } + if (frm.doc.post_status !='Deleted') { + frm.add_custom_button(('Delete Post'), function() { + frappe.confirm(__('Are you sure want to delete the Post from Social Media platforms?'), + function() { + frappe.call({ + doc: frm.doc, + method: 'delete_post', + freeze: true, + callback: () => { + frm.reload_doc(); + } + }); + } + ); + }); + } + + if (frm.doc.post_status !='Deleted') { + let html=''; + if (frm.doc.twitter) { + let color = frm.doc.twitter_post_id ? "green" : "red"; + let status = frm.doc.twitter_post_id ? "Posted" : "Not Posted"; + html += `
+ Twitter : ${status} +
` ; + } + if (frm.doc.linkedin) { + let color = frm.doc.linkedin_post_id ? "green" : "red"; + let status = frm.doc.linkedin_post_id ? "Posted" : "Not Posted"; + html += `
+ LinkedIn : ${status} +
` ; + } + html = `
${html}
`; + frm.dashboard.set_headline_alert(html); + } + } + }, + + add_post_btn: function(frm) { + frm.add_custom_button(__('Post Now'), function() { + frappe.call({ + doc: frm.doc, + method: 'post', + freeze: true, + callback: function() { + frm.reload_doc(); + } + }); + }); + } }); -var add_post_btn = function(frm){ - frm.add_custom_button(('Post Now'), function(){ - post(frm); - }); -} -var post = function(frm){ - frappe.dom.freeze(); - frappe.call({ - method: "erpnext.crm.doctype.social_media_post.social_media_post.publish", - args: { - doctype: frm.doc.doctype, - name: frm.doc.name - }, - callback: function(r) { - frm.reload_doc(); - frappe.dom.unfreeze(); - } - }) - -} diff --git a/erpnext/crm/doctype/social_media_post/social_media_post.json b/erpnext/crm/doctype/social_media_post/social_media_post.json index 0a00dca280..98e78f949e 100644 --- a/erpnext/crm/doctype/social_media_post/social_media_post.json +++ b/erpnext/crm/doctype/social_media_post/social_media_post.json @@ -3,9 +3,11 @@ "autoname": "format: CRM-SMP-{YYYY}-{MM}-{DD}-{###}", "creation": "2020-01-30 11:53:13.872864", "doctype": "DocType", + "documentation": "https://docs.erpnext.com/docs/user/manual/en/CRM/social-media-post", "editable_grid": 1, "engine": "InnoDB", "field_order": [ + "title", "campaign_name", "scheduled_time", "post_status", @@ -30,32 +32,24 @@ "fieldname": "text", "fieldtype": "Small Text", "label": "Tweet", - "mandatory_depends_on": "eval:doc.twitter ==1", - "show_days": 1, - "show_seconds": 1 + "mandatory_depends_on": "eval:doc.twitter ==1" }, { "fieldname": "image", "fieldtype": "Attach Image", - "label": "Image", - "show_days": 1, - "show_seconds": 1 + "label": "Image" }, { - "default": "0", + "default": "1", "fieldname": "twitter", "fieldtype": "Check", - "label": "Twitter", - "show_days": 1, - "show_seconds": 1 + "label": "Twitter" }, { - "default": "0", + "default": "1", "fieldname": "linkedin", "fieldtype": "Check", - "label": "LinkedIn", - "show_days": 1, - "show_seconds": 1 + "label": "LinkedIn" }, { "fieldname": "amended_from", @@ -64,27 +58,22 @@ "no_copy": 1, "options": "Social Media Post", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "depends_on": "eval:doc.twitter ==1", "fieldname": "content", "fieldtype": "Section Break", - "label": "Twitter", - "show_days": 1, - "show_seconds": 1 + "label": "Twitter" }, { "allow_on_submit": 1, "fieldname": "post_status", "fieldtype": "Select", "label": "Post Status", - "options": "\nScheduled\nPosted\nError", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "no_copy": 1, + "options": "\nScheduled\nPosted\nCancelled\nDeleted\nError", + "read_only": 1 }, { "allow_on_submit": 1, @@ -92,9 +81,8 @@ "fieldtype": "Data", "hidden": 1, "label": "Twitter Post Id", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "no_copy": 1, + "read_only": 1 }, { "allow_on_submit": 1, @@ -102,82 +90,69 @@ "fieldtype": "Data", "hidden": 1, "label": "LinkedIn Post Id", - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "no_copy": 1, + "read_only": 1 }, { "fieldname": "campaign_name", "fieldtype": "Link", "in_list_view": 1, "label": "Campaign", - "options": "Campaign", - "show_days": 1, - "show_seconds": 1 + "options": "Campaign" }, { "fieldname": "column_break_6", "fieldtype": "Column Break", - "label": "Share On", - "show_days": 1, - "show_seconds": 1 + "label": "Share On" }, { "fieldname": "column_break_14", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "fieldname": "tweet_preview", - "fieldtype": "HTML", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "HTML" }, { "collapsible": 1, "depends_on": "eval:doc.linkedin==1", "fieldname": "linkedin_section", "fieldtype": "Section Break", - "label": "LinkedIn", - "show_days": 1, - "show_seconds": 1 + "label": "LinkedIn" }, { "collapsible": 1, "fieldname": "attachments_section", "fieldtype": "Section Break", - "label": "Attachments", - "show_days": 1, - "show_seconds": 1 + "label": "Attachments" }, { "fieldname": "linkedin_post", "fieldtype": "Text", "label": "Post", - "mandatory_depends_on": "eval:doc.linkedin ==1", - "show_days": 1, - "show_seconds": 1 + "mandatory_depends_on": "eval:doc.linkedin ==1" }, { "fieldname": "column_break_15", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" }, { "allow_on_submit": 1, "fieldname": "scheduled_time", "fieldtype": "Datetime", "label": "Scheduled Time", - "read_only_depends_on": "eval:doc.post_status == \"Posted\"", - "show_days": 1, - "show_seconds": 1 + "read_only_depends_on": "eval:doc.post_status == \"Posted\"" + }, + { + "fieldname": "title", + "fieldtype": "Data", + "label": "Title", + "reqd": 1 } ], "is_submittable": 1, "links": [], - "modified": "2020-06-14 10:31:33.961381", + "modified": "2021-04-14 14:24:59.821223", "modified_by": "Administrator", "module": "CRM", "name": "Social Media Post", @@ -228,5 +203,6 @@ ], "sort_field": "modified", "sort_order": "DESC", + "title_field": "title", "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/crm/doctype/social_media_post/social_media_post.py b/erpnext/crm/doctype/social_media_post/social_media_post.py index ed1b583944..95320bff53 100644 --- a/erpnext/crm/doctype/social_media_post/social_media_post.py +++ b/erpnext/crm/doctype/social_media_post/social_media_post.py @@ -10,17 +10,51 @@ import datetime class SocialMediaPost(Document): def validate(self): + if (not self.twitter and not self.linkedin): + frappe.throw(_("Select atleast one Social Media Platform to Share on.")) + if self.scheduled_time: current_time = frappe.utils.now_datetime() scheduled_time = frappe.utils.get_datetime(self.scheduled_time) if scheduled_time < current_time: - frappe.throw(_("Invalid Scheduled Time")) + frappe.throw(_("Scheduled Time must be a future time.")) + + if self.text and len(self.text) > 280: + frappe.throw(_("Tweet length must be less than 280.")) def submit(self): if self.scheduled_time: self.post_status = "Scheduled" super(SocialMediaPost, self).submit() + + def on_cancel(self): + self.db_set('post_status', 'Cancelled') + @frappe.whitelist() + def delete_post(self): + if self.twitter and self.twitter_post_id: + twitter = frappe.get_doc("Twitter Settings") + twitter.delete_tweet(self.twitter_post_id) + + if self.linkedin and self.linkedin_post_id: + linkedin = frappe.get_doc("LinkedIn Settings") + linkedin.delete_post(self.linkedin_post_id) + + self.db_set('post_status', 'Deleted') + + @frappe.whitelist() + def get_post(self): + response = {} + if self.linkedin and self.linkedin_post_id: + linkedin = frappe.get_doc("LinkedIn Settings") + response['linkedin'] = linkedin.get_post(self.linkedin_post_id) + if self.twitter and self.twitter_post_id: + twitter = frappe.get_doc("Twitter Settings") + response['twitter'] = twitter.get_tweet(self.twitter_post_id) + + return response + + @frappe.whitelist() def post(self): try: if self.twitter and not self.twitter_post_id: @@ -29,28 +63,22 @@ class SocialMediaPost(Document): self.db_set("twitter_post_id", twitter_post.id) if self.linkedin and not self.linkedin_post_id: linkedin = frappe.get_doc("LinkedIn Settings") - linkedin_post = linkedin.post(self.linkedin_post, self.image) - self.db_set("linkedin_post_id", linkedin_post.headers['X-RestLi-Id'].split(":")[-1]) + linkedin_post = linkedin.post(self.linkedin_post, self.title, self.image) + self.db_set("linkedin_post_id", linkedin_post.headers['X-RestLi-Id']) self.db_set("post_status", "Posted") except: self.db_set("post_status", "Error") title = _("Error while POSTING {0}").format(self.name) - traceback = frappe.get_traceback() - frappe.log_error(message=traceback , title=title) + frappe.log_error(message=frappe.get_traceback(), title=title) def process_scheduled_social_media_posts(): - posts = frappe.get_list("Social Media Post", filters={"post_status": "Scheduled", "docstatus":1}, fields= ["name", "scheduled_time","post_status"]) + posts = frappe.get_list("Social Media Post", filters={"post_status": "Scheduled", "docstatus":1}, fields= ["name", "scheduled_time"]) start = frappe.utils.now_datetime() end = start + datetime.timedelta(minutes=10) for post in posts: if post.scheduled_time: post_time = frappe.utils.get_datetime(post.scheduled_time) if post_time > start and post_time <= end: - publish('Social Media Post', post.name) - -@frappe.whitelist() -def publish(doctype, name): - sm_post = frappe.get_doc(doctype, name) - sm_post.post() - frappe.db.commit() + sm_post = frappe.get_doc('Social Media Post', post.name) + sm_post.post() diff --git a/erpnext/crm/doctype/social_media_post/social_media_post_list.js b/erpnext/crm/doctype/social_media_post/social_media_post_list.js index c60b91a9a0..a8c8272ad0 100644 --- a/erpnext/crm/doctype/social_media_post/social_media_post_list.js +++ b/erpnext/crm/doctype/social_media_post/social_media_post_list.js @@ -1,10 +1,11 @@ frappe.listview_settings['Social Media Post'] = { - add_fields: ["status","post_status"], - get_indicator: function(doc) { - return [__(doc.post_status), { - "Scheduled": "orange", - "Posted": "green", - "Error": "red" - }[doc.post_status]]; - } + add_fields: ["status", "post_status"], + get_indicator: function(doc) { + return [__(doc.post_status), { + "Scheduled": "orange", + "Posted": "green", + "Error": "red", + "Deleted": "red" + }[doc.post_status]]; + } } diff --git a/erpnext/crm/doctype/twitter_settings/twitter_settings.js b/erpnext/crm/doctype/twitter_settings/twitter_settings.js index f6f431ca5c..112f3d4d1c 100644 --- a/erpnext/crm/doctype/twitter_settings/twitter_settings.js +++ b/erpnext/crm/doctype/twitter_settings/twitter_settings.js @@ -2,7 +2,7 @@ // For license information, please see license.txt frappe.ui.form.on('Twitter Settings', { - onload: function(frm){ + onload: function(frm) { if (frm.doc.session_status == 'Expired' && frm.doc.consumer_key && frm.doc.consumer_secret){ frappe.confirm( __('Session not valid, Do you want to login?'), @@ -14,10 +14,11 @@ frappe.ui.form.on('Twitter Settings', { } ); } + frm.dashboard.set_headline(__("For more information, {0}.", [`${__('Click here')}`])); }, - refresh: function(frm){ + refresh: function(frm) { let msg, color, flag=false; - if (frm.doc.session_status == "Active"){ + if (frm.doc.session_status == "Active") { msg = __("Session Active"); color = 'green'; flag = true; @@ -28,7 +29,7 @@ frappe.ui.form.on('Twitter Settings', { flag = true; } - if (flag){ + if (flag) { frm.dashboard.set_headline_alert( `
@@ -38,7 +39,7 @@ frappe.ui.form.on('Twitter Settings', { ); } }, - login: function(frm){ + login: function(frm) { if (frm.doc.consumer_key && frm.doc.consumer_secret){ frappe.dom.freeze(); frappe.call({ @@ -52,7 +53,7 @@ frappe.ui.form.on('Twitter Settings', { }); } }, - after_save: function(frm){ + after_save: function(frm) { frm.trigger("login"); } }); diff --git a/erpnext/crm/doctype/twitter_settings/twitter_settings.json b/erpnext/crm/doctype/twitter_settings/twitter_settings.json index 36776e5c20..8d05877f06 100644 --- a/erpnext/crm/doctype/twitter_settings/twitter_settings.json +++ b/erpnext/crm/doctype/twitter_settings/twitter_settings.json @@ -2,6 +2,7 @@ "actions": [], "creation": "2020-01-30 10:29:08.562108", "doctype": "DocType", + "documentation": "https://docs.erpnext.com/docs/user/manual/en/CRM/twitter-settings", "editable_grid": 1, "engine": "InnoDB", "field_order": [ @@ -77,7 +78,7 @@ "image_field": "profile_pic", "issingle": 1, "links": [], - "modified": "2020-05-13 17:50:47.934776", + "modified": "2021-02-18 15:18:07.900031", "modified_by": "Administrator", "module": "CRM", "name": "Twitter Settings", diff --git a/erpnext/crm/doctype/twitter_settings/twitter_settings.py b/erpnext/crm/doctype/twitter_settings/twitter_settings.py index 1e1beab2d2..47756560ec 100644 --- a/erpnext/crm/doctype/twitter_settings/twitter_settings.py +++ b/erpnext/crm/doctype/twitter_settings/twitter_settings.py @@ -32,7 +32,9 @@ class TwitterSettings(Document): try: auth.get_access_token(oauth_verifier) - api = self.get_api(auth.access_token, auth.access_token_secret) + self.access_token = auth.access_token + self.access_token_secret = auth.access_token_secret + api = self.get_api() user = api.me() profile_pic = (user._json["profile_image_url"]).replace("_normal","") @@ -50,11 +52,11 @@ class TwitterSettings(Document): frappe.msgprint(_("Error! Failed to get access token.")) frappe.throw(_('Invalid Consumer Key or Consumer Secret Key')) - def get_api(self, access_token, access_token_secret): - # authentication of consumer key and secret - auth = tweepy.OAuthHandler(self.consumer_key, self.get_password(fieldname="consumer_secret")) - # authentication of access token and secret - auth.set_access_token(access_token, access_token_secret) + def get_api(self): + # authentication of consumer key and secret + auth = tweepy.OAuthHandler(self.consumer_key, self.get_password(fieldname="consumer_secret")) + # authentication of access token and secret + auth.set_access_token(self.access_token, self.access_token_secret) return tweepy.API(auth) @@ -68,13 +70,13 @@ class TwitterSettings(Document): def upload_image(self, media): media = get_file_path(media) - api = self.get_api(self.access_token, self.access_token_secret) + api = self.get_api() media = api.media_upload(media) return media.media_id def send_tweet(self, text, media_id=None): - api = self.get_api(self.access_token, self.access_token_secret) + api = self.get_api() try: if media_id: response = api.update_status(status = text, media_ids = [media_id]) @@ -84,12 +86,32 @@ class TwitterSettings(Document): return response except TweepError as e: - content = json.loads(e.response.content) - content = content["errors"][0] - if e.response.status_code == 401: - self.db_set("session_status", "Expired") - frappe.db.commit() - frappe.throw(content["message"],title="Twitter Error {0} {1}".format(e.response.status_code, e.response.reason)) + self.api_error(e) + + def delete_tweet(self, tweet_id): + api = self.get_api() + try: + api.destroy_status(tweet_id) + except TweepError as e: + self.api_error(e) + + def get_tweet(self, tweet_id): + api = self.get_api() + try: + response = api.get_status(tweet_id, trim_user=True, include_entities=True) + except TweepError as e: + self.api_error(e) + + return response._json + + def api_error(self, e): + content = json.loads(e.response.content) + content = content["errors"][0] + if e.response.status_code == 401: + self.db_set("session_status", "Expired") + frappe.db.commit() + frappe.throw(content["message"],title=_("Twitter Error {0} : {1}").format(e.response.status_code, e.response.reason)) + @frappe.whitelist(allow_guest=True) def callback(oauth_token = None, oauth_verifier = None): From f1313302c98f8a1a63d222aca4321fbbfb30ba03 Mon Sep 17 00:00:00 2001 From: Saqib Date: Tue, 24 Aug 2021 19:45:21 +0530 Subject: [PATCH 44/65] fix: pos invoice test (#27113) --- erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py index 6172796129..d2527fb2e5 100644 --- a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py @@ -320,7 +320,8 @@ class TestPOSInvoice(unittest.TestCase): pos2.get("items")[0].serial_no = serial_nos[0] pos2.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 1000}) - self.assertRaises(frappe.ValidationError, pos2.insert) + pos2.insert() + self.assertRaises(frappe.ValidationError, pos2.submit) def test_delivered_serialized_item_transaction(self): from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item @@ -348,7 +349,8 @@ class TestPOSInvoice(unittest.TestCase): pos2.get("items")[0].serial_no = serial_nos[0] pos2.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 1000}) - self.assertRaises(frappe.ValidationError, pos2.insert) + pos2.insert() + self.assertRaises(frappe.ValidationError, pos2.submit) def test_loyalty_points(self): from erpnext.accounts.doctype.loyalty_program.test_loyalty_program import create_records From 1c279af0b30b4dcafe091d5183fbf13532c05e60 Mon Sep 17 00:00:00 2001 From: Anupam Kumar Date: Tue, 24 Aug 2021 20:06:04 +0530 Subject: [PATCH 45/65] fix: lead name issue (#26999) * fix: lead name issue * fix: change lead name wrt first name, middle and last name Co-authored-by: Jannat Patel <31363128+pateljannat@users.noreply.github.com> --- erpnext/crm/doctype/lead/lead.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/crm/doctype/lead/lead.py b/erpnext/crm/doctype/lead/lead.py index c0ce6badbf..cad17a3bee 100644 --- a/erpnext/crm/doctype/lead/lead.py +++ b/erpnext/crm/doctype/lead/lead.py @@ -36,7 +36,8 @@ class Lead(SellingController): }) def set_full_name(self): - self.lead_name = " ".join(filter(None, [self.first_name, self.middle_name, self.last_name])) + if self.first_name: + self.lead_name = " ".join(filter(None, [self.first_name, self.middle_name, self.last_name])) def validate_email_id(self): if self.email_id: From 255b99ebdc1ce3be6de02d9641fc72685de1a6a6 Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Tue, 24 Aug 2021 20:19:22 +0530 Subject: [PATCH 46/65] feat: Employee reminders (#25735) (#27115) * feat: Add reminders section to HR Settings * refactor: Extract generic function for getting Employees * feat: Employee Work Anniversary Reminder * feat: Daily Holiday Reminder * fix: Unnecessary params and replace [] with .get() * test: Daily Holiday Reminders * test: is_holiday basic tests * refactor: Move employee reminders code to separate module * feat: Add advance reminder to HR settings * feat: Advance Holiday Reminders * refactor: get_holidays_for_employee * feat: Email holiday reminders in advance + tests * fix: Remove unused import * refactor: HR Setting Reminder Section * refactor: Remove Daily Holiday Reminders feat * feat: Reminder miss warning * fix: Failing test and function name change * chore: Add patch for field rename * chore: Rename frequency label * fix: Failing patch test * fix: sider and removed description of fields * fix: email alignment Co-authored-by: pateljannat Co-authored-by: Jannat Patel <31363128+pateljannat@users.noreply.github.com> (cherry picked from commit 24b2a315818d08ad4cb03347ccf5297df916a5ac) Co-authored-by: Mohammad Hussain Nagaria <34810212+NagariaHussain@users.noreply.github.com> Co-authored-by: Jannat Patel <31363128+pateljannat@users.noreply.github.com> --- erpnext/hooks.py | 9 +- .../compensatory_leave_request.py | 4 +- erpnext/hr/doctype/employee/employee.py | 127 +++------ .../hr/doctype/employee/employee_reminders.py | 247 ++++++++++++++++++ erpnext/hr/doctype/employee/test_employee.py | 25 -- .../employee/test_employee_reminders.py | 173 ++++++++++++ .../hr/doctype/hr_settings/hr_settings.json | 57 +++- erpnext/hr/doctype/hr_settings/hr_settings.py | 66 ++++- .../upload_attendance/upload_attendance.py | 4 +- erpnext/hr/utils.py | 47 +++- erpnext/patches.txt | 1 + .../rename_stop_to_send_birthday_reminders.py | 23 ++ .../employee_benefit_application.py | 4 +- .../doctype/payroll_period/payroll_period.py | 4 +- .../doctype/salary_slip/salary_slip.py | 16 +- erpnext/public/js/utils.js | 11 + .../emails/anniversary_reminder.html | 25 ++ .../templates/emails/holiday_reminder.html | 16 ++ 18 files changed, 691 insertions(+), 168 deletions(-) create mode 100644 erpnext/hr/doctype/employee/employee_reminders.py create mode 100644 erpnext/hr/doctype/employee/test_employee_reminders.py create mode 100644 erpnext/patches/v13_0/rename_stop_to_send_birthday_reminders.py create mode 100644 erpnext/templates/emails/anniversary_reminder.html create mode 100644 erpnext/templates/emails/holiday_reminder.html diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 74977cd8bc..4854bfd1e1 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -355,7 +355,8 @@ scheduler_events = { "erpnext.crm.doctype.opportunity.opportunity.auto_close_opportunity", "erpnext.controllers.accounts_controller.update_invoice_status", "erpnext.accounts.doctype.fiscal_year.fiscal_year.auto_create_fiscal_year", - "erpnext.hr.doctype.employee.employee.send_birthday_reminders", + "erpnext.hr.doctype.employee.employee_reminders.send_work_anniversary_reminders", + "erpnext.hr.doctype.employee.employee_reminders.send_birthday_reminders", "erpnext.projects.doctype.task.task.set_tasks_as_overdue", "erpnext.assets.doctype.asset.depreciation.post_depreciation_entries", "erpnext.hr.doctype.daily_work_summary_group.daily_work_summary_group.send_summary", @@ -387,6 +388,12 @@ scheduler_events = { "erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual.process_loan_interest_accrual_for_term_loans", "erpnext.crm.doctype.lead.lead.daily_open_lead" ], + "weekly": [ + "erpnext.hr.doctype.employee.employee_reminders.send_reminders_in_advance_weekly" + ], + "monthly": [ + "erpnext.hr.doctype.employee.employee_reminders.send_reminders_in_advance_monthly" + ], "monthly_long": [ "erpnext.accounts.deferred_revenue.process_deferred_accounting", "erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual.process_loan_interest_accrual_for_demand_loans" diff --git a/erpnext/hr/doctype/compensatory_leave_request/compensatory_leave_request.py b/erpnext/hr/doctype/compensatory_leave_request/compensatory_leave_request.py index 0d7fded921..3db81654a6 100644 --- a/erpnext/hr/doctype/compensatory_leave_request/compensatory_leave_request.py +++ b/erpnext/hr/doctype/compensatory_leave_request/compensatory_leave_request.py @@ -8,7 +8,7 @@ from frappe import _ from frappe.utils import date_diff, add_days, getdate, cint, format_date from frappe.model.document import Document from erpnext.hr.utils import validate_dates, validate_overlap, get_leave_period, validate_active_employee, \ - get_holidays_for_employee, create_additional_leave_ledger_entry + create_additional_leave_ledger_entry, get_holiday_dates_for_employee class CompensatoryLeaveRequest(Document): @@ -39,7 +39,7 @@ class CompensatoryLeaveRequest(Document): frappe.throw(_("You are not present all day(s) between compensatory leave request days")) def validate_holidays(self): - holidays = get_holidays_for_employee(self.employee, self.work_from_date, self.work_end_date) + holidays = get_holiday_dates_for_employee(self.employee, self.work_from_date, self.work_end_date) if len(holidays) < date_diff(self.work_end_date, self.work_from_date) + 1: if date_diff(self.work_end_date, self.work_from_date): msg = _("The days between {0} to {1} are not valid holidays.").format(frappe.bold(format_date(self.work_from_date)), frappe.bold(format_date(self.work_end_date))) diff --git a/erpnext/hr/doctype/employee/employee.py b/erpnext/hr/doctype/employee/employee.py index f4280152c5..643f3da2ff 100755 --- a/erpnext/hr/doctype/employee/employee.py +++ b/erpnext/hr/doctype/employee/employee.py @@ -1,7 +1,5 @@ # 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 from frappe.utils import getdate, validate_email_address, today, add_years, cstr @@ -9,7 +7,6 @@ from frappe.model.naming import set_name_by_naming_series from frappe import throw, _, scrub from frappe.permissions import add_user_permission, remove_user_permission, \ set_user_permission_if_allowed, has_permission, get_doc_permissions -from frappe.model.document import Document from erpnext.utilities.transaction_base import delete_events from frappe.utils.nestedset import NestedSet @@ -286,94 +283,8 @@ def update_user_permissions(doc, method): employee = frappe.get_doc("Employee", {"user_id": doc.name}) employee.update_user_permissions() -def send_birthday_reminders(): - """Send Employee birthday reminders if no 'Stop Birthday Reminders' is not set.""" - if int(frappe.db.get_single_value("HR Settings", "stop_birthday_reminders") or 0): - return - - employees_born_today = get_employees_who_are_born_today() - - for company, birthday_persons in employees_born_today.items(): - employee_emails = get_all_employee_emails(company) - birthday_person_emails = [get_employee_email(doc) for doc in birthday_persons] - recipients = list(set(employee_emails) - set(birthday_person_emails)) - - reminder_text, message = get_birthday_reminder_text_and_message(birthday_persons) - send_birthday_reminder(recipients, reminder_text, birthday_persons, message) - - if len(birthday_persons) > 1: - # special email for people sharing birthdays - for person in birthday_persons: - person_email = person["user_id"] or person["personal_email"] or person["company_email"] - others = [d for d in birthday_persons if d != person] - reminder_text, message = get_birthday_reminder_text_and_message(others) - send_birthday_reminder(person_email, reminder_text, others, message) - def get_employee_email(employee_doc): - return employee_doc["user_id"] or employee_doc["personal_email"] or employee_doc["company_email"] - -def get_birthday_reminder_text_and_message(birthday_persons): - if len(birthday_persons) == 1: - birthday_person_text = birthday_persons[0]['name'] - else: - # converts ["Jim", "Rim", "Dim"] to Jim, Rim & Dim - person_names = [d['name'] for d in birthday_persons] - last_person = person_names[-1] - birthday_person_text = ", ".join(person_names[:-1]) - birthday_person_text = _("{} & {}").format(birthday_person_text, last_person) - - reminder_text = _("Today is {0}'s birthday 🎉").format(birthday_person_text) - message = _("A friendly reminder of an important date for our team.") - message += "
" - message += _("Everyone, let’s congratulate {0} on their birthday.").format(birthday_person_text) - - return reminder_text, message - -def send_birthday_reminder(recipients, reminder_text, birthday_persons, message): - frappe.sendmail( - recipients=recipients, - subject=_("Birthday Reminder"), - template="birthday_reminder", - args=dict( - reminder_text=reminder_text, - birthday_persons=birthday_persons, - message=message, - ), - header=_("Birthday Reminder 🎂") - ) - -def get_employees_who_are_born_today(): - """Get all employee born today & group them based on their company""" - from collections import defaultdict - employees_born_today = frappe.db.multisql({ - "mariadb": """ - SELECT `personal_email`, `company`, `company_email`, `user_id`, `employee_name` AS 'name', `image` - FROM `tabEmployee` - WHERE - DAY(date_of_birth) = DAY(%(today)s) - AND - MONTH(date_of_birth) = MONTH(%(today)s) - AND - `status` = 'Active' - """, - "postgres": """ - SELECT "personal_email", "company", "company_email", "user_id", "employee_name" AS 'name', "image" - FROM "tabEmployee" - WHERE - DATE_PART('day', "date_of_birth") = date_part('day', %(today)s) - AND - DATE_PART('month', "date_of_birth") = date_part('month', %(today)s) - AND - "status" = 'Active' - """, - }, dict(today=today()), as_dict=1) - - grouped_employees = defaultdict(lambda: []) - - for employee_doc in employees_born_today: - grouped_employees[employee_doc.get('company')].append(employee_doc) - - return grouped_employees + return employee_doc.get("user_id") or employee_doc.get("personal_email") or employee_doc.get("company_email") def get_holiday_list_for_employee(employee, raise_exception=True): if employee: @@ -390,17 +301,40 @@ def get_holiday_list_for_employee(employee, raise_exception=True): return holiday_list -def is_holiday(employee, date=None, raise_exception=True): - '''Returns True if given Employee has an holiday on the given date - :param employee: Employee `name` - :param date: Date to check. Will check for today if None''' +def is_holiday(employee, date=None, raise_exception=True, only_non_weekly=False, with_description=False): + ''' + Returns True if given Employee has an holiday on the given date + :param employee: Employee `name` + :param date: Date to check. Will check for today if None + :param raise_exception: Raise an exception if no holiday list found, default is True + :param only_non_weekly: Check only non-weekly holidays, default is False + ''' holiday_list = get_holiday_list_for_employee(employee, raise_exception) if not date: date = today() - if holiday_list: - return frappe.get_all('Holiday List', dict(name=holiday_list, holiday_date=date)) and True or False + if not holiday_list: + return False + + filters = { + 'parent': holiday_list, + 'holiday_date': date + } + if only_non_weekly: + filters['weekly_off'] = False + + holidays = frappe.get_all( + 'Holiday', + fields=['description'], + filters=filters, + pluck='description' + ) + + if with_description: + return len(holidays) > 0, holidays + + return len(holidays) > 0 @frappe.whitelist() def deactivate_sales_person(status = None, employee = None): @@ -503,7 +437,6 @@ def get_children(doctype, parent=None, company=None, is_root=False, is_tree=Fals return employees - def on_doctype_update(): frappe.db.add_index("Employee", ["lft", "rgt"]) diff --git a/erpnext/hr/doctype/employee/employee_reminders.py b/erpnext/hr/doctype/employee/employee_reminders.py new file mode 100644 index 0000000000..2155c027a9 --- /dev/null +++ b/erpnext/hr/doctype/employee/employee_reminders.py @@ -0,0 +1,247 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt + +import frappe +from frappe import _ +from frappe.utils import comma_sep, getdate, today, add_months, add_days +from erpnext.hr.doctype.employee.employee import get_all_employee_emails, get_employee_email +from erpnext.hr.utils import get_holidays_for_employee + +# ----------------- +# HOLIDAY REMINDERS +# ----------------- +def send_reminders_in_advance_weekly(): + to_send_in_advance = int(frappe.db.get_single_value("HR Settings", "send_holiday_reminders") or 1) + frequency = frappe.db.get_single_value("HR Settings", "frequency") + if not (to_send_in_advance and frequency == "Weekly"): + return + + send_advance_holiday_reminders("Weekly") + +def send_reminders_in_advance_monthly(): + to_send_in_advance = int(frappe.db.get_single_value("HR Settings", "send_holiday_reminders") or 1) + frequency = frappe.db.get_single_value("HR Settings", "frequency") + if not (to_send_in_advance and frequency == "Monthly"): + return + + send_advance_holiday_reminders("Monthly") + +def send_advance_holiday_reminders(frequency): + """Send Holiday Reminders in Advance to Employees + `frequency` (str): 'Weekly' or 'Monthly' + """ + if frequency == "Weekly": + start_date = getdate() + end_date = add_days(getdate(), 7) + elif frequency == "Monthly": + # Sent on 1st of every month + start_date = getdate() + end_date = add_months(getdate(), 1) + else: + return + + employees = frappe.db.get_all('Employee', pluck='name') + for employee in employees: + holidays = get_holidays_for_employee( + employee, + start_date, end_date, + only_non_weekly=True, + raise_exception=False + ) + + if not (holidays is None): + send_holidays_reminder_in_advance(employee, holidays) + +def send_holidays_reminder_in_advance(employee, holidays): + employee_doc = frappe.get_doc('Employee', employee) + employee_email = get_employee_email(employee_doc) + frequency = frappe.db.get_single_value("HR Settings", "frequency") + + email_header = _("Holidays this Month.") if frequency == "Monthly" else _("Holidays this Week.") + frappe.sendmail( + recipients=[employee_email], + subject=_("Upcoming Holidays Reminder"), + template="holiday_reminder", + args=dict( + reminder_text=_("Hey {}! This email is to remind you about the upcoming holidays.").format(employee_doc.get('first_name')), + message=_("Below is the list of upcoming holidays for you:"), + advance_holiday_reminder=True, + holidays=holidays, + frequency=frequency[:-2] + ), + header=email_header + ) + +# ------------------ +# BIRTHDAY REMINDERS +# ------------------ +def send_birthday_reminders(): + """Send Employee birthday reminders if no 'Stop Birthday Reminders' is not set.""" + to_send = int(frappe.db.get_single_value("HR Settings", "send_birthday_reminders") or 1) + if not to_send: + return + + employees_born_today = get_employees_who_are_born_today() + + for company, birthday_persons in employees_born_today.items(): + employee_emails = get_all_employee_emails(company) + birthday_person_emails = [get_employee_email(doc) for doc in birthday_persons] + recipients = list(set(employee_emails) - set(birthday_person_emails)) + + reminder_text, message = get_birthday_reminder_text_and_message(birthday_persons) + send_birthday_reminder(recipients, reminder_text, birthday_persons, message) + + if len(birthday_persons) > 1: + # special email for people sharing birthdays + for person in birthday_persons: + person_email = person["user_id"] or person["personal_email"] or person["company_email"] + others = [d for d in birthday_persons if d != person] + reminder_text, message = get_birthday_reminder_text_and_message(others) + send_birthday_reminder(person_email, reminder_text, others, message) + +def get_birthday_reminder_text_and_message(birthday_persons): + if len(birthday_persons) == 1: + birthday_person_text = birthday_persons[0]['name'] + else: + # converts ["Jim", "Rim", "Dim"] to Jim, Rim & Dim + person_names = [d['name'] for d in birthday_persons] + birthday_person_text = comma_sep(person_names, frappe._("{0} & {1}"), False) + + reminder_text = _("Today is {0}'s birthday 🎉").format(birthday_person_text) + message = _("A friendly reminder of an important date for our team.") + message += "
" + message += _("Everyone, let’s congratulate {0} on their birthday.").format(birthday_person_text) + + return reminder_text, message + +def send_birthday_reminder(recipients, reminder_text, birthday_persons, message): + frappe.sendmail( + recipients=recipients, + subject=_("Birthday Reminder"), + template="birthday_reminder", + args=dict( + reminder_text=reminder_text, + birthday_persons=birthday_persons, + message=message, + ), + header=_("Birthday Reminder 🎂") + ) + +def get_employees_who_are_born_today(): + """Get all employee born today & group them based on their company""" + return get_employees_having_an_event_today("birthday") + +def get_employees_having_an_event_today(event_type): + """Get all employee who have `event_type` today + & group them based on their company. `event_type` + can be `birthday` or `work_anniversary`""" + + from collections import defaultdict + + # Set column based on event type + if event_type == 'birthday': + condition_column = 'date_of_birth' + elif event_type == 'work_anniversary': + condition_column = 'date_of_joining' + else: + return + + employees_born_today = frappe.db.multisql({ + "mariadb": f""" + SELECT `personal_email`, `company`, `company_email`, `user_id`, `employee_name` AS 'name', `image`, `date_of_joining` + FROM `tabEmployee` + WHERE + DAY({condition_column}) = DAY(%(today)s) + AND + MONTH({condition_column}) = MONTH(%(today)s) + AND + `status` = 'Active' + """, + "postgres": f""" + SELECT "personal_email", "company", "company_email", "user_id", "employee_name" AS 'name', "image" + FROM "tabEmployee" + WHERE + DATE_PART('day', {condition_column}) = date_part('day', %(today)s) + AND + DATE_PART('month', {condition_column}) = date_part('month', %(today)s) + AND + "status" = 'Active' + """, + }, dict(today=today(), condition_column=condition_column), as_dict=1) + + grouped_employees = defaultdict(lambda: []) + + for employee_doc in employees_born_today: + grouped_employees[employee_doc.get('company')].append(employee_doc) + + return grouped_employees + + +# -------------------------- +# WORK ANNIVERSARY REMINDERS +# -------------------------- +def send_work_anniversary_reminders(): + """Send Employee Work Anniversary Reminders if 'Send Work Anniversary Reminders' is checked""" + to_send = int(frappe.db.get_single_value("HR Settings", "send_work_anniversary_reminders") or 1) + if not to_send: + return + + employees_joined_today = get_employees_having_an_event_today("work_anniversary") + + for company, anniversary_persons in employees_joined_today.items(): + employee_emails = get_all_employee_emails(company) + anniversary_person_emails = [get_employee_email(doc) for doc in anniversary_persons] + recipients = list(set(employee_emails) - set(anniversary_person_emails)) + + reminder_text, message = get_work_anniversary_reminder_text_and_message(anniversary_persons) + send_work_anniversary_reminder(recipients, reminder_text, anniversary_persons, message) + + if len(anniversary_persons) > 1: + # email for people sharing work anniversaries + for person in anniversary_persons: + person_email = person["user_id"] or person["personal_email"] or person["company_email"] + others = [d for d in anniversary_persons if d != person] + reminder_text, message = get_work_anniversary_reminder_text_and_message(others) + send_work_anniversary_reminder(person_email, reminder_text, others, message) + +def get_work_anniversary_reminder_text_and_message(anniversary_persons): + if len(anniversary_persons) == 1: + anniversary_person = anniversary_persons[0]['name'] + persons_name = anniversary_person + # Number of years completed at the company + completed_years = getdate().year - anniversary_persons[0]['date_of_joining'].year + anniversary_person += f" completed {completed_years} years" + else: + person_names_with_years = [] + names = [] + for person in anniversary_persons: + person_text = person['name'] + names.append(person_text) + # Number of years completed at the company + completed_years = getdate().year - person['date_of_joining'].year + person_text += f" completed {completed_years} years" + person_names_with_years.append(person_text) + + # converts ["Jim", "Rim", "Dim"] to Jim, Rim & Dim + anniversary_person = comma_sep(person_names_with_years, frappe._("{0} & {1}"), False) + persons_name = comma_sep(names, frappe._("{0} & {1}"), False) + + reminder_text = _("Today {0} at our Company! 🎉").format(anniversary_person) + message = _("A friendly reminder of an important date for our team.") + message += "
" + message += _("Everyone, let’s congratulate {0} on their work anniversary!").format(persons_name) + + return reminder_text, message + +def send_work_anniversary_reminder(recipients, reminder_text, anniversary_persons, message): + frappe.sendmail( + recipients=recipients, + subject=_("Work Anniversary Reminder"), + template="anniversary_reminder", + args=dict( + reminder_text=reminder_text, + anniversary_persons=anniversary_persons, + message=message, + ), + header=_("🎊️🎊️ Work Anniversary Reminder 🎊️🎊️") + ) diff --git a/erpnext/hr/doctype/employee/test_employee.py b/erpnext/hr/doctype/employee/test_employee.py index 8fc7cf1934..5feb6de8f2 100644 --- a/erpnext/hr/doctype/employee/test_employee.py +++ b/erpnext/hr/doctype/employee/test_employee.py @@ -1,7 +1,5 @@ # 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 erpnext @@ -12,29 +10,6 @@ from erpnext.hr.doctype.employee.employee import InactiveEmployeeStatusError test_records = frappe.get_test_records('Employee') class TestEmployee(unittest.TestCase): - def test_birthday_reminders(self): - employee = frappe.get_doc("Employee", frappe.db.sql_list("select name from tabEmployee limit 1")[0]) - employee.date_of_birth = "1992" + frappe.utils.nowdate()[4:] - employee.company_email = "test@example.com" - employee.company = "_Test Company" - employee.save() - - from erpnext.hr.doctype.employee.employee import get_employees_who_are_born_today, send_birthday_reminders - - employees_born_today = get_employees_who_are_born_today() - self.assertTrue(employees_born_today.get("_Test Company")) - - frappe.db.sql("delete from `tabEmail Queue`") - - hr_settings = frappe.get_doc("HR Settings", "HR Settings") - hr_settings.stop_birthday_reminders = 0 - hr_settings.save() - - send_birthday_reminders() - - email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True) - self.assertTrue("Subject: Birthday Reminder" in email_queue[0].message) - def test_employee_status_left(self): employee1 = make_employee("test_employee_1@company.com") employee2 = make_employee("test_employee_2@company.com") diff --git a/erpnext/hr/doctype/employee/test_employee_reminders.py b/erpnext/hr/doctype/employee/test_employee_reminders.py new file mode 100644 index 0000000000..7e560f512d --- /dev/null +++ b/erpnext/hr/doctype/employee/test_employee_reminders.py @@ -0,0 +1,173 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt + +import frappe +import unittest + +from frappe.utils import getdate +from datetime import timedelta +from erpnext.hr.doctype.employee.test_employee import make_employee +from erpnext.hr.doctype.hr_settings.hr_settings import set_proceed_with_frequency_change + + +class TestEmployeeReminders(unittest.TestCase): + @classmethod + def setUpClass(cls): + from erpnext.hr.doctype.holiday_list.test_holiday_list import make_holiday_list + + # Create a test holiday list + test_holiday_dates = cls.get_test_holiday_dates() + test_holiday_list = make_holiday_list( + 'TestHolidayRemindersList', + holiday_dates=[ + {'holiday_date': test_holiday_dates[0], 'description': 'test holiday1'}, + {'holiday_date': test_holiday_dates[1], 'description': 'test holiday2'}, + {'holiday_date': test_holiday_dates[2], 'description': 'test holiday3', 'weekly_off': 1}, + {'holiday_date': test_holiday_dates[3], 'description': 'test holiday4'}, + {'holiday_date': test_holiday_dates[4], 'description': 'test holiday5'}, + {'holiday_date': test_holiday_dates[5], 'description': 'test holiday6'}, + ], + from_date=getdate()-timedelta(days=10), + to_date=getdate()+timedelta(weeks=5) + ) + + # Create a test employee + test_employee = frappe.get_doc( + 'Employee', + make_employee('test@gopher.io', company="_Test Company") + ) + + # Attach the holiday list to employee + test_employee.holiday_list = test_holiday_list.name + test_employee.save() + + # Attach to class + cls.test_employee = test_employee + cls.test_holiday_dates = test_holiday_dates + + @classmethod + def get_test_holiday_dates(cls): + today_date = getdate() + return [ + today_date, + today_date-timedelta(days=4), + today_date-timedelta(days=3), + today_date+timedelta(days=1), + today_date+timedelta(days=3), + today_date+timedelta(weeks=3) + ] + + def setUp(self): + # Clear Email Queue + frappe.db.sql("delete from `tabEmail Queue`") + + def test_is_holiday(self): + from erpnext.hr.doctype.employee.employee import is_holiday + + self.assertTrue(is_holiday(self.test_employee.name)) + self.assertTrue(is_holiday(self.test_employee.name, date=self.test_holiday_dates[1])) + self.assertFalse(is_holiday(self.test_employee.name, date=getdate()-timedelta(days=1))) + + # Test weekly_off holidays + self.assertTrue(is_holiday(self.test_employee.name, date=self.test_holiday_dates[2])) + self.assertFalse(is_holiday(self.test_employee.name, date=self.test_holiday_dates[2], only_non_weekly=True)) + + # Test with descriptions + has_holiday, descriptions = is_holiday(self.test_employee.name, with_description=True) + self.assertTrue(has_holiday) + self.assertTrue('test holiday1' in descriptions) + + def test_birthday_reminders(self): + employee = frappe.get_doc("Employee", frappe.db.sql_list("select name from tabEmployee limit 1")[0]) + employee.date_of_birth = "1992" + frappe.utils.nowdate()[4:] + employee.company_email = "test@example.com" + employee.company = "_Test Company" + employee.save() + + from erpnext.hr.doctype.employee.employee_reminders import get_employees_who_are_born_today, send_birthday_reminders + + employees_born_today = get_employees_who_are_born_today() + self.assertTrue(employees_born_today.get("_Test Company")) + + hr_settings = frappe.get_doc("HR Settings", "HR Settings") + hr_settings.send_birthday_reminders = 1 + hr_settings.save() + + send_birthday_reminders() + + email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True) + self.assertTrue("Subject: Birthday Reminder" in email_queue[0].message) + + def test_work_anniversary_reminders(self): + employee = frappe.get_doc("Employee", frappe.db.sql_list("select name from tabEmployee limit 1")[0]) + employee.date_of_joining = "1998" + frappe.utils.nowdate()[4:] + employee.company_email = "test@example.com" + employee.company = "_Test Company" + employee.save() + + from erpnext.hr.doctype.employee.employee_reminders import get_employees_having_an_event_today, send_work_anniversary_reminders + + employees_having_work_anniversary = get_employees_having_an_event_today('work_anniversary') + self.assertTrue(employees_having_work_anniversary.get("_Test Company")) + + hr_settings = frappe.get_doc("HR Settings", "HR Settings") + hr_settings.send_work_anniversary_reminders = 1 + hr_settings.save() + + send_work_anniversary_reminders() + + email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True) + self.assertTrue("Subject: Work Anniversary Reminder" in email_queue[0].message) + + def test_send_holidays_reminder_in_advance(self): + from erpnext.hr.utils import get_holidays_for_employee + from erpnext.hr.doctype.employee.employee_reminders import send_holidays_reminder_in_advance + + # Get HR settings and enable advance holiday reminders + hr_settings = frappe.get_doc("HR Settings", "HR Settings") + hr_settings.send_holiday_reminders = 1 + set_proceed_with_frequency_change() + hr_settings.frequency = 'Weekly' + hr_settings.save() + + holidays = get_holidays_for_employee( + self.test_employee.get('name'), + getdate(), getdate() + timedelta(days=3), + only_non_weekly=True, + raise_exception=False + ) + + send_holidays_reminder_in_advance( + self.test_employee.get('name'), + holidays + ) + + email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True) + self.assertEqual(len(email_queue), 1) + + def test_advance_holiday_reminders_monthly(self): + from erpnext.hr.doctype.employee.employee_reminders import send_reminders_in_advance_monthly + # Get HR settings and enable advance holiday reminders + hr_settings = frappe.get_doc("HR Settings", "HR Settings") + hr_settings.send_holiday_reminders = 1 + set_proceed_with_frequency_change() + hr_settings.frequency = 'Monthly' + hr_settings.save() + + send_reminders_in_advance_monthly() + + email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True) + self.assertTrue(len(email_queue) > 0) + + def test_advance_holiday_reminders_weekly(self): + from erpnext.hr.doctype.employee.employee_reminders import send_reminders_in_advance_weekly + # Get HR settings and enable advance holiday reminders + hr_settings = frappe.get_doc("HR Settings", "HR Settings") + hr_settings.send_holiday_reminders = 1 + hr_settings.frequency = 'Weekly' + hr_settings.save() + + send_reminders_in_advance_weekly() + + email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True) + self.assertTrue(len(email_queue) > 0) diff --git a/erpnext/hr/doctype/hr_settings/hr_settings.json b/erpnext/hr/doctype/hr_settings/hr_settings.json index 2396a8eee9..8aa3c0ca9f 100644 --- a/erpnext/hr/doctype/hr_settings/hr_settings.json +++ b/erpnext/hr/doctype/hr_settings/hr_settings.json @@ -11,8 +11,14 @@ "emp_created_by", "column_break_4", "standard_working_hours", - "stop_birthday_reminders", "expense_approver_mandatory_in_expense_claim", + "reminders_section", + "send_birthday_reminders", + "column_break_9", + "send_work_anniversary_reminders", + "column_break_11", + "send_holiday_reminders", + "frequency", "leave_settings", "send_leave_notification", "leave_approval_notification_template", @@ -50,13 +56,6 @@ "fieldname": "column_break_4", "fieldtype": "Column Break" }, - { - "default": "0", - "description": "Don't send employee birthday reminders", - "fieldname": "stop_birthday_reminders", - "fieldtype": "Check", - "label": "Stop Birthday Reminders" - }, { "default": "1", "fieldname": "expense_approver_mandatory_in_expense_claim", @@ -142,13 +141,53 @@ "fieldname": "standard_working_hours", "fieldtype": "Int", "label": "Standard Working Hours" + }, + { + "collapsible": 1, + "fieldname": "reminders_section", + "fieldtype": "Section Break", + "label": "Reminders" + }, + { + "default": "1", + "fieldname": "send_holiday_reminders", + "fieldtype": "Check", + "label": "Holidays" + }, + { + "default": "1", + "fieldname": "send_work_anniversary_reminders", + "fieldtype": "Check", + "label": "Work Anniversaries " + }, + { + "default": "Weekly", + "depends_on": "eval:doc.send_holiday_reminders", + "fieldname": "frequency", + "fieldtype": "Select", + "label": "Set the frequency for holiday reminders", + "options": "Weekly\nMonthly" + }, + { + "default": "1", + "fieldname": "send_birthday_reminders", + "fieldtype": "Check", + "label": "Birthdays" + }, + { + "fieldname": "column_break_9", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_11", + "fieldtype": "Column Break" } ], "icon": "fa fa-cog", "idx": 1, "issingle": 1, "links": [], - "modified": "2021-05-11 10:52:56.192773", + "modified": "2021-08-24 14:54:12.834162", "modified_by": "Administrator", "module": "HR", "name": "HR Settings", diff --git a/erpnext/hr/doctype/hr_settings/hr_settings.py b/erpnext/hr/doctype/hr_settings/hr_settings.py index c99df269cc..a47409363c 100644 --- a/erpnext/hr/doctype/hr_settings/hr_settings.py +++ b/erpnext/hr/doctype/hr_settings/hr_settings.py @@ -1,17 +1,79 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt # For license information, please see license.txt -from __future__ import unicode_literals import frappe + from frappe.model.document import Document +from frappe.utils import format_date + +# Wether to proceed with frequency change +PROCEED_WITH_FREQUENCY_CHANGE = False class HRSettings(Document): def validate(self): self.set_naming_series() + # Based on proceed flag + global PROCEED_WITH_FREQUENCY_CHANGE + if not PROCEED_WITH_FREQUENCY_CHANGE: + self.validate_frequency_change() + PROCEED_WITH_FREQUENCY_CHANGE = False + def set_naming_series(self): from erpnext.setup.doctype.naming_series.naming_series import set_by_naming_series set_by_naming_series("Employee", "employee_number", self.get("emp_created_by")=="Naming Series", hide_name_field=True) + + def validate_frequency_change(self): + weekly_job, monthly_job = None, None + + try: + weekly_job = frappe.get_doc( + 'Scheduled Job Type', + 'employee_reminders.send_reminders_in_advance_weekly' + ) + + monthly_job = frappe.get_doc( + 'Scheduled Job Type', + 'employee_reminders.send_reminders_in_advance_monthly' + ) + except frappe.DoesNotExistError: + return + + next_weekly_trigger = weekly_job.get_next_execution() + next_monthly_trigger = monthly_job.get_next_execution() + + if self.freq_changed_from_monthly_to_weekly(): + if next_monthly_trigger < next_weekly_trigger: + self.show_freq_change_warning(next_monthly_trigger, next_weekly_trigger) + + elif self.freq_changed_from_weekly_to_monthly(): + if next_monthly_trigger > next_weekly_trigger: + self.show_freq_change_warning(next_weekly_trigger, next_monthly_trigger) + + def freq_changed_from_weekly_to_monthly(self): + return self.has_value_changed("frequency") and self.frequency == "Monthly" + + def freq_changed_from_monthly_to_weekly(self): + return self.has_value_changed("frequency") and self.frequency == "Weekly" + + def show_freq_change_warning(self, from_date, to_date): + from_date = frappe.bold(format_date(from_date)) + to_date = frappe.bold(format_date(to_date)) + frappe.msgprint( + msg=frappe._('Employees will miss holiday reminders from {} until {}.
Do you want to proceed with this change?').format(from_date, to_date), + title='Confirm change in Frequency', + primary_action={ + 'label': frappe._('Yes, Proceed'), + 'client_action': 'erpnext.proceed_save_with_reminders_frequency_change' + }, + raise_exception=frappe.ValidationError + ) + +@frappe.whitelist() +def set_proceed_with_frequency_change(): + '''Enables proceed with frequency change''' + global PROCEED_WITH_FREQUENCY_CHANGE + PROCEED_WITH_FREQUENCY_CHANGE = True diff --git a/erpnext/hr/doctype/upload_attendance/upload_attendance.py b/erpnext/hr/doctype/upload_attendance/upload_attendance.py index 674c8e3eb4..9c765d7371 100644 --- a/erpnext/hr/doctype/upload_attendance/upload_attendance.py +++ b/erpnext/hr/doctype/upload_attendance/upload_attendance.py @@ -10,7 +10,7 @@ from frappe import _ from frappe.utils.csvutils import UnicodeWriter from frappe.model.document import Document from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee -from erpnext.hr.utils import get_holidays_for_employee +from erpnext.hr.utils import get_holiday_dates_for_employee class UploadAttendance(Document): pass @@ -94,7 +94,7 @@ def get_holidays_for_employees(employees, from_date, to_date): holidays = {} for employee in employees: holiday_list = get_holiday_list_for_employee(employee) - holiday = get_holidays_for_employee(employee, getdate(from_date), getdate(to_date)) + holiday = get_holiday_dates_for_employee(employee, getdate(from_date), getdate(to_date)) if holiday_list not in holidays: holidays[holiday_list] = holiday diff --git a/erpnext/hr/utils.py b/erpnext/hr/utils.py index a1026ce055..15b237d93c 100644 --- a/erpnext/hr/utils.py +++ b/erpnext/hr/utils.py @@ -335,21 +335,44 @@ def get_sal_slip_total_benefit_given(employee, payroll_period, component=False): total_given_benefit_amount = sum_of_given_benefit[0].total_amount return total_given_benefit_amount -def get_holidays_for_employee(employee, start_date, end_date): - holiday_list = get_holiday_list_for_employee(employee) +def get_holiday_dates_for_employee(employee, start_date, end_date): + """return a list of holiday dates for the given employee between start_date and end_date""" + # return only date + holidays = get_holidays_for_employee(employee, start_date, end_date) + + return [cstr(h.holiday_date) for h in holidays] - holidays = frappe.db.sql_list('''select holiday_date from `tabHoliday` - where - parent=%(holiday_list)s - and holiday_date >= %(start_date)s - and holiday_date <= %(end_date)s''', { - "holiday_list": holiday_list, - "start_date": start_date, - "end_date": end_date - }) - holidays = [cstr(i) for i in holidays] +def get_holidays_for_employee(employee, start_date, end_date, raise_exception=True, only_non_weekly=False): + """Get Holidays for a given employee + `employee` (str) + `start_date` (str or datetime) + `end_date` (str or datetime) + `raise_exception` (bool) + `only_non_weekly` (bool) + + return: list of dicts with `holiday_date` and `description` + """ + holiday_list = get_holiday_list_for_employee(employee, raise_exception=raise_exception) + + if not holiday_list: + return [] + + filters = { + 'parent': holiday_list, + 'holiday_date': ('between', [start_date, end_date]) + } + + if only_non_weekly: + filters['weekly_off'] = False + + holidays = frappe.get_all( + 'Holiday', + fields=['description', 'holiday_date'], + filters=filters + ) + return holidays @erpnext.allow_regional diff --git a/erpnext/patches.txt b/erpnext/patches.txt index b86c236a7f..bf0446bb68 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -275,6 +275,7 @@ erpnext.patches.v13_0.remove_attribute_field_from_item_variant_setting erpnext.patches.v13_0.germany_make_custom_fields erpnext.patches.v13_0.germany_fill_debtor_creditor_number erpnext.patches.v13_0.set_pos_closing_as_failed +erpnext.patches.v13_0.rename_stop_to_send_birthday_reminders execute:frappe.rename_doc("Workspace", "Loan Management", "Loans", force=True) erpnext.patches.v13_0.update_timesheet_changes erpnext.patches.v13_0.add_doctype_to_sla #14-06-2021 diff --git a/erpnext/patches/v13_0/rename_stop_to_send_birthday_reminders.py b/erpnext/patches/v13_0/rename_stop_to_send_birthday_reminders.py new file mode 100644 index 0000000000..1787a56025 --- /dev/null +++ b/erpnext/patches/v13_0/rename_stop_to_send_birthday_reminders.py @@ -0,0 +1,23 @@ +import frappe +from frappe.model.utils.rename_field import rename_field + +def execute(): + frappe.reload_doc('hr', 'doctype', 'hr_settings') + + try: + # Rename the field + rename_field('HR Settings', 'stop_birthday_reminders', 'send_birthday_reminders') + + # Reverse the value + old_value = frappe.db.get_single_value('HR Settings', 'send_birthday_reminders') + + frappe.db.set_value( + 'HR Settings', + 'HR Settings', + 'send_birthday_reminders', + 1 if old_value == 0 else 0 + ) + + except Exception as e: + if e.args[0] != 1054: + raise \ No newline at end of file diff --git a/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py b/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py index c7fbb06b10..a1cde08a74 100644 --- a/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py +++ b/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py @@ -9,7 +9,7 @@ from frappe.utils import date_diff, getdate, rounded, add_days, cstr, cint, flt from frappe.model.document import Document from erpnext.payroll.doctype.payroll_period.payroll_period import get_payroll_period_days, get_period_factor from erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment import get_assigned_salary_structure -from erpnext.hr.utils import get_sal_slip_total_benefit_given, get_holidays_for_employee, get_previous_claimed_amount, validate_active_employee +from erpnext.hr.utils import get_sal_slip_total_benefit_given, get_holiday_dates_for_employee, get_previous_claimed_amount, validate_active_employee class EmployeeBenefitApplication(Document): def validate(self): @@ -139,7 +139,7 @@ def get_max_benefits_remaining(employee, on_date, payroll_period): # Then the sum multiply with the no of lwp in that period # Include that amount to the prev_sal_slip_flexi_total to get the actual if have_depends_on_payment_days and per_day_amount_total > 0: - holidays = get_holidays_for_employee(employee, payroll_period_obj.start_date, on_date) + holidays = get_holiday_dates_for_employee(employee, payroll_period_obj.start_date, on_date) working_days = date_diff(on_date, payroll_period_obj.start_date) + 1 leave_days = calculate_lwp(employee, payroll_period_obj.start_date, holidays, working_days) leave_days_amount = leave_days * per_day_amount_total diff --git a/erpnext/payroll/doctype/payroll_period/payroll_period.py b/erpnext/payroll/doctype/payroll_period/payroll_period.py index ef3a6cc006..66dec075d8 100644 --- a/erpnext/payroll/doctype/payroll_period/payroll_period.py +++ b/erpnext/payroll/doctype/payroll_period/payroll_period.py @@ -7,7 +7,7 @@ import frappe from frappe import _ from frappe.utils import date_diff, getdate, formatdate, cint, month_diff, flt, add_months from frappe.model.document import Document -from erpnext.hr.utils import get_holidays_for_employee +from erpnext.hr.utils import get_holiday_dates_for_employee class PayrollPeriod(Document): def validate(self): @@ -65,7 +65,7 @@ def get_payroll_period_days(start_date, end_date, employee, company=None): actual_no_of_days = date_diff(getdate(payroll_period[0][2]), getdate(payroll_period[0][1])) + 1 working_days = actual_no_of_days if not cint(frappe.db.get_value("Payroll Settings", None, "include_holidays_in_total_working_days")): - holidays = get_holidays_for_employee(employee, getdate(payroll_period[0][1]), getdate(payroll_period[0][2])) + holidays = get_holiday_dates_for_employee(employee, getdate(payroll_period[0][1]), getdate(payroll_period[0][2])) working_days -= len(holidays) return payroll_period[0][0], working_days, actual_no_of_days return False, False, False diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py index 5f5fdd5c83..6325351deb 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -11,6 +11,7 @@ from frappe.model.naming import make_autoname from frappe import msgprint, _ from erpnext.payroll.doctype.payroll_entry.payroll_entry import get_start_end_dates from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee +from erpnext.hr.utils import get_holiday_dates_for_employee from erpnext.utilities.transaction_base import TransactionBase from frappe.utils.background_jobs import enqueue from erpnext.payroll.doctype.additional_salary.additional_salary import get_additional_salaries @@ -337,20 +338,7 @@ class SalarySlip(TransactionBase): return payment_days def get_holidays_for_employee(self, start_date, end_date): - holiday_list = get_holiday_list_for_employee(self.employee) - holidays = frappe.db.sql_list('''select holiday_date from `tabHoliday` - where - parent=%(holiday_list)s - and holiday_date >= %(start_date)s - and holiday_date <= %(end_date)s''', { - "holiday_list": holiday_list, - "start_date": start_date, - "end_date": end_date - }) - - holidays = [cstr(i) for i in holidays] - - return holidays + return get_holiday_dates_for_employee(self.employee, start_date, end_date) def calculate_lwp_or_ppl_based_on_leave_application(self, holidays, working_days): lwp = 0 diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index f240b8ca91..9caf1defe9 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -82,6 +82,17 @@ $.extend(erpnext, { }); frappe.set_route('Form','Journal Entry', journal_entry.name); }); + }, + + proceed_save_with_reminders_frequency_change: () => { + frappe.ui.hide_open_dialog(); + + frappe.call({ + method: 'erpnext.hr.doctype.hr_settings.hr_settings.set_proceed_with_frequency_change', + callback: () => { + cur_frm.save(); + } + }); } }); diff --git a/erpnext/templates/emails/anniversary_reminder.html b/erpnext/templates/emails/anniversary_reminder.html new file mode 100644 index 0000000000..ac9f7e4993 --- /dev/null +++ b/erpnext/templates/emails/anniversary_reminder.html @@ -0,0 +1,25 @@ +
+
+ {% for person in anniversary_persons %} + {% if person.image %} + + + {% else %} + + {{ frappe.utils.get_abbr(person.name) }} + + {% endif %} + {% endfor %} +
+
+ {{ reminder_text }} +

{{ message }}

+
+
\ No newline at end of file diff --git a/erpnext/templates/emails/holiday_reminder.html b/erpnext/templates/emails/holiday_reminder.html new file mode 100644 index 0000000000..e38d27bf8b --- /dev/null +++ b/erpnext/templates/emails/holiday_reminder.html @@ -0,0 +1,16 @@ +
+ {{ reminder_text }} +

{{ message }}

+
+ +{% if advance_holiday_reminder %} + {% if holidays | len > 0 %} +
    + {% for holiday in holidays %} +
  1. {{ frappe.format(holiday.holiday_date, 'Date') }} - {{ holiday.description }}
  2. + {% endfor %} +
+ {% else %} +

You don't have no upcoming holidays this {{ frequency }}.

+ {% endif %} +{% endif %} From 7c957d72b362fbe9f74b5bee7409c7fb0fa33773 Mon Sep 17 00:00:00 2001 From: Pruthvi Patel Date: Tue, 24 Aug 2021 21:24:25 +0530 Subject: [PATCH 47/65] perf: reduce number of queries to validate selling price (#26225) * perf: reduce number of queries to validate selling price * fix: improved flow and formatting * fix: improve condition and use of `as_dict` Co-authored-by: Sagar Vora --- erpnext/controllers/selling_controller.py | 112 ++++++++++++++++------ 1 file changed, 84 insertions(+), 28 deletions(-) diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index da2765dede..fc2cc97e0a 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals import frappe from frappe.utils import cint, flt, cstr, get_link_to_form, nowtime -from frappe import _, throw +from frappe import _, bold, throw from erpnext.stock.get_item_details import get_bin_details from erpnext.stock.utils import get_incoming_rate from erpnext.stock.get_item_details import get_conversion_factor @@ -16,7 +16,6 @@ from erpnext.controllers.stock_controller import StockController from erpnext.controllers.sales_and_purchase_return import get_rate_for_return class SellingController(StockController): - def get_feed(self): return _("To {0} | {1} {2}").format(self.customer_name, self.currency, self.grand_total) @@ -169,39 +168,96 @@ class SellingController(StockController): def validate_selling_price(self): def throw_message(idx, item_name, rate, ref_rate_field): - bold_net_rate = frappe.bold("net rate") - msg = (_("""Row #{}: Selling rate for item {} is lower than its {}. Selling {} should be atleast {}""") - .format(idx, frappe.bold(item_name), frappe.bold(ref_rate_field), bold_net_rate, frappe.bold(rate))) - msg += "

" - msg += (_("""You can alternatively disable selling price validation in {} to bypass this validation.""") - .format(get_link_to_form("Selling Settings", "Selling Settings"))) - frappe.throw(msg, title=_("Invalid Selling Price")) + throw(_("""Row #{0}: Selling rate for item {1} is lower than its {2}. + Selling {3} should be atleast {4}.

Alternatively, + you can disable selling price validation in {5} to bypass + this validation.""").format( + idx, + bold(item_name), + bold(ref_rate_field), + bold("net rate"), + bold(rate), + get_link_to_form("Selling Settings", "Selling Settings"), + ), title=_("Invalid Selling Price")) - if not frappe.db.get_single_value("Selling Settings", "validate_selling_price"): - return - if hasattr(self, "is_return") and self.is_return: + if ( + self.get("is_return") + or not frappe.db.get_single_value("Selling Settings", "validate_selling_price") + ): return - for it in self.get("items"): - if not it.item_code: + is_internal_customer = self.get('is_internal_customer') + valuation_rate_map = {} + + for item in self.items: + if not item.item_code: continue - last_purchase_rate, is_stock_item = frappe.get_cached_value("Item", it.item_code, ["last_purchase_rate", "is_stock_item"]) - last_purchase_rate_in_sales_uom = last_purchase_rate * (it.conversion_factor or 1) - if flt(it.base_net_rate) < flt(last_purchase_rate_in_sales_uom): - throw_message(it.idx, frappe.bold(it.item_name), last_purchase_rate_in_sales_uom, "last purchase rate") + last_purchase_rate, is_stock_item = frappe.get_cached_value( + "Item", item.item_code, ("last_purchase_rate", "is_stock_item") + ) - last_valuation_rate = frappe.db.sql(""" - SELECT valuation_rate FROM `tabStock Ledger Entry` WHERE item_code = %s - AND warehouse = %s AND valuation_rate > 0 - ORDER BY posting_date DESC, posting_time DESC, creation DESC LIMIT 1 - """, (it.item_code, it.warehouse)) - if last_valuation_rate: - last_valuation_rate_in_sales_uom = last_valuation_rate[0][0] * (it.conversion_factor or 1) - if is_stock_item and flt(it.base_net_rate) < flt(last_valuation_rate_in_sales_uom) \ - and not self.get('is_internal_customer'): - throw_message(it.idx, frappe.bold(it.item_name), last_valuation_rate_in_sales_uom, "valuation rate") + last_purchase_rate_in_sales_uom = ( + last_purchase_rate * (item.conversion_factor or 1) + ) + if flt(item.base_net_rate) < flt(last_purchase_rate_in_sales_uom): + throw_message( + item.idx, + item.item_name, + last_purchase_rate_in_sales_uom, + "last purchase rate" + ) + + if is_internal_customer or not is_stock_item: + continue + + valuation_rate_map[(item.item_code, item.warehouse)] = None + + if not valuation_rate_map: + return + + or_conditions = ( + f"""(item_code = {frappe.db.escape(valuation_rate[0])} + and warehouse = {frappe.db.escape(valuation_rate[1])})""" + for valuation_rate in valuation_rate_map + ) + + valuation_rates = frappe.db.sql(f""" + select + item_code, warehouse, valuation_rate + from + `tabBin` + where + ({" or ".join(or_conditions)}) + and valuation_rate > 0 + """, as_dict=True) + + for rate in valuation_rates: + valuation_rate_map[(rate.item_code, rate.warehouse)] = rate.valuation_rate + + for item in self.items: + if not item.item_code: + continue + + last_valuation_rate = valuation_rate_map.get( + (item.item_code, item.warehouse) + ) + + if not last_valuation_rate: + continue + + last_valuation_rate_in_sales_uom = ( + last_valuation_rate * (item.conversion_factor or 1) + ) + + if flt(item.base_net_rate) < flt(last_valuation_rate_in_sales_uom): + throw_message( + item.idx, + item.item_name, + last_valuation_rate_in_sales_uom, + "valuation rate" + ) def get_item_list(self): il = [] From c30fb04e960c944cda3ea84865f2563bc50b29ed Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Tue, 24 Aug 2021 21:57:25 +0530 Subject: [PATCH 48/65] fix(minor): Update GSTR-1 json version (#27074) --- erpnext/regional/report/gstr_1/gstr_1.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/regional/report/gstr_1/gstr_1.py b/erpnext/regional/report/gstr_1/gstr_1.py index 4b7309440c..9d4f9206f5 100644 --- a/erpnext/regional/report/gstr_1/gstr_1.py +++ b/erpnext/regional/report/gstr_1/gstr_1.py @@ -588,7 +588,7 @@ def get_json(filters, report_name, data): fp = "%02d%s" % (getdate(filters["to_date"]).month, getdate(filters["to_date"]).year) - gst_json = {"version": "GST2.2.9", + gst_json = {"version": "GST3.0.4", "hash": "hash", "gstin": gstin, "fp": fp} res = {} @@ -765,7 +765,7 @@ def get_cdnr_reg_json(res, gstin): "ntty": invoice[0]["document_type"], "pos": "%02d" % int(invoice[0]["place_of_supply"].split('-')[0]), "rchrg": invoice[0]["reverse_charge"], - "inv_type": get_invoice_type_for_cdnr(invoice[0]) + "inv_typ": get_invoice_type_for_cdnr(invoice[0]) } inv_item["itms"] = [] From 512ddc589f07cb9a938f0ef742db5a2e075ce55d Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Tue, 24 Aug 2021 22:00:32 +0530 Subject: [PATCH 49/65] fix: timesheet amount issue (#25993) (#26889) * fix: timesheet amount issue * fix: timesheet detail rate conversion * fix: condition to check timesheet currency * fix: removing console statement (cherry picked from commit a6aa6cd7d63eb426b986d995985985c2aae4f553) Co-authored-by: Anupam Kumar Co-authored-by: Nabin Hait --- erpnext/accounts/doctype/sales_invoice/sales_invoice.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 2071827d99..d8dee99b99 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -443,6 +443,15 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e this.frm.refresh_field("outstanding_amount"); this.frm.refresh_field("paid_amount"); this.frm.refresh_field("base_paid_amount"); + }, + + currency() { + this._super(); + $.each(cur_frm.doc.timesheets, function(i, d) { + let row = frappe.get_doc(d.doctype, d.name) + set_timesheet_detail_rate(row.doctype, row.name, cur_frm.doc.currency, row.timesheet_detail) + }); + calculate_total_billing_amount(cur_frm) } currency() { From 1d6ef4b0d3dd84ad84c64c602a919f6776c62eb4 Mon Sep 17 00:00:00 2001 From: Anupam Kumar Date: Tue, 24 Aug 2021 22:06:19 +0530 Subject: [PATCH 50/65] fix: replacing $.each -> forEach in SI (#26995) * fix: replacing $.each -> forEach in SI * fix: removed console log Co-authored-by: Nabin Hait --- .../doctype/sales_invoice/sales_invoice.js | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index d8dee99b99..1e37e4b207 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -154,9 +154,9 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e return } - $.each(doc["items"], function(i, row) { + doc.items.forEach((row) => { if(row.delivery_note) frappe.model.clear_doc("Delivery Note", row.delivery_note) - }) + }); } set_default_print_format() { @@ -455,12 +455,15 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e } currency() { + var me = this; super.currency(); - $.each(cur_frm.doc.timesheets, function(i, d) { - let row = frappe.get_doc(d.doctype, d.name) - set_timesheet_detail_rate(row.doctype, row.name, cur_frm.doc.currency, row.timesheet_detail) - }); - calculate_total_billing_amount(cur_frm) + if (this.frm.doc.timesheets) { + this.frm.doc.timesheets.forEach((d) => { + let row = frappe.get_doc(d.doctype, d.name) + set_timesheet_detail_rate(row.doctype, row.name, me.frm.doc.currency, row.timesheet_detail) + }); + calculate_total_billing_amount(this.frm); + } } }; @@ -983,9 +986,9 @@ var calculate_total_billing_amount = function(frm) { doc.total_billing_amount = 0.0 if (doc.timesheets) { - $.each(doc.timesheets, function(index, data){ - doc.total_billing_amount += flt(data.billing_amount) - }) + doc.timesheets.forEach((d) => { + doc.total_billing_amount += flt(d.billing_amount) + }); } refresh_field('total_billing_amount') From c7bad657b1f73aa2d1232b416752128afc5f77e8 Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Tue, 24 Aug 2021 22:10:14 +0530 Subject: [PATCH 51/65] fix: broken URL in supplier portal (#26823) * fix: broken URL The quotations are supplier quotations, not sales quotation. * fix: remove erpnext from path --- erpnext/templates/includes/rfq/rfq_items.html | 2 +- erpnext/templates/pages/rfq.html | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/erpnext/templates/includes/rfq/rfq_items.html b/erpnext/templates/includes/rfq/rfq_items.html index caa15f386b..04cf922664 100644 --- a/erpnext/templates/includes/rfq/rfq_items.html +++ b/erpnext/templates/includes/rfq/rfq_items.html @@ -1,4 +1,4 @@ -{% from "erpnext/templates/includes/rfq/rfq_macros.html" import item_name_and_description %} +{% from "templates/includes/rfq/rfq_macros.html" import item_name_and_description %} {% for d in doc.items %}
diff --git a/erpnext/templates/pages/rfq.html b/erpnext/templates/pages/rfq.html index 6e2edb6391..6516482c23 100644 --- a/erpnext/templates/pages/rfq.html +++ b/erpnext/templates/pages/rfq.html @@ -86,7 +86,7 @@ {{d.transaction_date}}
- Link + Link
{% endfor %} @@ -95,6 +95,4 @@ - - {% endblock %} From fbc59772483cbe17c8ec58cbe806b38d01486ac6 Mon Sep 17 00:00:00 2001 From: Subin Tom <36098155+nemesis189@users.noreply.github.com> Date: Tue, 24 Aug 2021 22:34:02 +0530 Subject: [PATCH 52/65] fix: Updated timestamp for pos invoice json (#27110) Co-authored-by: Afshan <33727827+AfshanKhan@users.noreply.github.com> --- erpnext/accounts/doctype/pos_invoice/pos_invoice.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json index b819537400..19c6c8f347 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json @@ -1564,7 +1564,7 @@ "icon": "fa fa-file-text", "is_submittable": 1, "links": [], - "modified": "2021-08-18 16:13:52.080543", + "modified": "2021-08-24 18:19:20.728433", "modified_by": "Administrator", "module": "Accounts", "name": "POS Invoice", From 1dadea38a55346c1569a46029be39d662d7be139 Mon Sep 17 00:00:00 2001 From: Saqib Date: Wed, 25 Aug 2021 11:05:51 +0530 Subject: [PATCH 53/65] fix: remove unexpected comma (#27132) --- erpnext/accounts/doctype/sales_invoice/sales_invoice.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 1e37e4b207..2dd3d690e9 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -443,7 +443,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e this.frm.refresh_field("outstanding_amount"); this.frm.refresh_field("paid_amount"); this.frm.refresh_field("base_paid_amount"); - }, + } currency() { this._super(); From 8366b6322e5fe9aae5a7c2d20f4061fd07f197fc Mon Sep 17 00:00:00 2001 From: Afshan <33727827+AfshanKhan@users.noreply.github.com> Date: Wed, 25 Aug 2021 11:57:24 +0530 Subject: [PATCH 54/65] fix: validate party and party type only if both available (#27002) * fix: validate party and party type only if both available * fix: indentation --- .../accounts/report/general_ledger/general_ledger.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index 5d8d49d6a6..3723c8e0d2 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -78,13 +78,10 @@ def validate_filters(filters, account_details): def validate_party(filters): party_type, party = filters.get("party_type"), filters.get("party") - if party: - if not party_type: - frappe.throw(_("To filter based on Party, select Party Type first")) - else: - for d in party: - if not frappe.db.exists(party_type, d): - frappe.throw(_("Invalid {0}: {1}").format(party_type, d)) + if party and party_type: + for d in party: + if not frappe.db.exists(party_type, d): + frappe.throw(_("Invalid {0}: {1}").format(party_type, d)) def set_account_currency(filters): if filters.get("account") or (filters.get('party') and len(filters.party) == 1): From a65498dc610287640e76dc8a646e9ada0c29a44a Mon Sep 17 00:00:00 2001 From: Chillar Anand Date: Wed, 25 Aug 2021 14:40:22 +0530 Subject: [PATCH 55/65] fix(healthcare): Made payment fields mandatory for new appointments (#26608) * fix(healthcare): Made payment fields mandatory for new appointments * fix: sider issues * fix: Fix failing test * fix: Patient appointment invoicing Co-authored-by: Rucha Mahabal Co-authored-by: Syed Mujeer Hashmi --- .../doctype/fee_validity/test_fee_validity.py | 4 +-- .../patient_appointment.js | 7 ++++++ .../patient_appointment.json | 5 ++-- .../patient_appointment.py | 8 +----- .../test_patient_appointment.py | 25 ++++++++++++++++++- 5 files changed, 37 insertions(+), 12 deletions(-) diff --git a/erpnext/healthcare/doctype/fee_validity/test_fee_validity.py b/erpnext/healthcare/doctype/fee_validity/test_fee_validity.py index 6ae3e12d50..82e7136d6b 100644 --- a/erpnext/healthcare/doctype/fee_validity/test_fee_validity.py +++ b/erpnext/healthcare/doctype/fee_validity/test_fee_validity.py @@ -29,10 +29,10 @@ class TestFeeValidity(unittest.TestCase): healthcare_settings.save(ignore_permissions=True) patient, medical_department, practitioner = create_healthcare_docs() - # appointment should not be invoiced. Check Fee Validity created for new patient + # For first appointment, invoice is generated appointment = create_appointment(patient, practitioner, nowdate()) invoiced = frappe.db.get_value("Patient Appointment", appointment.name, "invoiced") - self.assertEqual(invoiced, 0) + self.assertEqual(invoiced, 1) # appointment should not be invoiced as it is within fee validity appointment = create_appointment(patient, practitioner, add_days(nowdate(), 4)) diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js index 2976ef13a1..c6e489ec17 100644 --- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js +++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js @@ -241,6 +241,13 @@ frappe.ui.form.on('Patient Appointment', { frm.toggle_reqd('mode_of_payment', 0); frm.toggle_reqd('paid_amount', 0); frm.toggle_reqd('billing_item', 0); + } else if (data.message) { + frm.toggle_display('mode_of_payment', 1); + frm.toggle_display('paid_amount', 1); + frm.toggle_display('billing_item', 1); + frm.toggle_reqd('mode_of_payment', 1); + frm.toggle_reqd('paid_amount', 1); + frm.toggle_reqd('billing_item', 1); } else { // if automated appointment invoicing is disabled, hide fields frm.toggle_display('mode_of_payment', data.message ? 1 : 0); diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json index 83c92af36a..6e996bd128 100644 --- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json +++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json @@ -134,6 +134,7 @@ "set_only_once": 1 }, { + "depends_on": "eval:doc.practitioner;", "fieldname": "section_break_12", "fieldtype": "Section Break", "label": "Appointment Details" @@ -349,7 +350,7 @@ } ], "links": [], - "modified": "2021-02-08 13:13:15.116833", + "modified": "2021-06-16 00:40:26.841794", "modified_by": "Administrator", "module": "Healthcare", "name": "Patient Appointment", @@ -400,4 +401,4 @@ "title_field": "title", "track_changes": 1, "track_seen": 1 -} \ No newline at end of file +} diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py index cdd4ad39c8..05e2cd30df 100755 --- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py +++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py @@ -135,8 +135,6 @@ def check_payment_fields_reqd(patient): fee_validity = frappe.db.exists('Fee Validity', {'patient': patient, 'status': 'Pending'}) if fee_validity: return {'fee_validity': fee_validity} - if check_is_new_patient(patient): - return False return True return False @@ -151,8 +149,6 @@ def invoice_appointment(appointment_doc): elif not fee_validity: if frappe.db.exists('Fee Validity Reference', {'appointment': appointment_doc.name}): return - if check_is_new_patient(appointment_doc.patient, appointment_doc.name): - return else: fee_validity = None @@ -196,9 +192,7 @@ def check_is_new_patient(patient, name=None): filters['name'] = ('!=', name) has_previous_appointment = frappe.db.exists('Patient Appointment', filters) - if has_previous_appointment: - return False - return True + return not has_previous_appointment def get_appointment_item(appointment_doc, item): diff --git a/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py b/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py index 9dd4a2c73c..2df6921b14 100644 --- a/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py +++ b/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py @@ -4,11 +4,12 @@ from __future__ import unicode_literals import unittest import frappe -from erpnext.healthcare.doctype.patient_appointment.patient_appointment import update_status, make_encounter +from erpnext.healthcare.doctype.patient_appointment.patient_appointment import update_status, make_encounter, check_payment_fields_reqd, check_is_new_patient from frappe.utils import nowdate, add_days, now_datetime from frappe.utils.make_random import get_random from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile + class TestPatientAppointment(unittest.TestCase): def setUp(self): frappe.db.sql("""delete from `tabPatient Appointment`""") @@ -176,6 +177,28 @@ class TestPatientAppointment(unittest.TestCase): mark_invoiced_inpatient_occupancy(ip_record1) discharge_patient(ip_record1) + def test_payment_should_be_mandatory_for_new_patient_appointment(self): + frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 1) + frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1) + frappe.db.set_value('Healthcare Settings', None, 'max_visits', 3) + frappe.db.set_value('Healthcare Settings', None, 'valid_days', 30) + + patient = create_patient() + assert check_is_new_patient(patient) + payment_required = check_payment_fields_reqd(patient) + assert payment_required is True + + def test_sales_invoice_should_be_generated_for_new_patient_appointment(self): + patient, medical_department, practitioner = create_healthcare_docs() + frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1) + invoice_count = frappe.db.count('Sales Invoice') + + assert check_is_new_patient(patient) + create_appointment(patient, practitioner, nowdate()) + new_invoice_count = frappe.db.count('Sales Invoice') + + assert new_invoice_count == invoice_count + 1 + def create_healthcare_docs(): patient = create_patient() From 62114b226f30a5f4c634c2f17fd47a94149c6df8 Mon Sep 17 00:00:00 2001 From: Saqib Date: Wed, 25 Aug 2021 16:25:51 +0530 Subject: [PATCH 56/65] feat: unreconcile on cancellation of bank transaction (#27109) --- .../bank_transaction/bank_transaction.py | 31 +++++++++++++------ .../bank_transaction/bank_transaction_list.js | 6 ++-- .../bank_transaction/test_bank_transaction.py | 9 +++++- erpnext/controllers/status_updater.py | 3 +- 4 files changed, 36 insertions(+), 13 deletions(-) diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py index 31cfb2da1d..0544a469d6 100644 --- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py +++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py @@ -21,6 +21,10 @@ class BankTransaction(StatusUpdater): self.update_allocations() self.clear_linked_payment_entries() self.set_status(update=True) + + def on_cancel(self): + self.clear_linked_payment_entries(for_cancel=True) + self.set_status(update=True) def update_allocations(self): if self.payment_entries: @@ -41,21 +45,30 @@ class BankTransaction(StatusUpdater): frappe.db.set_value(self.doctype, self.name, "status", "Reconciled") self.reload() - - def clear_linked_payment_entries(self): + + def clear_linked_payment_entries(self, for_cancel=False): for payment_entry in self.payment_entries: if payment_entry.payment_document in ["Payment Entry", "Journal Entry", "Purchase Invoice", "Expense Claim"]: - self.clear_simple_entry(payment_entry) + self.clear_simple_entry(payment_entry, for_cancel=for_cancel) elif payment_entry.payment_document == "Sales Invoice": - self.clear_sales_invoice(payment_entry) + self.clear_sales_invoice(payment_entry, for_cancel=for_cancel) - def clear_simple_entry(self, payment_entry): - frappe.db.set_value(payment_entry.payment_document, payment_entry.payment_entry, "clearance_date", self.date) + def clear_simple_entry(self, payment_entry, for_cancel=False): + clearance_date = self.date if not for_cancel else None + frappe.db.set_value( + payment_entry.payment_document, payment_entry.payment_entry, + "clearance_date", clearance_date) - def clear_sales_invoice(self, payment_entry): - frappe.db.set_value("Sales Invoice Payment", dict(parenttype=payment_entry.payment_document, - parent=payment_entry.payment_entry), "clearance_date", self.date) + def clear_sales_invoice(self, payment_entry, for_cancel=False): + clearance_date = self.date if not for_cancel else None + frappe.db.set_value( + "Sales Invoice Payment", + dict( + parenttype=payment_entry.payment_document, + parent=payment_entry.payment_entry + ), + "clearance_date", clearance_date) def get_total_allocated_amount(payment_entry): return frappe.db.sql(""" diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction_list.js b/erpnext/accounts/doctype/bank_transaction/bank_transaction_list.js index bff41d5539..2585ee9c92 100644 --- a/erpnext/accounts/doctype/bank_transaction/bank_transaction_list.js +++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction_list.js @@ -4,10 +4,12 @@ frappe.listview_settings['Bank Transaction'] = { add_fields: ["unallocated_amount"], get_indicator: function(doc) { - if(flt(doc.unallocated_amount)>0) { - return [__("Unreconciled"), "orange", "unallocated_amount,>,0"]; + if(doc.docstatus == 2) { + return [__("Cancelled"), "red", "docstatus,=,2"]; } else if(flt(doc.unallocated_amount)<=0) { return [__("Reconciled"), "green", "unallocated_amount,=,0"]; + } else if(flt(doc.unallocated_amount)>0) { + return [__("Unreconciled"), "orange", "unallocated_amount,>,0"]; } } }; diff --git a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py index ce149f96e6..439d489119 100644 --- a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py +++ b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py @@ -25,7 +25,8 @@ class TestBankTransaction(unittest.TestCase): def tearDownClass(cls): for bt in frappe.get_all("Bank Transaction"): doc = frappe.get_doc("Bank Transaction", bt.name) - doc.cancel() + if doc.docstatus == 1: + doc.cancel() doc.delete() # Delete directly in DB to avoid validation errors for countries not allowing deletion @@ -57,6 +58,12 @@ class TestBankTransaction(unittest.TestCase): clearance_date = frappe.db.get_value("Payment Entry", payment.name, "clearance_date") self.assertTrue(clearance_date is not None) + bank_transaction.reload() + bank_transaction.cancel() + + clearance_date = frappe.db.get_value("Payment Entry", payment.name, "clearance_date") + self.assertFalse(clearance_date) + # Check if ERPNext can correctly filter a linked payments based on the debit/credit amount def test_debit_credit_output(self): bank_transaction = frappe.get_doc("Bank Transaction", dict(description="Auszahlung Karte MC/000002916 AUTOMAT 698769 K002 27.10. 14:07")) diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py index b1f89b08d7..7b24e50b14 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -86,7 +86,8 @@ status_map = { ], "Bank Transaction": [ ["Unreconciled", "eval:self.docstatus == 1 and self.unallocated_amount>0"], - ["Reconciled", "eval:self.docstatus == 1 and self.unallocated_amount<=0"] + ["Reconciled", "eval:self.docstatus == 1 and self.unallocated_amount<=0"], + ["Cancelled", "eval:self.docstatus == 2"] ], "POS Opening Entry": [ ["Draft", None], From dbea840ecfc82cb94fb2fb90f6125b97bb2e9150 Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Wed, 25 Aug 2021 16:34:31 +0530 Subject: [PATCH 57/65] fix: re-order tour to circumvent glitchy save highlight --- .../form_tour/stock_entry/stock_entry.json | 4 +- .../stock_reconciliation.json | 28 +++++------ .../stock_settings/stock_settings.json | 50 +++++++++---------- 3 files changed, 41 insertions(+), 41 deletions(-) diff --git a/erpnext/stock/form_tour/stock_entry/stock_entry.json b/erpnext/stock/form_tour/stock_entry/stock_entry.json index 98c5d62f10..6363c6ad4d 100644 --- a/erpnext/stock/form_tour/stock_entry/stock_entry.json +++ b/erpnext/stock/form_tour/stock_entry/stock_entry.json @@ -4,7 +4,7 @@ "doctype": "Form Tour", "idx": 0, "is_standard": 1, - "modified": "2021-08-24 14:50:48.017420", + "modified": "2021-08-25 16:31:31.441194", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry", @@ -48,7 +48,7 @@ "label": "Items", "next_step_condition": "eval: doc.items[0]?.item_code", "parent_field": "", - "position": "Bottom", + "position": "Top", "title": "Items" } ], diff --git a/erpnext/stock/form_tour/stock_reconciliation/stock_reconciliation.json b/erpnext/stock/form_tour/stock_reconciliation/stock_reconciliation.json index 83083b639a..5b7fd72c08 100644 --- a/erpnext/stock/form_tour/stock_reconciliation/stock_reconciliation.json +++ b/erpnext/stock/form_tour/stock_reconciliation/stock_reconciliation.json @@ -4,7 +4,7 @@ "doctype": "Form Tour", "idx": 0, "is_standard": 1, - "modified": "2021-08-24 14:50:41.512219", + "modified": "2021-08-25 16:26:11.718664", "modified_by": "Administrator", "module": "Stock", "name": "Stock Reconciliation", @@ -25,6 +25,19 @@ "position": "Top", "title": "Purpose" }, + { + "description": "Select the items for which the opening stock has to be set.", + "field": "", + "fieldname": "items", + "fieldtype": "Table", + "has_next_condition": 1, + "is_table_field": 0, + "label": "Items", + "next_step_condition": "eval: doc.items[0]?.item_code", + "parent_field": "", + "position": "Top", + "title": "Items" + }, { "description": "Edit the Posting Date by clicking on the Edit Posting Date and Time checkbox below.", "field": "", @@ -36,19 +49,6 @@ "parent_field": "", "position": "Bottom", "title": "Posting Date" - }, - { - "description": "Select the items for which the opening stock has to be set.", - "field": "", - "fieldname": "items", - "fieldtype": "Table", - "has_next_condition": 1, - "is_table_field": 0, - "label": "Items", - "next_step_condition": "eval: doc.items[0]?.item_code", - "parent_field": "", - "position": "Bottom", - "title": "Items" } ], "title": "Stock Reconciliation" diff --git a/erpnext/stock/form_tour/stock_settings/stock_settings.json b/erpnext/stock/form_tour/stock_settings/stock_settings.json index 282b87c21f..3d164e33b3 100644 --- a/erpnext/stock/form_tour/stock_settings/stock_settings.json +++ b/erpnext/stock/form_tour/stock_settings/stock_settings.json @@ -4,7 +4,7 @@ "doctype": "Form Tour", "idx": 0, "is_standard": 1, - "modified": "2021-08-20 15:37:36.151783", + "modified": "2021-08-25 16:19:37.699528", "modified_by": "Administrator", "module": "Stock", "name": "Stock Settings", @@ -36,30 +36,6 @@ "position": "Bottom", "title": "Default Warehouse" }, - { - "description": "Choose between FIFO and Moving Average Valuation Methods. Click here to know more about them.", - "field": "", - "fieldname": "valuation_method", - "fieldtype": "Select", - "has_next_condition": 0, - "is_table_field": 0, - "label": "Default Valuation Method", - "parent_field": "", - "position": "Bottom", - "title": "Default Valuation Method" - }, - { - "description": "Show 'Scan Barcode' field above every child table to insert Items with ease.", - "field": "", - "fieldname": "show_barcode_field", - "fieldtype": "Check", - "has_next_condition": 0, - "is_table_field": 0, - "label": "Show Barcode Field in Stock Transactions", - "parent_field": "", - "position": "Bottom", - "title": "Show Barcode Field" - }, { "description": "Quality inspection is performed on the inward and outward movement of goods. Receipt and delivery transactions will be stopped or the user will be warned if the quality inspection is not performed.", "field": "", @@ -83,6 +59,30 @@ "parent_field": "", "position": "Bottom", "title": "Automatically Set Serial Nos based on FIFO" + }, + { + "description": "Show 'Scan Barcode' field above every child table to insert Items with ease.", + "field": "", + "fieldname": "show_barcode_field", + "fieldtype": "Check", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Show Barcode Field in Stock Transactions", + "parent_field": "", + "position": "Bottom", + "title": "Show Barcode Field" + }, + { + "description": "Choose between FIFO and Moving Average Valuation Methods. Click here to know more about them.", + "field": "", + "fieldname": "valuation_method", + "fieldtype": "Select", + "has_next_condition": 0, + "is_table_field": 0, + "label": "Default Valuation Method", + "parent_field": "", + "position": "Bottom", + "title": "Default Valuation Method" } ], "title": "Stock Settings" From 8d116fb9ff45b193d4dad75db700439bc6fa5799 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 25 Aug 2021 16:45:41 +0530 Subject: [PATCH 58/65] fix: remove VARCHARs from Sales Invoice (#27136) Sales Invoice doctype is starting to hit row length limit as many integrations add custom fields on this doctype. This is just a small change to remove VARCHAR(140) fields and reduce row size wherever possible. --- .../doctype/sales_invoice/sales_invoice.json | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index 5023c9c61a..d8aa32e224 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -247,7 +247,7 @@ "depends_on": "customer", "fetch_from": "customer.customer_name", "fieldname": "customer_name", - "fieldtype": "Data", + "fieldtype": "Small Text", "hide_days": 1, "hide_seconds": 1, "in_global_search": 1, @@ -692,10 +692,11 @@ { "fieldname": "scan_barcode", "fieldtype": "Data", - "options": "Barcode", "hide_days": 1, "hide_seconds": 1, - "label": "Scan Barcode" + "label": "Scan Barcode", + "length": 1, + "options": "Barcode" }, { "allow_bulk_edit": 1, @@ -1059,6 +1060,7 @@ "hide_days": 1, "hide_seconds": 1, "label": "Apply Additional Discount On", + "length": 15, "options": "\nGrand Total\nNet Total", "print_hide": 1 }, @@ -1145,7 +1147,7 @@ { "description": "In Words will be visible once you save the Sales Invoice.", "fieldname": "base_in_words", - "fieldtype": "Data", + "fieldtype": "Small Text", "hide_days": 1, "hide_seconds": 1, "label": "In Words (Company Currency)", @@ -1205,7 +1207,7 @@ }, { "fieldname": "in_words", - "fieldtype": "Data", + "fieldtype": "Small Text", "hide_days": 1, "hide_seconds": 1, "label": "In Words", @@ -1558,6 +1560,7 @@ "hide_days": 1, "hide_seconds": 1, "label": "Print Language", + "length": 6, "print_hide": 1, "read_only": 1 }, @@ -1645,6 +1648,7 @@ "hide_seconds": 1, "in_standard_filter": 1, "label": "Status", + "length": 30, "no_copy": 1, "options": "\nDraft\nReturn\nCredit Note Issued\nSubmitted\nPaid\nUnpaid\nUnpaid and Discounted\nOverdue and Discounted\nOverdue\nCancelled\nInternal Transfer", "print_hide": 1, @@ -1704,6 +1708,7 @@ "hide_days": 1, "hide_seconds": 1, "label": "Is Opening Entry", + "length": 4, "oldfieldname": "is_opening", "oldfieldtype": "Select", "options": "No\nYes", @@ -1715,6 +1720,7 @@ "hide_days": 1, "hide_seconds": 1, "label": "C-Form Applicable", + "length": 4, "no_copy": 1, "options": "No\nYes", "print_hide": 1 @@ -2015,7 +2021,7 @@ "link_fieldname": "consolidated_invoice" } ], - "modified": "2021-08-18 16:07:45.122570", + "modified": "2021-08-25 14:46:05.279588", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice", From 81b459047944c79443369658f4d6fabcb4e3cb20 Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Wed, 25 Aug 2021 18:18:16 +0530 Subject: [PATCH 59/65] fix: TDS calculation on net total (#27058) (#27140) (cherry picked from commit 4eb7c2a011f3abbe547c2a32978e7af6e1fd6d33) Co-authored-by: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> --- .../tax_withholding_category.py | 9 +++--- .../test_tax_withholding_category.py | 32 ++++++++++++++++++- 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py index 1536a237de..0cb872c4b8 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -240,14 +240,15 @@ def get_deducted_tax(taxable_vouchers, fiscal_year, tax_details): def get_tds_amount(ldc, parties, inv, tax_details, fiscal_year_details, tax_deducted, vouchers): tds_amount = 0 invoice_filters = { - 'name': ('in', vouchers), - 'docstatus': 1 + 'name': ('in', vouchers), + 'docstatus': 1, + 'apply_tds': 1 } field = 'sum(net_total)' - if not cint(tax_details.consider_party_ledger_amount): - invoice_filters.update({'apply_tds': 1}) + if cint(tax_details.consider_party_ledger_amount): + invoice_filters.pop('apply_tds', None) field = 'sum(grand_total)' supp_credit_amt = frappe.db.get_value('Purchase Invoice', invoice_filters, field) or 0.0 diff --git a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py index 1c687e5cb1..0f921db678 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py @@ -145,6 +145,36 @@ class TestTaxWithholdingCategory(unittest.TestCase): for d in invoices: d.cancel() + def test_tds_calculation_on_net_total(self): + frappe.db.set_value("Supplier", "Test TDS Supplier4", "tax_withholding_category", "Cumulative Threshold TDS") + invoices = [] + + pi = create_purchase_invoice(supplier = "Test TDS Supplier4", rate = 20000, do_not_save=True) + pi.append('taxes', { + "category": "Total", + "charge_type": "Actual", + "account_head": '_Test Account VAT - _TC', + "cost_center": 'Main - _TC', + "tax_amount": 1000, + "description": "Test", + "add_deduct_tax": "Add" + + }) + pi.save() + pi.submit() + invoices.append(pi) + + # Second Invoice will apply TDS checked + pi1 = create_purchase_invoice(supplier = "Test TDS Supplier4", rate = 20000) + pi1.submit() + invoices.append(pi1) + + self.assertEqual(pi1.taxes[0].tax_amount, 4000) + + #delete invoices to avoid clashing + for d in invoices: + d.cancel() + def cancel_invoices(): purchase_invoices = frappe.get_all("Purchase Invoice", { 'supplier': ['in', ['Test TDS Supplier', 'Test TDS Supplier1', 'Test TDS Supplier2']], @@ -220,7 +250,7 @@ def create_sales_invoice(**args): def create_records(): # create a new suppliers - for name in ['Test TDS Supplier', 'Test TDS Supplier1', 'Test TDS Supplier2', 'Test TDS Supplier3']: + for name in ['Test TDS Supplier', 'Test TDS Supplier1', 'Test TDS Supplier2', 'Test TDS Supplier3', 'Test TDS Supplier4']: if frappe.db.exists('Supplier', name): continue From 60675379c2c5cb311bef4cf2374908511c6569a3 Mon Sep 17 00:00:00 2001 From: Saqib Date: Wed, 25 Aug 2021 18:26:00 +0530 Subject: [PATCH 60/65] fix: partial rename while replacing abbreviation (#27065) --- erpnext/setup/doctype/company/company.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index 45d5ce0c1c..6dee2ad92a 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -423,11 +423,11 @@ def replace_abbr(company, old, new): _rename_record(d) try: frappe.db.auto_commit_on_many_writes = 1 - frappe.db.set_value("Company", company, "abbr", new) for dt in ["Warehouse", "Account", "Cost Center", "Department", "Sales Taxes and Charges Template", "Purchase Taxes and Charges Template"]: _rename_records(dt) frappe.db.commit() + frappe.db.set_value("Company", company, "abbr", new) except Exception: frappe.log_error(title=_('Abbreviation Rename Error')) From 4d98be2126572a32d13ab76db5954d674d6845b2 Mon Sep 17 00:00:00 2001 From: Anupam Kumar Date: Wed, 25 Aug 2021 19:18:17 +0530 Subject: [PATCH 61/65] feat: provision to create customer from opportunity (#27141) * feat: provision to create customer from opportunity * fead: linking of address and contact * revert: create_opportunity_address_contact * enabming print hide and no copy --- .../crm/doctype/opportunity/opportunity.js | 19 +++++++++++++++++-- .../crm/doctype/opportunity/opportunity.py | 18 ++++++++++++++++++ .../selling/doctype/customer/customer.json | 11 ++++++++++- 3 files changed, 45 insertions(+), 3 deletions(-) diff --git a/erpnext/crm/doctype/opportunity/opportunity.js b/erpnext/crm/doctype/opportunity/opportunity.js index 632012b31d..cb95881cb4 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.js +++ b/erpnext/crm/doctype/opportunity/opportunity.js @@ -95,9 +95,17 @@ frappe.ui.form.on("Opportunity", { }, __('Create')); } - frm.add_custom_button(__('Quotation'), - cur_frm.cscript.create_quotation, __('Create')); + if (frm.doc.opportunity_from != "Customer") { + frm.add_custom_button(__('Customer'), + function() { + frm.trigger("make_customer") + }, __('Create')); + } + frm.add_custom_button(__('Quotation'), + function() { + frm.trigger("create_quotation") + }, __('Create')); } if(!frm.doc.__islocal && frm.perm[0].write && frm.doc.docstatus==0) { @@ -195,6 +203,13 @@ erpnext.crm.Opportunity = class Opportunity extends frappe.ui.form.Controller { frm: cur_frm }) } + + make_customer() { + frappe.model.open_mapped_doc({ + method: "erpnext.crm.doctype.opportunity.opportunity.make_customer", + frm: cur_frm + }) + } }; extend_cscript(cur_frm.cscript, new erpnext.crm.Opportunity({frm: cur_frm})); diff --git a/erpnext/crm/doctype/opportunity/opportunity.py b/erpnext/crm/doctype/opportunity/opportunity.py index 8ce482a3f9..a74a94afd6 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.py +++ b/erpnext/crm/doctype/opportunity/opportunity.py @@ -287,6 +287,24 @@ def make_request_for_quotation(source_name, target_doc=None): return doclist +@frappe.whitelist() +def make_customer(source_name, target_doc=None): + def set_missing_values(source, target): + if source.opportunity_from == "Lead": + target.lead_name = source.party_name + + doclist = get_mapped_doc("Opportunity", source_name, { + "Opportunity": { + "doctype": "Customer", + "field_map": { + "currency": "default_currency", + "customer_name": "customer_name" + } + } + }, target_doc, set_missing_values) + + return doclist + @frappe.whitelist() def make_supplier_quotation(source_name, target_doc=None): doclist = get_mapped_doc("Opportunity", source_name, { diff --git a/erpnext/selling/doctype/customer/customer.json b/erpnext/selling/doctype/customer/customer.json index cd94ee101a..0d839fc822 100644 --- a/erpnext/selling/doctype/customer/customer.json +++ b/erpnext/selling/doctype/customer/customer.json @@ -20,6 +20,7 @@ "tax_withholding_category", "default_bank_account", "lead_name", + "opportunity_name", "image", "column_break0", "account_manager", @@ -493,6 +494,14 @@ "fieldtype": "Link", "label": "Tax Withholding Category", "options": "Tax Withholding Category" + }, + { + "fieldname": "opportunity_name", + "fieldtype": "Link", + "label": "From Opportunity", + "no_copy": 1, + "options": "Opportunity", + "print_hide": 1 } ], "icon": "fa fa-user", @@ -500,7 +509,7 @@ "image_field": "image", "index_web_pages_for_search": 1, "links": [], - "modified": "2021-01-28 12:54:57.258959", + "modified": "2021-08-25 18:56:09.929905", "modified_by": "Administrator", "module": "Selling", "name": "Customer", From ad45ddcabe90c628344474ca5022fd67e27f43ad Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Wed, 25 Aug 2021 20:07:18 +0530 Subject: [PATCH 62/65] fix: sequence of sub-operations in job card (#27138) --- erpnext/manufacturing/doctype/job_card/job_card.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index 66e2394b84..3efbe88ada 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -26,17 +26,17 @@ class JobCard(Document): self.set_status() self.validate_operation_id() self.validate_sequence_id() - self.get_sub_operations() + self.set_sub_operations() self.update_sub_operation_status() - def get_sub_operations(self): + def set_sub_operations(self): if self.operation: self.sub_operations = [] - for row in frappe.get_all("Sub Operation", - filters = {"parent": self.operation}, fields=["operation", "idx"]): - row.status = "Pending" + for row in frappe.get_all('Sub Operation', + filters = {'parent': self.operation}, fields=['operation', 'idx'], order_by='idx'): + row.status = 'Pending' row.sub_operation = row.operation - self.append("sub_operations", row) + self.append('sub_operations', row) def validate_time_logs(self): self.total_time_in_mins = 0.0 @@ -690,7 +690,7 @@ def make_corrective_job_card(source_name, operation=None, for_operation=None, ta target.set('time_logs', []) target.set('employee', []) target.set('items', []) - target.get_sub_operations() + target.set_sub_operations() target.get_required_items() target.validate_time_logs() From 81b28b899843dcbf66d5c2cc241429e363ab04ac Mon Sep 17 00:00:00 2001 From: Chillar Anand Date: Wed, 25 Aug 2021 20:08:11 +0530 Subject: [PATCH 63/65] fix(healthcare): Removed ignore user permissions flag in appointment (#27129) --- .../healthcare_practitioner.json | 5 ++- .../patient_appointment.json | 1 - .../test_patient_appointment.py | 43 +++++++++++++++++-- 3 files changed, 44 insertions(+), 5 deletions(-) diff --git a/erpnext/healthcare/doctype/healthcare_practitioner/healthcare_practitioner.json b/erpnext/healthcare/doctype/healthcare_practitioner/healthcare_practitioner.json index 8162f03f6d..cb455eb501 100644 --- a/erpnext/healthcare/doctype/healthcare_practitioner/healthcare_practitioner.json +++ b/erpnext/healthcare/doctype/healthcare_practitioner/healthcare_practitioner.json @@ -282,7 +282,7 @@ ], "image_field": "image", "links": [], - "modified": "2021-01-22 10:14:43.187675", + "modified": "2021-08-24 10:42:08.513054", "modified_by": "Administrator", "module": "Healthcare", "name": "Healthcare Practitioner", @@ -295,6 +295,7 @@ "read": 1, "report": 1, "role": "Laboratory User", + "select": 1, "share": 1, "write": 1 }, @@ -307,6 +308,7 @@ "read": 1, "report": 1, "role": "Physician", + "select": 1, "share": 1, "write": 1 }, @@ -319,6 +321,7 @@ "read": 1, "report": 1, "role": "Nursing User", + "select": 1, "share": 1, "write": 1 } diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json index 6e996bd128..73ec3bc325 100644 --- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json +++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json @@ -142,7 +142,6 @@ { "fieldname": "practitioner", "fieldtype": "Link", - "ignore_user_permissions": 1, "in_standard_filter": 1, "label": "Healthcare Practitioner", "options": "Healthcare Practitioner", diff --git a/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py b/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py index 2df6921b14..9c3392cd5b 100644 --- a/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py +++ b/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py @@ -199,10 +199,33 @@ class TestPatientAppointment(unittest.TestCase): assert new_invoice_count == invoice_count + 1 + def test_patient_appointment_should_consider_permissions_while_fetching_appointments(self): + patient, medical_department, practitioner = create_healthcare_docs() + create_appointment(patient, practitioner, nowdate()) + + patient, medical_department, new_practitioner = create_healthcare_docs(practitioner_name='Dr. John') + create_appointment(patient, new_practitioner, nowdate()) + + roles = [{"doctype": "Has Role", "role": "Physician"}] + user = create_user(roles=roles) + new_practitioner = frappe.get_doc('Healthcare Practitioner', new_practitioner) + new_practitioner.user_id = user.email + new_practitioner.save() + + frappe.set_user(user.name) + appointments = frappe.get_list('Patient Appointment') + assert len(appointments) == 1 + + frappe.set_user("Administrator") + appointments = frappe.get_list('Patient Appointment') + assert len(appointments) == 2 + +def create_healthcare_docs(practitioner_name=None): + if not practitioner_name: + practitioner_name = '_Test Healthcare Practitioner' -def create_healthcare_docs(): patient = create_patient() - practitioner = frappe.db.exists('Healthcare Practitioner', '_Test Healthcare Practitioner') + practitioner = frappe.db.exists('Healthcare Practitioner', practitioner_name) medical_department = frappe.db.exists('Medical Department', '_Test Medical Department') if not medical_department: @@ -213,7 +236,7 @@ def create_healthcare_docs(): if not practitioner: practitioner = frappe.new_doc('Healthcare Practitioner') - practitioner.first_name = '_Test Healthcare Practitioner' + practitioner.first_name = practitioner_name practitioner.gender = 'Female' practitioner.department = medical_department practitioner.op_consulting_charge = 500 @@ -319,3 +342,17 @@ def create_appointment_type(args=None): 'price_list': args.get('price_list') or frappe.db.get_value("Price List", {"selling": 1}), 'items': args.get('items') or items }).insert() + +def create_user(email=None, roles=None): + if not email: + email = '{}@frappe.com'.format(frappe.utils.random_string(10)) + user = frappe.db.exists('User', email) + if not user: + user = frappe.get_doc({ + "doctype": "User", + "email": email, + "first_name": "test_user", + "password": "password", + "roles": roles, + }).insert() + return user From 87308465d8b361485b6d63a2fc11283eba0c2b00 Mon Sep 17 00:00:00 2001 From: Rohan Date: Wed, 25 Aug 2021 20:11:05 +0530 Subject: [PATCH 64/65] fix: create zero-dollar invoices fro trial subscriptions (#26441) --- .../doctype/subscription/subscription.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/doctype/subscription/subscription.py b/erpnext/accounts/doctype/subscription/subscription.py index 7c4ff73d90..8bf7b78f58 100644 --- a/erpnext/accounts/doctype/subscription/subscription.py +++ b/erpnext/accounts/doctype/subscription/subscription.py @@ -367,21 +367,25 @@ class Subscription(Document): ) # Discounts - if self.additional_discount_percentage: - invoice.additional_discount_percentage = self.additional_discount_percentage + if self.is_trialling(): + invoice.additional_discount_percentage = 100 + else: + if self.additional_discount_percentage: + invoice.additional_discount_percentage = self.additional_discount_percentage - if self.additional_discount_amount: - invoice.discount_amount = self.additional_discount_amount + if self.additional_discount_amount: + invoice.discount_amount = self.additional_discount_amount - if self.additional_discount_percentage or self.additional_discount_amount: - discount_on = self.apply_additional_discount - invoice.apply_discount_on = discount_on if discount_on else 'Grand Total' + if self.additional_discount_percentage or self.additional_discount_amount: + discount_on = self.apply_additional_discount + invoice.apply_discount_on = discount_on if discount_on else 'Grand Total' # Subscription period invoice.from_date = self.current_invoice_start invoice.to_date = self.current_invoice_end invoice.flags.ignore_mandatory = True + invoice.save() if self.submit_invoice: From 842ceb1301c60f4442313c3c266d4fc194f5172c Mon Sep 17 00:00:00 2001 From: Ganga Manoj Date: Wed, 25 Aug 2021 20:32:26 +0530 Subject: [PATCH 65/65] fix: cannot reconcile bank transactions against internal transfer payment entries (#26932) * fix: Only set Clearance Date for Payment Entries of type Internal Transfer if both Transactions have been reconciled * fix: Reset clearance_date for intra-company Payment Entries that have only been reconciled with one Bank Transaction * fix: indentation and args Co-authored-by: Saqib Co-authored-by: Nabin Hait --- .../bank_transaction/bank_transaction.py | 16 +++++++ erpnext/patches.txt | 1 + ...e_date_for_intracompany_payment_entries.py | 45 +++++++++++++++++++ 3 files changed, 62 insertions(+) create mode 100644 erpnext/patches/v13_0/reset_clearance_date_for_intracompany_payment_entries.py diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py index 0544a469d6..7ea71fc103 100644 --- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py +++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py @@ -55,6 +55,11 @@ class BankTransaction(StatusUpdater): self.clear_sales_invoice(payment_entry, for_cancel=for_cancel) def clear_simple_entry(self, payment_entry, for_cancel=False): + if payment_entry.payment_document == "Payment Entry": + if frappe.db.get_value("Payment Entry", payment_entry.payment_entry, "payment_type") == "Internal Transfer": + if len(get_reconciled_bank_transactions(payment_entry)) < 2: + return + clearance_date = self.date if not for_cancel else None frappe.db.set_value( payment_entry.payment_document, payment_entry.payment_entry, @@ -70,6 +75,17 @@ class BankTransaction(StatusUpdater): ), "clearance_date", clearance_date) +def get_reconciled_bank_transactions(payment_entry): + reconciled_bank_transactions = frappe.get_all( + 'Bank Transaction Payments', + filters = { + 'payment_entry': payment_entry.payment_entry + }, + fields = ['parent'] + ) + + return reconciled_bank_transactions + def get_total_allocated_amount(payment_entry): return frappe.db.sql(""" SELECT diff --git a/erpnext/patches.txt b/erpnext/patches.txt index bf0446bb68..0a6a8bdbdc 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -295,5 +295,6 @@ erpnext.patches.v13_0.update_tds_check_field #3 erpnext.patches.v13_0.add_custom_field_for_south_africa #2 erpnext.patches.v13_0.update_recipient_email_digest erpnext.patches.v13_0.shopify_deprecation_warning +erpnext.patches.v13_0.reset_clearance_date_for_intracompany_payment_entries erpnext.patches.v13_0.einvoicing_deprecation_warning erpnext.patches.v14_0.delete_einvoicing_doctypes diff --git a/erpnext/patches/v13_0/reset_clearance_date_for_intracompany_payment_entries.py b/erpnext/patches/v13_0/reset_clearance_date_for_intracompany_payment_entries.py new file mode 100644 index 0000000000..1da5275761 --- /dev/null +++ b/erpnext/patches/v13_0/reset_clearance_date_for_intracompany_payment_entries.py @@ -0,0 +1,45 @@ +# Copyright (c) 2019, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals + +import frappe + +def execute(): + """ + Reset Clearance Date for Payment Entries of type Internal Transfer that have only been reconciled with one Bank Transaction. + This will allow the Payment Entries to be reconciled with the second Bank Transaction using the Bank Reconciliation Tool. + """ + + intra_company_pe = get_intra_company_payment_entries_with_clearance_dates() + reconciled_bank_transactions = get_reconciled_bank_transactions(intra_company_pe) + + for payment_entry in reconciled_bank_transactions: + if len(reconciled_bank_transactions[payment_entry]) == 1: + frappe.db.set_value('Payment Entry', payment_entry, 'clearance_date', None) + +def get_intra_company_payment_entries_with_clearance_dates(): + return frappe.get_all( + 'Payment Entry', + filters = { + 'payment_type': 'Internal Transfer', + 'clearance_date': ["not in", None] + }, + pluck = 'name' + ) + +def get_reconciled_bank_transactions(intra_company_pe): + """Returns dictionary where each key:value pair is Payment Entry : List of Bank Transactions reconciled with Payment Entry""" + + reconciled_bank_transactions = {} + + for payment_entry in intra_company_pe: + reconciled_bank_transactions[payment_entry] = frappe.get_all( + 'Bank Transaction Payments', + filters = { + 'payment_entry': payment_entry + }, + pluck='parent' + ) + + return reconciled_bank_transactions \ No newline at end of file