diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index 3f7d4bb741..78c2f5a187 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -4,7 +4,7 @@ on: [pull_request, workflow_dispatch, push] jobs: test: - runs-on: ubuntu-latest + runs-on: ubuntu-18.04 strategy: fail-fast: false diff --git a/erpnext/accounts/doctype/account/account.py b/erpnext/accounts/doctype/account/account.py index c801cfcbba..0606823821 100644 --- a/erpnext/accounts/doctype/account/account.py +++ b/erpnext/accounts/doctype/account/account.py @@ -214,6 +214,7 @@ class Account(NestedSet): if parent_value_changed: doc.save() + @frappe.whitelist() def convert_group_to_ledger(self): if self.check_if_child_exists(): throw(_("Account with child nodes cannot be converted to ledger")) @@ -224,6 +225,7 @@ class Account(NestedSet): self.save() return 1 + @frappe.whitelist() def convert_ledger_to_group(self): if self.check_gle_exists(): throw(_("Account with existing transaction can not be converted to group.")) diff --git a/erpnext/accounts/doctype/accounting_period/accounting_period.py b/erpnext/accounts/doctype/accounting_period/accounting_period.py index df6cedd7cf..63b5dbbd3e 100644 --- a/erpnext/accounts/doctype/accounting_period/accounting_period.py +++ b/erpnext/accounts/doctype/accounting_period/accounting_period.py @@ -39,6 +39,7 @@ class AccountingPeriod(Document): frappe.throw(_("Accounting Period overlaps with {0}") .format(existing_accounting_period[0].get("name")), OverlapError) + @frappe.whitelist() def get_doctypes_for_closing(self): docs_for_closing = [] doctypes = ["Sales Invoice", "Purchase Invoice", "Journal Entry", "Payroll Entry", \ diff --git a/erpnext/accounts/doctype/accounting_period/test_accounting_period.py b/erpnext/accounts/doctype/accounting_period/test_accounting_period.py index 022d7a7e80..10cd939894 100644 --- a/erpnext/accounts/doctype/accounting_period/test_accounting_period.py +++ b/erpnext/accounts/doctype/accounting_period/test_accounting_period.py @@ -11,36 +11,36 @@ from erpnext.accounts.doctype.accounting_period.accounting_period import Overlap from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice class TestAccountingPeriod(unittest.TestCase): - def test_overlap(self): - ap1 = create_accounting_period(start_date = "2018-04-01", - end_date = "2018-06-30", company = "Wind Power LLC") - ap1.save() + def test_overlap(self): + ap1 = create_accounting_period(start_date = "2018-04-01", + end_date = "2018-06-30", company = "Wind Power LLC") + ap1.save() - ap2 = create_accounting_period(start_date = "2018-06-30", - end_date = "2018-07-10", company = "Wind Power LLC", period_name = "Test Accounting Period 1") - self.assertRaises(OverlapError, ap2.save) + ap2 = create_accounting_period(start_date = "2018-06-30", + end_date = "2018-07-10", company = "Wind Power LLC", period_name = "Test Accounting Period 1") + self.assertRaises(OverlapError, ap2.save) - def test_accounting_period(self): - ap1 = create_accounting_period(period_name = "Test Accounting Period 2") - ap1.save() + def test_accounting_period(self): + ap1 = create_accounting_period(period_name = "Test Accounting Period 2") + ap1.save() - doc = create_sales_invoice(do_not_submit=1, cost_center = "_Test Company - _TC", warehouse = "Stores - _TC") - self.assertRaises(ClosedAccountingPeriod, doc.submit) + doc = create_sales_invoice(do_not_submit=1, cost_center="_Test Company - _TC", warehouse="Stores - _TC") + self.assertRaises(ClosedAccountingPeriod, doc.submit) - def tearDown(self): - for d in frappe.get_all("Accounting Period"): - frappe.delete_doc("Accounting Period", d.name) + def tearDown(self): + for d in frappe.get_all("Accounting Period"): + frappe.delete_doc("Accounting Period", d.name) def create_accounting_period(**args): - args = frappe._dict(args) + args = frappe._dict(args) - accounting_period = frappe.new_doc("Accounting Period") - accounting_period.start_date = args.start_date or nowdate() - accounting_period.end_date = args.end_date or add_months(nowdate(), 1) - accounting_period.company = args.company or "_Test Company" - accounting_period.period_name =args.period_name or "_Test_Period_Name_1" - accounting_period.append("closed_documents", { - "document_type": 'Sales Invoice', "closed": 1 - }) + accounting_period = frappe.new_doc("Accounting Period") + accounting_period.start_date = args.start_date or nowdate() + accounting_period.end_date = args.end_date or add_months(nowdate(), 1) + accounting_period.company = args.company or "_Test Company" + accounting_period.period_name =args.period_name or "_Test_Period_Name_1" + accounting_period.append("closed_documents", { + "document_type": 'Sales Invoice', "closed": 1 + }) - return accounting_period \ No newline at end of file + return accounting_period diff --git a/erpnext/accounts/doctype/bank_clearance/bank_clearance.py b/erpnext/accounts/doctype/bank_clearance/bank_clearance.py index 76d82e7339..79f5596384 100644 --- a/erpnext/accounts/doctype/bank_clearance/bank_clearance.py +++ b/erpnext/accounts/doctype/bank_clearance/bank_clearance.py @@ -12,6 +12,7 @@ form_grid_templates = { } class BankClearance(Document): + @frappe.whitelist() def get_payment_entries(self): if not (self.from_date and self.to_date): frappe.throw(_("From Date and To Date are Mandatory")) @@ -108,6 +109,7 @@ class BankClearance(Document): row.update(d) self.total_amount += flt(amount) + @frappe.whitelist() def update_clearance_date(self): clearance_date_updated = False for d in self.get('payment_entries'): diff --git a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.js b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.js index ad4ff9ee60..3dbd605344 100644 --- a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.js +++ b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.js @@ -532,43 +532,4 @@ frappe.ui.form.on("Bank Statement Import", { `); }, - - show_missing_link_values(frm, missing_link_values) { - let can_be_created_automatically = missing_link_values.every( - (d) => d.has_one_mandatory_field - ); - - let html = missing_link_values - .map((d) => { - let doctype = d.doctype; - let values = d.missing_values; - return ` -
${doctype}
- - `; - }) - .join(""); - - if (can_be_created_automatically) { - // prettier-ignore - let message = __('There are some linked records which needs to be created before we can import your file. Do you want to create the following missing records automatically?'); - frappe.confirm(message + html, () => { - frm.call("create_missing_link_values", { - missing_link_values, - }).then((r) => { - let records = r.message; - frappe.msgprint(__( - "Created {0} records successfully.", [ - records.length, - ] - )); - }); - }); - } else { - frappe.msgprint( - // prettier-ignore - __('The following records needs to be created before we can import your file.') + html - ); - } - }, }); diff --git a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py index 3b14e4efa0..17e39d562a 100644 --- a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py +++ b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py @@ -15,12 +15,14 @@ from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profi test_dependencies = ["Item", "Cost Center"] class TestBankTransaction(unittest.TestCase): - def setUp(self): + @classmethod + def setUpClass(cls): make_pos_profile() add_transactions() add_vouchers() - def tearDown(self): + @classmethod + def tearDownClass(cls): for bt in frappe.get_all("Bank Transaction"): doc = frappe.get_doc("Bank Transaction", bt.name) doc.cancel() @@ -33,9 +35,6 @@ class TestBankTransaction(unittest.TestCase): # Delete POS Profile frappe.db.sql("delete from `tabPOS Profile`") - frappe.flags.test_bank_transactions_created = False - frappe.flags.test_payments_created = False - # This test checks if ERPNext is able to provide a linked payment for a bank transaction based on the amount of the bank transaction. def test_linked_payments(self): bank_transaction = frappe.get_doc("Bank Transaction", dict(description="Re 95282925234 FE/000002917 AT171513000281183046 Conrad Electronic")) @@ -44,8 +43,8 @@ class TestBankTransaction(unittest.TestCase): # This test validates a simple reconciliation leading to the clearance of the bank transaction and the payment def test_reconcile(self): - bank_transaction = frappe.get_doc("Bank Transaction", dict(description="1512567 BG/000002918 OPSKATTUZWXXX AT776000000098709837 Herr G")) - payment = frappe.get_doc("Payment Entry", dict(party="Mr G", paid_amount=1200)) + bank_transaction = frappe.get_doc("Bank Transaction", dict(description="1512567 BG/000003025 OPSKATTUZWXXX AT776000000098709849 Herr G")) + payment = frappe.get_doc("Payment Entry", dict(party="Mr G", paid_amount=1700)) vouchers = json.dumps([{ "payment_doctype":"Payment Entry", "payment_name":payment.name, @@ -116,10 +115,6 @@ def create_bank_account(bank_name="Citi Bank", account_name="_Test Bank - _TC"): pass def add_transactions(): - if frappe.flags.test_bank_transactions_created: - return - - frappe.set_user("Administrator") create_bank_account() doc = frappe.get_doc({ @@ -172,14 +167,8 @@ def add_transactions(): }).insert() doc.submit() - frappe.flags.test_bank_transactions_created = True def add_vouchers(): - if frappe.flags.test_payments_created: - return - - frappe.set_user("Administrator") - try: frappe.get_doc({ "doctype": "Supplier", @@ -272,13 +261,6 @@ def add_vouchers(): except frappe.DuplicateEntryError: pass - si = create_sales_invoice(customer="Fayva", qty=1, rate=109080) - pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank - _TC") - pe.reference_no = "Fayva Oct 18" - pe.reference_date = "2018-10-29" - pe.insert() - pe.submit() - mode_of_payment = frappe.get_doc({ "doctype": "Mode of Payment", "name": "Cash" @@ -291,14 +273,12 @@ def add_vouchers(): }) mode_of_payment.save() - si = create_sales_invoice(customer="Fayva", qty=1, rate=109080, do_not_submit=1) + si = create_sales_invoice(customer="Fayva", qty=1, rate=109080, do_not_save=1) si.is_pos = 1 si.append("payments", { "mode_of_payment": "Cash", "account": "_Test Bank - _TC", "amount": 109080 }) - si.save() + si.insert() si.submit() - - frappe.flags.test_payments_created = True diff --git a/erpnext/accounts/doctype/c_form/c_form.py b/erpnext/accounts/doctype/c_form/c_form.py index 9b64f8100f..fd86ed4c90 100644 --- a/erpnext/accounts/doctype/c_form/c_form.py +++ b/erpnext/accounts/doctype/c_form/c_form.py @@ -57,6 +57,7 @@ class CForm(Document): total = sum([flt(d.grand_total) for d in self.get('invoices')]) frappe.db.set(self, 'total_invoiced_amount', total) + @frappe.whitelist() def get_invoice_details(self, invoice_no): """ Pull details from invoices for referrence """ if invoice_no: diff --git a/erpnext/accounts/doctype/cost_center/cost_center.py b/erpnext/accounts/doctype/cost_center/cost_center.py index 12094d4f98..8a5473f3a1 100644 --- a/erpnext/accounts/doctype/cost_center/cost_center.py +++ b/erpnext/accounts/doctype/cost_center/cost_center.py @@ -50,6 +50,7 @@ class CostCenter(NestedSet): frappe.throw(_("{0} is not a group node. Please select a group node as parent cost center").format( frappe.bold(self.parent_cost_center))) + @frappe.whitelist() def convert_group_to_ledger(self): if self.check_if_child_exists(): frappe.throw(_("Cannot convert Cost Center to ledger as it has child nodes")) @@ -60,6 +61,7 @@ class CostCenter(NestedSet): self.save() return 1 + @frappe.whitelist() def convert_ledger_to_group(self): if cint(self.enable_distributed_cost_center): frappe.throw(_("Cost Center with enabled distributed cost center can not be converted to group")) diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py index 9594706d0f..c1b8ba70ba 100644 --- a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py +++ b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py @@ -27,6 +27,7 @@ class ExchangeRateRevaluation(Document): if not (self.company and self.posting_date): frappe.throw(_("Please select Company and Posting Date to getting entries")) + @frappe.whitelist() def get_accounts_data(self, account=None): accounts = [] self.validate_mandatory() @@ -95,6 +96,7 @@ class ExchangeRateRevaluation(Document): message = _("No outstanding invoices found") frappe.msgprint(message) + @frappe.whitelist() def make_jv_entry(self): if self.total_gain_loss == 0: return diff --git a/erpnext/accounts/doctype/fiscal_year/fiscal_year.py b/erpnext/accounts/doctype/fiscal_year/fiscal_year.py index da6a3fd2ef..42556269fd 100644 --- a/erpnext/accounts/doctype/fiscal_year/fiscal_year.py +++ b/erpnext/accounts/doctype/fiscal_year/fiscal_year.py @@ -12,6 +12,7 @@ from frappe.model.document import Document class FiscalYearIncorrectDate(frappe.ValidationError): pass class FiscalYear(Document): + @frappe.whitelist() def set_as_default(self): frappe.db.set_value("Global Defaults", None, "current_fiscal_year", self.name) global_defaults = frappe.get_doc("Global Defaults") @@ -54,7 +55,7 @@ class FiscalYear(Document): def on_update(self): check_duplicate_fiscal_year(self) frappe.cache().delete_value("fiscal_years") - + def on_trash(self): global_defaults = frappe.get_doc("Global Defaults") if global_defaults.current_fiscal_year == self.name: diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py index ce76d0a39c..78febf9c2e 100644 --- a/erpnext/accounts/doctype/gl_entry/gl_entry.py +++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py @@ -290,4 +290,8 @@ def rename_temporarily_named_docs(doctype): oldname = doc.name set_name_from_naming_options(frappe.get_meta(doctype).autoname, doc) newname = doc.name - frappe.db.sql("""UPDATE `tab{}` SET name = %s, to_rename = 0 where name = %s""".format(doctype), (newname, oldname)) + frappe.db.sql( + "UPDATE `tab{}` SET name = %s, to_rename = 0 where name = %s".format(doctype), + (newname, oldname), + auto_commit=True + ) diff --git a/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.py b/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.py index af8940cde5..7b62b617f9 100644 --- a/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.py +++ b/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.py @@ -125,6 +125,7 @@ class InvoiceDiscounting(AccountsController): make_gl_entries(gl_entries, cancel=(self.docstatus == 2), update_outstanding='No') + @frappe.whitelist() def create_disbursement_entry(self): je = frappe.new_doc("Journal Entry") je.voucher_type = 'Journal Entry' @@ -174,6 +175,7 @@ class InvoiceDiscounting(AccountsController): return je + @frappe.whitelist() def close_loan(self): je = frappe.new_doc("Journal Entry") je.voucher_type = 'Journal Entry' diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index 3419bb6c3e..ff2c8c29b4 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -564,6 +564,7 @@ class JournalEntry(AccountsController): if gl_map: make_gl_entries(gl_map, cancel=cancel, adv_adj=adv_adj, update_outstanding=update_outstanding) + @frappe.whitelist() def get_balance(self): if not self.get('accounts'): msgprint(_("'Entries' cannot be empty"), raise_exception=True) diff --git a/erpnext/accounts/doctype/monthly_distribution/monthly_distribution.py b/erpnext/accounts/doctype/monthly_distribution/monthly_distribution.py index 18f853cadc..88667d7207 100644 --- a/erpnext/accounts/doctype/monthly_distribution/monthly_distribution.py +++ b/erpnext/accounts/doctype/monthly_distribution/monthly_distribution.py @@ -8,6 +8,7 @@ from frappe.utils import (flt, add_months) from frappe.model.document import Document class MonthlyDistribution(Document): + @frappe.whitelist() def get_months(self): month_list = ['January','February','March','April','May','June','July','August','September', 'October','November','December'] diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py index e6449b7831..29dc96e8c6 100644 --- a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py +++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py @@ -167,6 +167,7 @@ class OpeningInvoiceCreationTool(Document): return invoice + @frappe.whitelist() def make_invoices(self): self.validate_company() invoices = self.get_invoices() diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py b/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py index bdfe532b9f..8d6de2d562 100644 --- a/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py +++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py @@ -6,10 +6,12 @@ from __future__ import unicode_literals import frappe import unittest -test_dependencies = ["Customer", "Supplier"] +from frappe.cache_manager import clear_doctype_cache from frappe.custom.doctype.property_setter.property_setter import make_property_setter from erpnext.accounts.doctype.opening_invoice_creation_tool.opening_invoice_creation_tool import get_temporary_opening_account +test_dependencies = ["Customer", "Supplier"] + class TestOpeningInvoiceCreationTool(unittest.TestCase): def setUp(self): if not frappe.db.exists("Company", "_Test Opening Invoice Company"): @@ -24,22 +26,25 @@ class TestOpeningInvoiceCreationTool(unittest.TestCase): def test_opening_sales_invoice_creation(self): property_setter = make_property_setter("Sales Invoice", "update_stock", "default", 1, "Check") - invoices = self.make_invoices(company="_Test Opening Invoice Company") + try: + invoices = self.make_invoices(company="_Test Opening Invoice Company") - self.assertEqual(len(invoices), 2) - expected_value = { - "keys": ["customer", "outstanding_amount", "status"], - 0: ["_Test Customer", 300, "Overdue"], - 1: ["_Test Customer 1", 250, "Overdue"], - } - self.check_expected_values(invoices, expected_value) + self.assertEqual(len(invoices), 2) + expected_value = { + "keys": ["customer", "outstanding_amount", "status"], + 0: ["_Test Customer", 300, "Overdue"], + 1: ["_Test Customer 1", 250, "Overdue"], + } + self.check_expected_values(invoices, expected_value) - si = frappe.get_doc("Sales Invoice", invoices[0]) + si = frappe.get_doc("Sales Invoice", invoices[0]) - # Check if update stock is not enabled - self.assertEqual(si.update_stock, 0) + # Check if update stock is not enabled + self.assertEqual(si.update_stock, 0) - property_setter.delete() + finally: + property_setter.delete() + clear_doctype_cache("Sales Invoice") def check_expected_values(self, invoices, expected_value, invoice_type="Sales"): doctype = "Sales Invoice" if invoice_type == "Sales" else "Purchase Invoice" @@ -143,4 +148,4 @@ def make_customer(customer=None): customer.insert(ignore_permissions=True) return customer.name else: - return frappe.db.exists("Customer", customer_name) \ No newline at end of file + return frappe.db.exists("Customer", customer_name) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index 6412772073..c2e804e441 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -605,12 +605,22 @@ frappe.ui.form.on('Payment Entry', { {fieldtype:"Column Break"}, {fieldtype:"Float", label: __("Less Than Amount"), fieldname:"outstanding_amt_less_than"}, {fieldtype:"Section Break"}, + {fieldtype:"Link", label:__("Cost Center"), fieldname:"cost_center", options:"Cost Center", + "get_query": function() { + return { + "filters": {"company": frm.doc.company} + } + } + }, + {fieldtype:"Column Break"}, + {fieldtype:"Section Break"}, {fieldtype:"Check", label: __("Allocate Payment Amount"), fieldname:"allocate_payment_amount", default:1}, ]; frappe.prompt(fields, function(filters){ frappe.flags.allocate_payment_amount = true; frm.events.validate_filters_data(frm, filters); + frm.doc.cost_center = filters.cost_center; frm.events.get_outstanding_documents(frm, filters); }, __("Filters"), __("Get Outstanding Documents")); }, @@ -627,13 +637,13 @@ frappe.ui.form.on('Payment Entry', { let to_field = fields[key][1]; if (filters[from_field] && !filters[to_field]) { - frappe.throw(__("Error: {0} is mandatory field", - [to_field.replace(/_/g, " ")] - )); + frappe.throw( + __("Error: {0} is mandatory field", [to_field.replace(/_/g, " ")]) + ); } else if (filters[from_field] && filters[from_field] > filters[to_field]) { - frappe.throw(__("{0}: {1} must be less than {2}", - [key, from_field.replace(/_/g, " "), to_field.replace(/_/g, " ")] - )); + frappe.throw( + __("{0}: {1} must be less than {2}", [key, from_field.replace(/_/g, " "), to_field.replace(/_/g, " ")]) + ); } } }, @@ -682,6 +692,8 @@ frappe.ui.form.on('Payment Entry', { c.total_amount = d.invoice_amount; c.outstanding_amount = d.outstanding_amount; c.bill_no = d.bill_no; + c.payment_term = d.payment_term; + c.allocated_amount = d.allocated_amount; if(!in_list(["Sales Order", "Purchase Order", "Expense Claim", "Fees"], d.voucher_type)) { if(flt(d.outstanding_amount) > 0) @@ -764,12 +776,15 @@ frappe.ui.form.on('Payment Entry', { } else if (in_list(["Customer", "Supplier"], frm.doc.party_type)) { if(paid_amount > total_negative_outstanding) { if(total_negative_outstanding == 0) { - frappe.msgprint(__("Cannot {0} {1} {2} without any negative outstanding invoice", - [frm.doc.payment_type, - (frm.doc.party_type=="Customer" ? "to" : "from"), frm.doc.party_type])); + frappe.msgprint( + __("Cannot {0} {1} {2} without any negative outstanding invoice", [frm.doc.payment_type, + (frm.doc.party_type=="Customer" ? "to" : "from"), frm.doc.party_type]) + ); return false } else { - frappe.msgprint(__("Paid Amount cannot be greater than total negative outstanding amount {0}", [total_negative_outstanding])); + frappe.msgprint( + __("Paid Amount cannot be greater than total negative outstanding amount {0}", [total_negative_outstanding]) + ); return false; } } else { @@ -781,10 +796,13 @@ frappe.ui.form.on('Payment Entry', { } $.each(frm.doc.references || [], function(i, row) { - row.allocated_amount = 0 //If allocate payment amount checkbox is unchecked, set zero to allocate amount - if(frappe.flags.allocate_payment_amount != 0){ - if(row.outstanding_amount > 0 && allocated_positive_outstanding > 0) { - if(row.outstanding_amount >= allocated_positive_outstanding) { + if (frappe.flags.allocate_payment_amount == 0) { + //If allocate payment amount checkbox is unchecked, set zero to allocate amount + row.allocated_amount = 0; + + } else if (frappe.flags.allocate_payment_amount != 0 && !row.allocated_amount) { + if (row.outstanding_amount > 0 && allocated_positive_outstanding > 0) { + if (row.outstanding_amount >= allocated_positive_outstanding) { row.allocated_amount = allocated_positive_outstanding; } else { row.allocated_amount = row.outstanding_amount; @@ -792,9 +810,11 @@ frappe.ui.form.on('Payment Entry', { allocated_positive_outstanding -= flt(row.allocated_amount); } else if (row.outstanding_amount < 0 && allocated_negative_outstanding) { - if(Math.abs(row.outstanding_amount) >= allocated_negative_outstanding) + if (Math.abs(row.outstanding_amount) >= allocated_negative_outstanding) { row.allocated_amount = -1*allocated_negative_outstanding; - else row.allocated_amount = row.outstanding_amount; + } else { + row.allocated_amount = row.outstanding_amount; + }; allocated_negative_outstanding -= Math.abs(flt(row.allocated_amount)); } @@ -1066,11 +1086,6 @@ frappe.ui.form.on('Payment Entry', { frm.set_value("paid_from_account_balance", r.message.paid_from_account_balance); frm.set_value("paid_to_account_balance", r.message.paid_to_account_balance); frm.set_value("party_balance", r.message.party_balance); - }, - () => { - if(frm.doc.payment_type != "Internal") { - frm.clear_table("references"); - } } ]); diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 8acd92cb6b..62ab76c323 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -333,33 +333,50 @@ class PaymentEntry(AccountsController): invoice_payment_amount_map = {} invoice_paid_amount_map = {} - for reference in self.get('references'): - if reference.payment_term and reference.reference_name: - key = (reference.payment_term, reference.reference_name) + for ref in self.get('references'): + if ref.payment_term and ref.reference_name: + key = (ref.payment_term, ref.reference_name) invoice_payment_amount_map.setdefault(key, 0.0) - invoice_payment_amount_map[key] += reference.allocated_amount + invoice_payment_amount_map[key] += ref.allocated_amount if not invoice_paid_amount_map.get(key): - payment_schedule = frappe.get_all('Payment Schedule', filters={'parent': reference.reference_name}, - fields=['paid_amount', 'payment_amount', 'payment_term']) + payment_schedule = frappe.get_all( + 'Payment Schedule', + filters={'parent': ref.reference_name}, + fields=['paid_amount', 'payment_amount', 'payment_term', 'discount', 'outstanding'] + ) for term in payment_schedule: - invoice_key = (term.payment_term, reference.reference_name) + invoice_key = (term.payment_term, ref.reference_name) invoice_paid_amount_map.setdefault(invoice_key, {}) - invoice_paid_amount_map[invoice_key]['outstanding'] = term.payment_amount - term.paid_amount + invoice_paid_amount_map[invoice_key]['outstanding'] = term.outstanding + invoice_paid_amount_map[invoice_key]['discounted_amt'] = ref.total_amount * (term.discount / 100) + + for key, allocated_amount in iteritems(invoice_payment_amount_map): + outstanding = flt(invoice_paid_amount_map.get(key, {}).get('outstanding')) + discounted_amt = flt(invoice_paid_amount_map.get(key, {}).get('discounted_amt')) - for key, amount in iteritems(invoice_payment_amount_map): if cancel: - frappe.db.sql(""" UPDATE `tabPayment Schedule` SET paid_amount = `paid_amount` - %s - WHERE parent = %s and payment_term = %s""", (amount, key[1], key[0])) + frappe.db.sql(""" + UPDATE `tabPayment Schedule` + SET + paid_amount = `paid_amount` - %s, + discounted_amount = `discounted_amount` - %s, + outstanding = `outstanding` + %s + WHERE parent = %s and payment_term = %s""", + (allocated_amount - discounted_amt, discounted_amt, allocated_amount, key[1], key[0])) else: - outstanding = flt(invoice_paid_amount_map.get(key, {}).get('outstanding')) - - if amount > outstanding: + if allocated_amount > outstanding: frappe.throw(_('Cannot allocate more than {0} against payment term {1}').format(outstanding, key[0])) - if amount and outstanding: - frappe.db.sql(""" UPDATE `tabPayment Schedule` SET paid_amount = `paid_amount` + %s - WHERE parent = %s and payment_term = %s""", (amount, key[1], key[0])) + if allocated_amount and outstanding: + frappe.db.sql(""" + UPDATE `tabPayment Schedule` + SET + paid_amount = `paid_amount` + %s, + discounted_amount = `discounted_amount` + %s, + outstanding = `outstanding` - %s + WHERE parent = %s and payment_term = %s""", + (allocated_amount - discounted_amt, discounted_amt, allocated_amount, key[1], key[0])) def set_status(self): if self.docstatus == 2: @@ -708,6 +725,8 @@ def get_outstanding_reference_documents(args): outstanding_invoices = get_outstanding_invoices(args.get("party_type"), args.get("party"), args.get("party_account"), filters=args, condition=condition) + outstanding_invoices = split_invoices_based_on_payment_terms(outstanding_invoices) + for d in outstanding_invoices: d["exchange_rate"] = 1 if party_account_currency != company_currency: @@ -735,6 +754,46 @@ def get_outstanding_reference_documents(args): return data +def split_invoices_based_on_payment_terms(outstanding_invoices): + invoice_ref_based_on_payment_terms = {} + for idx, d in enumerate(outstanding_invoices): + if d.voucher_type in ['Sales Invoice', 'Purchase Invoice']: + payment_term_template = frappe.db.get_value(d.voucher_type, d.voucher_no, 'payment_terms_template') + if payment_term_template: + allocate_payment_based_on_payment_terms = frappe.db.get_value( + 'Payment Terms Template', payment_term_template, 'allocate_payment_based_on_payment_terms') + if allocate_payment_based_on_payment_terms: + payment_schedule = frappe.get_all('Payment Schedule', filters={'parent': d.voucher_no}, fields=["*"]) + + for payment_term in payment_schedule: + if payment_term.outstanding > 0.1: + invoice_ref_based_on_payment_terms.setdefault(idx, []) + invoice_ref_based_on_payment_terms[idx].append(frappe._dict({ + 'due_date': d.due_date, + 'currency': d.currency, + 'voucher_no': d.voucher_no, + 'voucher_type': d.voucher_type, + 'posting_date': d.posting_date, + 'invoice_amount': flt(d.invoice_amount), + 'outstanding_amount': flt(d.outstanding_amount), + 'payment_amount': payment_term.payment_amount, + 'payment_term': payment_term.payment_term, + 'allocated_amount': payment_term.outstanding + })) + + if invoice_ref_based_on_payment_terms: + for idx, ref in invoice_ref_based_on_payment_terms.items(): + voucher_no = outstanding_invoices[idx]['voucher_no'] + voucher_type = outstanding_invoices[idx]['voucher_type'] + + frappe.msgprint(_("Spliting {} {} into {} rows as per payment terms").format( + voucher_type, voucher_no, len(ref)), alert=True) + + outstanding_invoices.pop(idx - 1) + outstanding_invoices += invoice_ref_based_on_payment_terms[idx] + + return outstanding_invoices + def get_orders_to_be_billed(posting_date, party_type, party, company, party_account_currency, company_currency, cost_center=None, filters=None): if party_type == "Customer": @@ -1091,6 +1150,8 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount= paid_amount, received_amount = set_paid_amount_and_received_amount( dt, party_account_currency, bank, outstanding_amount, payment_type, bank_amount, doc) + paid_amount, received_amount, discount_amount = apply_early_payment_discount(paid_amount, received_amount, doc) + pe = frappe.new_doc("Payment Entry") pe.payment_type = payment_type pe.company = doc.company @@ -1160,11 +1221,20 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount= pe.setup_party_account_field() pe.set_missing_values() + if party_account and bank: if dt == "Employee Advance": reference_doc = doc pe.set_exchange_rate(ref_doc=reference_doc) pe.set_amounts() + if discount_amount: + pe.set_gain_or_loss(account_details={ + 'account': frappe.get_cached_value('Company', pe.company, "default_discount_account"), + 'cost_center': pe.cost_center or frappe.get_cached_value('Company', pe.company, "cost_center"), + 'amount': discount_amount * (-1 if payment_type == "Pay" else 1) + }) + pe.set_difference_amount() + return pe def get_bank_cash_account(doc, bank_account): @@ -1285,6 +1355,33 @@ def set_paid_amount_and_received_amount(dt, party_account_currency, bank, outsta paid_amount = received_amount * doc.get('exchange_rate', 1) return paid_amount, received_amount +def apply_early_payment_discount(paid_amount, received_amount, doc): + total_discount = 0 + if doc.doctype in ['Sales Invoice', 'Purchase Invoice'] and doc.payment_schedule: + for term in doc.payment_schedule: + if not term.discounted_amount and term.discount and getdate(nowdate()) <= term.discount_date: + if term.discount_type == 'Percentage': + discount_amount = flt(doc.get('grand_total')) * (term.discount / 100) + else: + discount_amount = term.discount + + discount_amount_in_foreign_currency = discount_amount * doc.get('conversion_rate', 1) + + if doc.doctype == 'Sales Invoice': + paid_amount -= discount_amount + received_amount -= discount_amount_in_foreign_currency + else: + received_amount -= discount_amount + paid_amount -= discount_amount_in_foreign_currency + + total_discount += discount_amount + + if total_discount: + money = frappe.utils.fmt_money(total_discount, currency=doc.get('currency')) + frappe.msgprint(_("Discount of {} applied as per Payment Term").format(money), alert=1) + + return paid_amount, received_amount, total_discount + def get_reference_as_per_payment_terms(payment_schedule, dt, dn, doc, grand_total, outstanding_amount): references = [] for payment_term in payment_schedule: diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py index 772fc1a252..4641d6b5ff 100644 --- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py @@ -193,6 +193,34 @@ class TestPaymentEntry(unittest.TestCase): self.assertEqual(si.payment_schedule[0].paid_amount, 200.0) self.assertEqual(si.payment_schedule[1].paid_amount, 36.0) + def test_payment_entry_against_payment_terms_with_discount(self): + si = create_sales_invoice(do_not_save=1, qty=1, rate=200) + create_payment_terms_template_with_discount() + si.payment_terms_template = 'Test Discount Template' + + frappe.db.set_value('Company', si.company, 'default_discount_account', 'Write Off - _TC') + + si.append('taxes', { + "charge_type": "On Net Total", + "account_head": "_Test Account Service Tax - _TC", + "cost_center": "_Test Cost Center - _TC", + "description": "Service Tax", + "rate": 18 + }) + si.save() + + si.submit() + + pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Cash - _TC") + pe.submit() + si.load_from_db() + + self.assertEqual(pe.references[0].payment_term, '30 Credit Days with 10% Discount') + self.assertEqual(si.payment_schedule[0].payment_amount, 236.0) + self.assertEqual(si.payment_schedule[0].paid_amount, 212.40) + self.assertEqual(si.payment_schedule[0].outstanding, 0) + self.assertEqual(si.payment_schedule[0].discounted_amount, 23.6) + def test_payment_against_purchase_invoice_to_check_status(self): pi = make_purchase_invoice(supplier="_Test Supplier USD", debit_to="_Test Payable USD - _TC", @@ -591,6 +619,26 @@ def create_payment_terms_template(): }] }).insert() +def create_payment_terms_template_with_discount(): + + create_payment_term('30 Credit Days with 10% Discount') + + if not frappe.db.exists('Payment Terms Template', 'Test Discount Template'): + payment_term_template = frappe.get_doc({ + 'doctype': 'Payment Terms Template', + 'template_name': 'Test Discount Template', + 'allocate_payment_based_on_payment_terms': 1, + 'terms': [{ + 'doctype': 'Payment Terms Template Detail', + 'payment_term': '30 Credit Days with 10% Discount', + 'invoice_portion': 100, + 'credit_days_based_on': 'Day(s) after invoice date', + 'credit_days': 2, + 'discount': 10, + 'discount_validity_based_on': 'Day(s) after invoice date', + 'discount_validity': 1 + }] + }).insert() def create_payment_term(name): if not frappe.db.exists('Payment Term', name): diff --git a/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json b/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json index 8f5e9fbc28..912ad0977a 100644 --- a/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json +++ b/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json @@ -58,7 +58,7 @@ "fieldname": "total_amount", "fieldtype": "Float", "in_list_view": 1, - "label": "Total Amount", + "label": "Grand Total", "print_hide": 1, "read_only": 1 }, @@ -92,9 +92,10 @@ "options": "Payment Term" } ], + "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-03-13 12:07:19.362539", + "modified": "2021-02-10 11:25:47.144392", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Entry Reference", diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index f7a15c04fa..cf6ec18f3b 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -11,6 +11,7 @@ from erpnext.accounts.utils import (get_outstanding_invoices, from erpnext.controllers.accounts_controller import get_advance_payment_entries class PaymentReconciliation(Document): + @frappe.whitelist() def get_unreconciled_entries(self): self.get_nonreconciled_payment_entries() self.get_invoice_entries() @@ -147,6 +148,7 @@ class PaymentReconciliation(Document): ent.currency = e.get('currency') ent.outstanding_amount = e.get('outstanding_amount') + @frappe.whitelist() def reconcile(self, args): for e in self.get('payments'): e.invoice_type = None @@ -197,6 +199,7 @@ class PaymentReconciliation(Document): 'difference_account': row.difference_account }) + @frappe.whitelist() def get_difference_amount(self, child_row): if child_row.get("reference_type") != 'Payment Entry': return diff --git a/erpnext/accounts/doctype/payment_schedule/payment_schedule.json b/erpnext/accounts/doctype/payment_schedule/payment_schedule.json index d363cf161b..e362566af0 100644 --- a/erpnext/accounts/doctype/payment_schedule/payment_schedule.json +++ b/erpnext/accounts/doctype/payment_schedule/payment_schedule.json @@ -6,11 +6,23 @@ "engine": "InnoDB", "field_order": [ "payment_term", + "section_break_15", "description", + "section_break_4", "due_date", - "invoice_portion", - "payment_amount", "mode_of_payment", + "column_break_5", + "invoice_portion", + "section_break_6", + "discount_type", + "discount_date", + "column_break_9", + "discount", + "section_break_9", + "payment_amount", + "discounted_amount", + "column_break_3", + "outstanding", "paid_amount" ], "fields": [ @@ -25,6 +37,7 @@ }, { "columns": 2, + "fetch_from": "payment_term.description", "fieldname": "description", "fieldtype": "Small Text", "in_list_view": 1, @@ -62,14 +75,82 @@ "options": "Mode of Payment" }, { + "depends_on": "paid_amount", "fieldname": "paid_amount", "fieldtype": "Currency", "label": "Paid Amount" + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "default": "0", + "depends_on": "discounted_amount", + "fieldname": "discounted_amount", + "fieldtype": "Currency", + "label": "Discounted Amount", + "read_only": 1 + }, + { + "fetch_from": "payment_amount", + "fieldname": "outstanding", + "fieldtype": "Currency", + "label": "Outstanding", + "read_only": 1 + }, + { + "fieldname": "column_break_5", + "fieldtype": "Column Break" + }, + { + "depends_on": "discount", + "fieldname": "discount_date", + "fieldtype": "Date", + "label": "Discount Date", + "mandatory_depends_on": "discount" + }, + { + "default": "Percentage", + "fetch_from": "payment_term.discount_type", + "fieldname": "discount_type", + "fieldtype": "Select", + "label": "Discount Type", + "options": "Percentage\nAmount" + }, + { + "fetch_from": "payment_term.discount", + "fieldname": "discount", + "fieldtype": "Float", + "label": "Discount" + }, + { + "fieldname": "section_break_9", + "fieldtype": "Section Break" + }, + { + "collapsible": 1, + "fieldname": "section_break_15", + "fieldtype": "Section Break", + "label": "Description" + }, + { + "fieldname": "section_break_6", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_9", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_4", + "fieldtype": "Section Break" } ], + "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-03-13 17:58:24.729526", + "modified": "2021-02-15 21:03:12.540546", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Schedule", diff --git a/erpnext/accounts/doctype/payment_term/payment_term.js b/erpnext/accounts/doctype/payment_term/payment_term.js index 054c2d1191..acd0144c2e 100644 --- a/erpnext/accounts/doctype/payment_term/payment_term.js +++ b/erpnext/accounts/doctype/payment_term/payment_term.js @@ -1,2 +1,22 @@ // Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt +frappe.ui.form.on('Payment Term', { + onload(frm) { + frm.trigger('set_dynamic_description'); + }, + discount(frm) { + frm.trigger('set_dynamic_description'); + }, + discount_type(frm) { + frm.trigger('set_dynamic_description'); + }, + set_dynamic_description(frm) { + if (frm.doc.discount) { + let description = __("{0}% of total invoice value will be given as discount.", [frm.doc.discount]); + if (frm.doc.discount_type == 'Amount') { + description = __("{0} will be given as discount.", [fmt_money(frm.doc.discount)]); + } + frm.set_df_property("discount", "description", description); + } + } +}); \ No newline at end of file diff --git a/erpnext/accounts/doctype/payment_term/payment_term.json b/erpnext/accounts/doctype/payment_term/payment_term.json index e77c244d3d..aec4965d79 100644 --- a/erpnext/accounts/doctype/payment_term/payment_term.json +++ b/erpnext/accounts/doctype/payment_term/payment_term.json @@ -1,386 +1,166 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 1, - "allow_rename": 1, - "autoname": "field:payment_term_name", - "beta": 0, - "creation": "2017-08-10 15:24:54.876365", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "allow_import": 1, + "allow_rename": 1, + "autoname": "field:payment_term_name", + "creation": "2017-08-10 15:24:54.876365", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "payment_term_name", + "invoice_portion", + "mode_of_payment", + "column_break_3", + "due_date_based_on", + "credit_days", + "credit_months", + "section_break_8", + "discount_type", + "discount", + "column_break_11", + "discount_validity_based_on", + "discount_validity", + "section_break_6", + "description" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 1, - "collapsible": 0, - "columns": 0, - "fieldname": "payment_term_name", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Payment Term Name", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "bold": 1, + "fieldname": "payment_term_name", + "fieldtype": "Data", + "label": "Payment Term Name", + "unique": 1 + }, { - "description": "Provide the invoice portion in percent", - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 1, - "collapsible": 0, - "columns": 0, - "fieldname": "invoice_portion", - "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Invoice Portion", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "bold": 1, + "fieldname": "invoice_portion", + "fieldtype": "Float", + "label": "Invoice Portion (%)" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "mode_of_payment", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Mode of Payment", - "length": 0, - "no_copy": 0, - "options": "Mode of Payment", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "mode_of_payment", + "fieldtype": "Link", + "label": "Mode of Payment", + "options": "Mode of Payment" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_3", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 1, - "collapsible": 0, - "columns": 0, - "fieldname": "due_date_based_on", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Due Date Based On", - "length": 0, - "no_copy": 0, - "options": "Day(s) after invoice date\nDay(s) after the end of the invoice month\nMonth(s) after the end of the invoice month", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "bold": 1, + "fieldname": "due_date_based_on", + "fieldtype": "Select", + "label": "Due Date Based On", + "options": "Day(s) after invoice date\nDay(s) after the end of the invoice month\nMonth(s) after the end of the invoice month" + }, { - "description": "Give number of days according to prior selection", - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 1, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:in_list(['Day(s) after invoice date', 'Day(s) after the end of the invoice month'], doc.due_date_based_on)", - "fieldname": "credit_days", - "fieldtype": "Int", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Credit Days", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "bold": 1, + "depends_on": "eval:in_list(['Day(s) after invoice date', 'Day(s) after the end of the invoice month'], doc.due_date_based_on)", + "fieldname": "credit_days", + "fieldtype": "Int", + "label": "Credit Days" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:doc.due_date_based_on=='Month(s) after the end of the invoice month'", - "fieldname": "credit_months", - "fieldtype": "Int", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Credit Months", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "depends_on": "eval:doc.due_date_based_on=='Month(s) after the end of the invoice month'", + "fieldname": "credit_months", + "fieldtype": "Int", + "label": "Credit Months" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "section_break_6", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "section_break_6", + "fieldtype": "Section Break" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 1, - "collapsible": 0, - "columns": 0, - "fieldname": "description", - "fieldtype": "Small Text", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Description", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "bold": 1, + "fieldname": "description", + "fieldtype": "Small Text", + "label": "Description" + }, + { + "fieldname": "section_break_8", + "fieldtype": "Section Break", + "label": "Discount Settings" + }, + { + "default": "Percentage", + "fieldname": "discount_type", + "fieldtype": "Select", + "label": "Discount Type", + "options": "Percentage\nAmount" + }, + { + "fieldname": "discount", + "fieldtype": "Float", + "label": "Discount" + }, + { + "default": "Day(s) after invoice date", + "depends_on": "discount", + "fieldname": "discount_validity_based_on", + "fieldtype": "Select", + "label": "Discount Validity Based On", + "options": "Day(s) after invoice date\nDay(s) after the end of the invoice month\nMonth(s) after the end of the invoice month" + }, + { + "depends_on": "discount", + "fieldname": "discount_validity", + "fieldtype": "Int", + "label": "Discount Validity", + "mandatory_depends_on": "discount" + }, + { + "fieldname": "column_break_11", + "fieldtype": "Column Break" } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2020-10-14 10:47:32.830478", - "modified_by": "Administrator", - "module": "Accounts", - "name": "Payment Term", - "name_case": "", - "owner": "Administrator", + ], + "links": [], + "modified": "2021-02-15 20:30:56.256403", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Payment Term", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, "write": 1 - }, + }, { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Accounts Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts Manager", + "share": 1, "write": 1 - }, + }, { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Accounts User", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts User", + "share": 1, "write": 1 } - ], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0 -} + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.js b/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.js index f5c5bca87a..84c8d09b16 100644 --- a/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.js +++ b/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.js @@ -3,11 +3,6 @@ frappe.ui.form.on('Payment Terms Template', { setup: function(frm) { - frm.add_fetch("payment_term", "description", "description"); - frm.add_fetch("payment_term", "invoice_portion", "invoice_portion"); - frm.add_fetch("payment_term", "due_date_based_on", "due_date_based_on"); - frm.add_fetch("payment_term", "credit_days", "credit_days"); - frm.add_fetch("payment_term", "credit_months", "credit_months"); - frm.add_fetch("payment_term", "mode_of_payment", "mode_of_payment"); + } }); diff --git a/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.py b/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.py index 2b2b6afe79..80e3348d81 100644 --- a/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.py +++ b/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.py @@ -13,7 +13,6 @@ from frappe import _ class PaymentTermsTemplate(Document): def validate(self): self.validate_invoice_portion() - self.validate_credit_days() self.check_duplicate_terms() def validate_invoice_portion(self): @@ -24,11 +23,6 @@ class PaymentTermsTemplate(Document): if flt(total_portion, 2) != 100.00: frappe.msgprint(_('Combined invoice portion must equal 100%'), raise_exception=1, indicator='red') - def validate_credit_days(self): - for term in self.terms: - if cint(term.credit_days) < 0: - frappe.msgprint(_('Credit Days cannot be a negative number'), raise_exception=1, indicator='red') - def check_duplicate_terms(self): terms = [] for term in self.terms: diff --git a/erpnext/accounts/doctype/payment_terms_template_detail/payment_terms_template_detail.json b/erpnext/accounts/doctype/payment_terms_template_detail/payment_terms_template_detail.json index eee3223314..20b3dca6aa 100644 --- a/erpnext/accounts/doctype/payment_terms_template_detail/payment_terms_template_detail.json +++ b/erpnext/accounts/doctype/payment_terms_template_detail/payment_terms_template_detail.json @@ -1,278 +1,164 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "", - "beta": 0, - "creation": "2017-08-10 15:34:09.409562", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "creation": "2017-08-10 15:34:09.409562", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "payment_term", + "section_break_13", + "description", + "section_break_4", + "invoice_portion", + "mode_of_payment", + "column_break_3", + "due_date_based_on", + "credit_days", + "credit_months", + "section_break_8", + "discount_type", + "discount", + "column_break_11", + "discount_validity_based_on", + "discount_validity" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 2, - "fieldname": "payment_term", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Payment Term", - "length": 0, - "no_copy": 0, - "options": "Payment Term", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "columns": 2, + "fieldname": "payment_term", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Payment Term", + "options": "Payment Term" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 2, - "fieldname": "description", - "fieldtype": "Small Text", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Description", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "columns": 2, + "fetch_from": "payment_term.description", + "fieldname": "description", + "fieldtype": "Small Text", + "in_list_view": 1, + "label": "Description" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 2, - "default": "0", - "fieldname": "invoice_portion", - "fieldtype": "Percent", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Invoice Portion", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "columns": 2, + "fetch_from": "payment_term.invoice_portion", + "fetch_if_empty": 1, + "fieldname": "invoice_portion", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Invoice Portion (%)", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 2, - "fieldname": "due_date_based_on", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Due Date Based On", - "length": 0, - "no_copy": 0, - "options": "Day(s) after invoice date\nDay(s) after the end of the invoice month\nMonth(s) after the end of the invoice month", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "columns": 2, + "fetch_from": "payment_term.due_date_based_on", + "fetch_if_empty": 1, + "fieldname": "due_date_based_on", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Due Date Based On", + "options": "Day(s) after invoice date\nDay(s) after the end of the invoice month\nMonth(s) after the end of the invoice month", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 2, - "default": "0", - "depends_on": "eval:in_list(['Day(s) after invoice date', 'Day(s) after the end of the invoice month'], doc.due_date_based_on)", - "fieldname": "credit_days", - "fieldtype": "Int", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Credit Days", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "columns": 2, + "default": "0", + "depends_on": "eval:in_list(['Day(s) after invoice date', 'Day(s) after the end of the invoice month'], doc.due_date_based_on)", + "fetch_from": "payment_term.credit_days", + "fetch_if_empty": 1, + "fieldname": "credit_days", + "fieldtype": "Int", + "in_list_view": 1, + "label": "Credit Days", + "non_negative": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "0", - "depends_on": "eval:doc.due_date_based_on=='Month(s) after the end of the invoice month'", - "fieldname": "credit_months", - "fieldtype": "Int", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Credit Months", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "default": "0", + "depends_on": "eval:doc.due_date_based_on=='Month(s) after the end of the invoice month'", + "fetch_from": "payment_term.credit_months", + "fetch_if_empty": 1, + "fieldname": "credit_months", + "fieldtype": "Int", + "label": "Credit Months", + "non_negative": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "mode_of_payment", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Mode of Payment", - "length": 0, - "no_copy": 0, - "options": "Mode of Payment", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fetch_from": "payment_term.mode_of_payment", + "fieldname": "mode_of_payment", + "fieldtype": "Link", + "label": "Mode of Payment", + "options": "Mode of Payment" + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_8", + "fieldtype": "Section Break", + "label": "Discount Settings" + }, + { + "default": "Percentage", + "fetch_from": "payment_term.discount_type", + "fetch_if_empty": 1, + "fieldname": "discount_type", + "fieldtype": "Select", + "label": "Discount Type", + "options": "Percentage\nAmount" + }, + { + "fetch_from": "payment_term.discount", + "fetch_if_empty": 1, + "fieldname": "discount", + "fieldtype": "Float", + "label": "Discount" + }, + { + "fieldname": "column_break_11", + "fieldtype": "Column Break" + }, + { + "default": "Day(s) after invoice date", + "depends_on": "discount", + "fetch_from": "payment_term.discount_validity_based_on", + "fetch_if_empty": 1, + "fieldname": "discount_validity_based_on", + "fieldtype": "Select", + "label": "Discount Validity Based On", + "options": "Day(s) after invoice date\nDay(s) after the end of the invoice month\nMonth(s) after the end of the invoice month" + }, + { + "collapsible": 1, + "fieldname": "section_break_13", + "fieldtype": "Section Break", + "label": "Description" + }, + { + "depends_on": "discount", + "fetch_from": "payment_term.discount_validity", + "fetch_if_empty": 1, + "fieldname": "discount_validity", + "fieldtype": "Int", + "label": "Discount Validity", + "mandatory_depends_on": "discount" + }, + { + "fieldname": "section_break_4", + "fieldtype": "Section Break" } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2018-08-21 16:15:55.143025", - "modified_by": "Administrator", - "module": "Accounts", - "name": "Payment Terms Template Detail", - "name_case": "", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-02-24 11:56:12.410807", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Payment Terms Template Detail", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py index f5224a269e..a05e5984f5 100644 --- a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py +++ b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py @@ -18,7 +18,7 @@ class POSClosingEntry(StatusUpdater): self.validate_pos_closing() self.validate_pos_invoices() - + def validate_pos_closing(self): user = frappe.db.sql(""" SELECT name FROM `tabPOS Closing Entry` @@ -37,12 +37,12 @@ class POSClosingEntry(StatusUpdater): bold_user = frappe.bold(self.user) frappe.throw(_("POS Closing Entry {} against {} between selected period") .format(bold_already_exists, bold_user), title=_("Invalid Period")) - + def validate_pos_invoices(self): invalid_rows = [] for d in self.pos_transactions: invalid_row = {'idx': d.idx} - pos_invoice = frappe.db.get_values("POS Invoice", d.pos_invoice, + pos_invoice = frappe.db.get_values("POS Invoice", d.pos_invoice, ["consolidated_invoice", "pos_profile", "docstatus", "owner"], as_dict=1)[0] if pos_invoice.consolidated_invoice: invalid_row.setdefault('msg', []).append(_('POS Invoice is {}').format(frappe.bold("already consolidated"))) @@ -68,14 +68,15 @@ class POSClosingEntry(StatusUpdater): frappe.throw(error_list, title=_("Invalid POS Invoices"), as_list=True) + @frappe.whitelist() def get_payment_reconciliation_details(self): currency = frappe.get_cached_value('Company', self.company, "default_currency") return frappe.render_template("erpnext/accounts/doctype/pos_closing_entry/closing_voucher_details.html", {"data": self, "currency": currency}) - + def on_submit(self): consolidate_pos_invoices(closing_entry=self) - + def on_cancel(self): unconsolidate_pos_invoices(closing_entry=self) 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 40db09ec3b..b596c0cf25 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 @@ -5,12 +5,21 @@ from __future__ import unicode_literals import frappe import unittest from frappe.utils import nowdate +from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry from erpnext.accounts.doctype.pos_invoice.test_pos_invoice import create_pos_invoice from erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry import make_closing_entry_from_opening from erpnext.accounts.doctype.pos_opening_entry.test_pos_opening_entry import create_opening_entry from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile class TestPOSClosingEntry(unittest.TestCase): + def setUp(self): + # Make stock available for POS Sales + make_stock_entry(target="_Test Warehouse - _TC", qty=2, basic_rate=100) + + def tearDown(self): + frappe.set_user("Administrator") + frappe.db.sql("delete from `tabPOS Profile`") + def test_pos_closing_entry(self): test_user, pos_profile = init_user_and_profile() opening_entry = create_opening_entry(pos_profile, test_user.name) @@ -41,9 +50,6 @@ class TestPOSClosingEntry(unittest.TestCase): self.assertEqual(pcv_doc.total_quantity, 2) self.assertEqual(pcv_doc.net_total, 6700) - frappe.set_user("Administrator") - frappe.db.sql("delete from `tabPOS Profile`") - def test_cancelling_of_pos_closing_entry(self): test_user, pos_profile = init_user_and_profile() opening_entry = create_opening_entry(pos_profile, test_user.name) @@ -84,8 +90,6 @@ class TestPOSClosingEntry(unittest.TestCase): self.assertEqual(si_doc.docstatus, 2) self.assertEqual(pos_inv1.status, 'Paid') - frappe.set_user("Administrator") - frappe.db.sql("delete from `tabPOS Profile`") def init_user_and_profile(**args): user = 'test@example.com' @@ -103,4 +107,4 @@ def init_user_and_profile(**args): pos_profile.save() - return test_user, pos_profile \ No newline at end of file + return test_user, pos_profile diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index 76e00923c4..832fb8069a 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -57,7 +57,7 @@ class POSInvoice(SalesInvoice): self.apply_loyalty_points() self.check_phone_payments() self.set_status(update=True) - + 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( @@ -221,7 +221,7 @@ class POSInvoice(SalesInvoice): base_grand_total = flt(self.base_rounded_total) or flt(self.base_grand_total) if not flt(self.change_amount) and grand_total < flt(self.paid_amount): self.change_amount = flt(self.paid_amount - grand_total + flt(self.write_off_amount)) - self.base_change_amount = flt(self.base_paid_amount - base_grand_total + flt(self.base_write_off_amount)) + self.base_change_amount = flt(self.base_paid_amount) - base_grand_total + flt(self.base_write_off_amount) if flt(self.change_amount) and not self.account_for_change_amount: frappe.msgprint(_("Please enter Account for Change Amount"), raise_exception=1) @@ -355,6 +355,7 @@ class POSInvoice(SalesInvoice): return profile + @frappe.whitelist() def set_missing_values(self, for_validate=False): profile = self.set_pos_fields(for_validate) @@ -377,12 +378,20 @@ class POSInvoice(SalesInvoice): "allow_print_before_pay": profile.get("allow_print_before_pay") } + @frappe.whitelist() + def reset_mode_of_payments(self): + if self.pos_profile: + pos_profile = frappe.get_cached_doc('POS Profile', self.pos_profile) + update_multi_mode_option(self, pos_profile) + self.paid_amount = 0 + def set_account_for_mode_of_payment(self): self.payments = [d for d in self.payments if d.amount or d.base_amount or d.default] for pay in self.payments: if not pay.account: pay.account = get_bank_cash_account(pay.mode_of_payment, self.company).get("account") + @frappe.whitelist() def create_payment_request(self): for pay in self.payments: if pay.type == "Phone": @@ -400,7 +409,7 @@ class POSInvoice(SalesInvoice): pay_req.request_phone_payment() return pay_req - + def get_new_payment_request(self, mop): payment_gateway_account = frappe.db.get_value("Payment Gateway Account", { "payment_account": mop.account, diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py index db046c9800..d880caa3c7 100644 --- a/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py +++ b/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py @@ -14,85 +14,89 @@ class TestPOSInvoiceMergeLog(unittest.TestCase): def test_consolidated_invoice_creation(self): frappe.db.sql("delete from `tabPOS Invoice`") - test_user, pos_profile = init_user_and_profile() + try: + test_user, pos_profile = init_user_and_profile() - pos_inv = create_pos_invoice(rate=300, do_not_submit=1) - pos_inv.append('payments', { - 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 300 - }) - pos_inv.submit() + pos_inv = create_pos_invoice(rate=300, do_not_submit=1) + pos_inv.append('payments', { + 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 300 + }) + pos_inv.submit() - pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1) - pos_inv2.append('payments', { - 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 3200 - }) - pos_inv2.submit() + pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1) + pos_inv2.append('payments', { + 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 3200 + }) + pos_inv2.submit() - pos_inv3 = create_pos_invoice(customer="_Test Customer 2", rate=2300, do_not_submit=1) - pos_inv3.append('payments', { - 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 2300 - }) - pos_inv3.submit() + pos_inv3 = create_pos_invoice(customer="_Test Customer 2", rate=2300, do_not_submit=1) + pos_inv3.append('payments', { + 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 2300 + }) + pos_inv3.submit() - consolidate_pos_invoices() + consolidate_pos_invoices() - pos_inv.load_from_db() - self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv.consolidated_invoice)) + pos_inv.load_from_db() + self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv.consolidated_invoice)) - pos_inv3.load_from_db() - self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv3.consolidated_invoice)) + pos_inv3.load_from_db() + self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv3.consolidated_invoice)) - self.assertFalse(pos_inv.consolidated_invoice == pos_inv3.consolidated_invoice) + self.assertFalse(pos_inv.consolidated_invoice == pos_inv3.consolidated_invoice) + + finally: + frappe.set_user("Administrator") + frappe.db.sql("delete from `tabPOS Profile`") + frappe.db.sql("delete from `tabPOS Invoice`") - frappe.set_user("Administrator") - frappe.db.sql("delete from `tabPOS Profile`") - frappe.db.sql("delete from `tabPOS Invoice`") - def test_consolidated_credit_note_creation(self): frappe.db.sql("delete from `tabPOS Invoice`") - test_user, pos_profile = init_user_and_profile() + try: + test_user, pos_profile = init_user_and_profile() - pos_inv = create_pos_invoice(rate=300, do_not_submit=1) - pos_inv.append('payments', { - 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 300 - }) - pos_inv.submit() + pos_inv = create_pos_invoice(rate=300, do_not_submit=1) + pos_inv.append('payments', { + 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 300 + }) + pos_inv.submit() - pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1) - pos_inv2.append('payments', { - 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 3200 - }) - pos_inv2.submit() + pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1) + pos_inv2.append('payments', { + 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 3200 + }) + pos_inv2.submit() - pos_inv3 = create_pos_invoice(customer="_Test Customer 2", rate=2300, do_not_submit=1) - pos_inv3.append('payments', { - 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 2300 - }) - pos_inv3.submit() + pos_inv3 = create_pos_invoice(customer="_Test Customer 2", rate=2300, do_not_submit=1) + pos_inv3.append('payments', { + 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 2300 + }) + pos_inv3.submit() - pos_inv_cn = make_sales_return(pos_inv.name) - pos_inv_cn.set("payments", []) - pos_inv_cn.append('payments', { - 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': -300 - }) - pos_inv_cn.paid_amount = -300 - pos_inv_cn.submit() + pos_inv_cn = make_sales_return(pos_inv.name) + pos_inv_cn.set("payments", []) + pos_inv_cn.append('payments', { + 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': -300 + }) + pos_inv_cn.paid_amount = -300 + pos_inv_cn.submit() - consolidate_pos_invoices() + consolidate_pos_invoices() - pos_inv.load_from_db() - self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv.consolidated_invoice)) + pos_inv.load_from_db() + self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv.consolidated_invoice)) - pos_inv3.load_from_db() - self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv3.consolidated_invoice)) + pos_inv3.load_from_db() + self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv3.consolidated_invoice)) - pos_inv_cn.load_from_db() - self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv_cn.consolidated_invoice)) - self.assertTrue(frappe.db.get_value("Sales Invoice", pos_inv_cn.consolidated_invoice, "is_return")) + pos_inv_cn.load_from_db() + self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv_cn.consolidated_invoice)) + self.assertTrue(frappe.db.get_value("Sales Invoice", pos_inv_cn.consolidated_invoice, "is_return")) - frappe.set_user("Administrator") - frappe.db.sql("delete from `tabPOS Profile`") - frappe.db.sql("delete from `tabPOS Invoice`") + finally: + frappe.set_user("Administrator") + frappe.db.sql("delete from `tabPOS Profile`") + frappe.db.sql("delete from `tabPOS Invoice`") diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.json b/erpnext/accounts/doctype/pricing_rule/pricing_rule.json index 33771645fe..428989aa96 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.json +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.json @@ -44,6 +44,14 @@ "column_break_21", "min_amt", "max_amt", + "product_discount_scheme_section", + "same_item", + "free_item", + "free_qty", + "free_item_rate", + "column_break_42", + "free_item_uom", + "is_recursive", "section_break_23", "valid_from", "valid_upto", @@ -62,13 +70,6 @@ "discount_amount", "discount_percentage", "for_price_list", - "product_discount_scheme_section", - "same_item", - "free_item", - "free_qty", - "column_break_51", - "free_item_uom", - "free_item_rate", "section_break_13", "threshold_percentage", "priority", @@ -458,10 +459,6 @@ "fieldtype": "Float", "label": "Qty" }, - { - "fieldname": "column_break_51", - "fieldtype": "Column Break" - }, { "fieldname": "free_item_uom", "fieldtype": "Link", @@ -552,19 +549,33 @@ "fieldname": "promotional_scheme", "fieldtype": "Link", "label": "Promotional Scheme", - "options": "Promotional Scheme" + "no_copy": 1, + "options": "Promotional Scheme", + "print_hide": 1, + "read_only": 1 }, { "description": "Simple Python Expression, Example: territory != 'All Territories'", "fieldname": "condition", "fieldtype": "Code", "label": "Condition" + }, + { + "fieldname": "column_break_42", + "fieldtype": "Column Break" + }, + { + "default": "0", + "description": "Discounts to be applied in sequential ranges like buy 1 get 1, buy 2 get 2, buy 3 get 3 and so on", + "fieldname": "is_recursive", + "fieldtype": "Check", + "label": "Is Recursive" } ], "icon": "fa fa-gift", "idx": 1, "links": [], - "modified": "2021-03-01 23:18:38.717613", + "modified": "2021-03-06 22:01:24.840422", "modified_by": "Administrator", "module": "Accounts", "name": "Pricing Rule", diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py index f0b4e2976d..aedf1c6f1a 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py @@ -237,6 +237,7 @@ def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=Fa "doctype": args.doctype, "has_margin": False, "name": args.name, + "free_item_data": [], "parent": args.parent, "parenttype": args.parenttype, "child_docname": args.get('child_docname') diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py index d163335996..c676abd4c6 100644 --- a/erpnext/accounts/doctype/pricing_rule/utils.py +++ b/erpnext/accounts/doctype/pricing_rule/utils.py @@ -367,7 +367,7 @@ def get_qty_and_rate_for_mixed_conditions(doc, pr_doc, args): if items and doc.get("items"): for row in doc.get('items'): - if row.get(apply_on) not in items: continue + if (row.get(apply_on) or args.get(apply_on)) not in items: continue if pr_doc.mixed_conditions: amt = args.get('qty') * args.get("price_list_rate") @@ -479,7 +479,7 @@ def apply_pricing_rule_on_transaction(doc): doc.calculate_taxes_and_totals() elif d.price_or_product_discount == 'Product': - item_details = frappe._dict({'parenttype': doc.doctype}) + item_details = frappe._dict({'parenttype': doc.doctype, 'free_item_data': []}) get_product_discount_rule(d, item_details, doc=doc) apply_pricing_rule_for_free_items(doc, item_details.free_item_data) doc.set_missing_values() @@ -508,9 +508,16 @@ def get_product_discount_rule(pricing_rule, item_details, args=None, doc=None): frappe.throw(_("Free item not set in the pricing rule {0}") .format(get_link_to_form("Pricing Rule", pricing_rule.name))) - item_details.free_item_data = { + qty = pricing_rule.free_qty or 1 + if pricing_rule.is_recursive: + transaction_qty = args.get('qty') if args else doc.total_qty + if transaction_qty: + qty = flt(transaction_qty) * qty + + free_item_data_args = { 'item_code': free_item, - 'qty': pricing_rule.free_qty or 1, + 'qty': qty, + 'pricing_rules': pricing_rule.name, 'rate': pricing_rule.free_item_rate or 0, 'price_list_rate': pricing_rule.free_item_rate or 0, 'is_free_item': 1 @@ -519,24 +526,26 @@ def get_product_discount_rule(pricing_rule, item_details, args=None, doc=None): item_data = frappe.get_cached_value('Item', free_item, ['item_name', 'description', 'stock_uom'], as_dict=1) - item_details.free_item_data.update(item_data) - item_details.free_item_data['uom'] = pricing_rule.free_item_uom or item_data.stock_uom - item_details.free_item_data['conversion_factor'] = get_conversion_factor(free_item, - item_details.free_item_data['uom']).get("conversion_factor", 1) + free_item_data_args.update(item_data) + free_item_data_args['uom'] = pricing_rule.free_item_uom or item_data.stock_uom + free_item_data_args['conversion_factor'] = get_conversion_factor(free_item, + free_item_data_args['uom']).get("conversion_factor", 1) if item_details.get("parenttype") == 'Purchase Order': - item_details.free_item_data['schedule_date'] = doc.schedule_date if doc else today() + free_item_data_args['schedule_date'] = doc.schedule_date if doc else today() if item_details.get("parenttype") == 'Sales Order': - item_details.free_item_data['delivery_date'] = doc.delivery_date if doc else today() + free_item_data_args['delivery_date'] = doc.delivery_date if doc else today() + + item_details.free_item_data.append(free_item_data_args) def apply_pricing_rule_for_free_items(doc, pricing_rule_args, set_missing_values=False): - if pricing_rule_args.get('item_code'): - items = [d.item_code for d in doc.items - if d.item_code == (pricing_rule_args.get("item_code")) and d.is_free_item] + if pricing_rule_args: + items = tuple([(d.item_code, d.pricing_rules) for d in doc.items if d.is_free_item]) - if not items: - doc.append('items', pricing_rule_args) + for args in pricing_rule_args: + if not items or (args.get('item_code'), args.get('pricing_rules')) not in items: + doc.append('items', args) def get_pricing_rule_items(pr_doc): apply_on_data = [] diff --git a/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py b/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py index 89f7238a06..523e9ee08a 100644 --- a/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py +++ b/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py @@ -12,16 +12,16 @@ from frappe.model.document import Document pricing_rule_fields = ['apply_on', 'mixed_conditions', 'is_cumulative', 'other_item_code', 'other_item_group' 'apply_rule_on_other', 'other_brand', 'selling', 'buying', 'applicable_for', 'valid_from', 'valid_upto', 'customer', 'customer_group', 'territory', 'sales_partner', 'campaign', 'supplier', - 'supplier_group', 'company', 'currency'] + 'supplier_group', 'company', 'currency', 'apply_multiple_pricing_rules'] other_fields = ['min_qty', 'max_qty', 'min_amt', 'max_amt', 'priority','warehouse', 'threshold_percentage', 'rule_description'] price_discount_fields = ['rate_or_discount', 'apply_discount_on', 'apply_discount_on_rate', - 'rate', 'discount_amount', 'discount_percentage', 'validate_applied_rule'] + 'rate', 'discount_amount', 'discount_percentage', 'validate_applied_rule', 'apply_multiple_pricing_rules'] product_discount_fields = ['free_item', 'free_qty', 'free_item_uom', - 'free_item_rate', 'same_item'] + 'free_item_rate', 'same_item', 'is_recursive', 'apply_multiple_pricing_rules'] class PromotionalScheme(Document): def validate(self): diff --git a/erpnext/accounts/doctype/promotional_scheme_price_discount/promotional_scheme_price_discount.json b/erpnext/accounts/doctype/promotional_scheme_price_discount/promotional_scheme_price_discount.json index 224b8de779..795fb1c6f4 100644 --- a/erpnext/accounts/doctype/promotional_scheme_price_discount/promotional_scheme_price_discount.json +++ b/erpnext/accounts/doctype/promotional_scheme_price_discount/promotional_scheme_price_discount.json @@ -1,792 +1,181 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, + "actions": [], "creation": "2019-03-24 14:48:59.649168", - "custom": 0, - "docstatus": 0, "doctype": "DocType", - "document_type": "", "editable_grid": 1, "engine": "InnoDB", + "field_order": [ + "disable", + "apply_multiple_pricing_rules", + "column_break_2", + "rule_description", + "section_break_2", + "min_qty", + "max_qty", + "column_break_3", + "min_amount", + "max_amount", + "section_break_6", + "rate_or_discount", + "column_break_10", + "rate", + "discount_amount", + "discount_percentage", + "section_break_11", + "warehouse", + "threshold_percentage", + "validate_applied_rule", + "column_break_14", + "priority", + "apply_discount_on_rate" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, + "default": "0", "fieldname": "disable", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Disable", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Disable" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "column_break_2", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldtype": "Column Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "rule_description", "fieldtype": "Small Text", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Rule Description", - "length": 0, "no_copy": 1, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "section_break_2", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldtype": "Section Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, "columns": 1, "default": "0", "fieldname": "min_qty", "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, - "in_standard_filter": 0, - "label": "Min Qty", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Min Qty" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, "columns": 1, "default": "0", "fieldname": "max_qty", "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, - "in_standard_filter": 0, - "label": "Max Qty", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Max Qty" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "column_break_3", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldtype": "Column Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "0", "fieldname": "min_amount", "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, - "in_standard_filter": 0, - "label": "Min Amount", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Min Amount" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "0", - "depends_on": "", "fieldname": "max_amount", "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, - "in_standard_filter": 0, - "label": "Max Amount", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Max Amount" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "", "fieldname": "section_break_6", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldtype": "Section Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "Discount Percentage", - "depends_on": "", "fieldname": "rate_or_discount", "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, - "in_standard_filter": 0, "label": "Discount Type", - "length": 0, - "no_copy": 0, - "options": "\nRate\nDiscount Percentage\nDiscount Amount", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "options": "\nRate\nDiscount Percentage\nDiscount Amount" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "", "fieldname": "column_break_10", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldtype": "Column Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, "columns": 2, "depends_on": "eval:doc.rate_or_discount==\"Rate\"", "fieldname": "rate", "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, - "in_standard_filter": 0, - "label": "Rate", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Rate" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "depends_on": "eval:doc.rate_or_discount==\"Discount Amount\"", "fieldname": "discount_amount", "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Discount Amount", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Discount Amount" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "depends_on": "eval:doc.rate_or_discount==\"Discount Percentage\"", "fieldname": "discount_percentage", "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Discount Percentage", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Discount Percentage" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "section_break_11", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldtype": "Section Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "warehouse", "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Warehouse", - "length": 0, - "no_copy": 0, - "options": "Warehouse", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "options": "Warehouse" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "threshold_percentage", "fieldtype": "Percent", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Threshold for Suggestion", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Threshold for Suggestion" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "1", "fieldname": "validate_applied_rule", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Validate Applied Rule", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Validate Applied Rule" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "column_break_14", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldtype": "Column Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "priority", "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Priority", - "length": 0, - "no_copy": 0, - "options": "\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "options": "\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, + "default": "0", "depends_on": "priority", "fieldname": "apply_multiple_pricing_rules", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Apply Multiple Pricing Rules", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Apply Multiple Pricing Rules" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "0", "depends_on": "eval:in_list(['Discount Percentage', 'Discount Amount'], doc.rate_or_discount) && doc.apply_multiple_pricing_rules", "fieldname": "apply_discount_on_rate", "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Apply Discount on Rate", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "label": "Apply Discount on Rate" } ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, + "index_web_pages_for_search": 1, "istable": 1, - "max_attachments": 0, - "modified": "2019-03-24 14:48:59.649168", + "links": [], + "modified": "2021-03-07 11:56:23.424137", "modified_by": "Administrator", "module": "Accounts", "name": "Promotional Scheme Price Discount", - "name_case": "", "owner": "Administrator", "permissions": [], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 0, - "track_seen": 0, - "track_views": 0 + "sort_order": "DESC" } \ No newline at end of file diff --git a/erpnext/accounts/doctype/promotional_scheme_product_discount/promotional_scheme_product_discount.json b/erpnext/accounts/doctype/promotional_scheme_product_discount/promotional_scheme_product_discount.json index 72d53bfa01..3eab51510d 100644 --- a/erpnext/accounts/doctype/promotional_scheme_product_discount/promotional_scheme_product_discount.json +++ b/erpnext/accounts/doctype/promotional_scheme_product_discount/promotional_scheme_product_discount.json @@ -1,10 +1,12 @@ { + "actions": [], "creation": "2019-03-24 14:48:59.649168", "doctype": "DocType", "editable_grid": 1, "engine": "InnoDB", "field_order": [ "disable", + "apply_multiple_pricing_rules", "column_break_2", "rule_description", "section_break_1", @@ -25,7 +27,7 @@ "threshold_percentage", "column_break_15", "priority", - "apply_multiple_pricing_rules" + "is_recursive" ], "fields": [ { @@ -152,10 +154,19 @@ "fieldname": "apply_multiple_pricing_rules", "fieldtype": "Check", "label": "Apply Multiple Pricing Rules" + }, + { + "default": "0", + "description": "Discounts to be applied in sequential ranges like buy 1 get 1, buy 2 get 2, buy 3 get 3 and so on", + "fieldname": "is_recursive", + "fieldtype": "Check", + "label": "Is Recursive" } ], + "index_web_pages_for_search": 1, "istable": 1, - "modified": "2019-07-21 00:00:56.674284", + "links": [], + "modified": "2021-03-06 21:58:18.162346", "modified_by": "Administrator", "module": "Accounts", "name": "Promotional Scheme Product Discount", diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index 06aa20bfc5..66a8e206a8 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -524,7 +524,7 @@ frappe.ui.form.on("Purchase Invoice", { }, onload: function(frm) { - if(frm.doc.__onload) { + if(frm.doc.__onload && frm.is_new()) { if(frm.doc.supplier) { frm.doc.apply_tds = frm.doc.__onload.supplier_tds ? 1 : 0; } diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index ded293b88d..50492f50b5 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -898,7 +898,7 @@ class TestPurchaseInvoice(unittest.TestCase): acc_settings.submit_journal_entries = 1 acc_settings.save() - item = create_item("_Test Item for Deferred Accounting") + item = create_item("_Test Item for Deferred Accounting", is_purchase_item=True) item.enable_deferred_expense = 1 item.deferred_expense_account = deferred_account item.save() diff --git a/erpnext/accounts/doctype/purchase_invoice/test_records.json b/erpnext/accounts/doctype/purchase_invoice/test_records.json index e7166c5a12..9f9e90d8a7 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_records.json +++ b/erpnext/accounts/doctype/purchase_invoice/test_records.json @@ -43,7 +43,7 @@ } ], "grand_total": 0, - "naming_series": "_T-BILL", + "naming_series": "T-PINV-", "taxes": [ { "account_head": "_Test Account Shipping Charges - _TC", @@ -167,7 +167,7 @@ } ], "grand_total": 0, - "naming_series": "_T-Purchase Invoice-", + "naming_series": "T-PINV-", "taxes": [ { "account_head": "_Test Account Shipping Charges - _TC", diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index 720a9175e6..d382386a32 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -1952,13 +1952,12 @@ "is_submittable": 1, "links": [ { - "custom": 1, "group": "Reference", "link_doctype": "POS Invoice", "link_fieldname": "consolidated_invoice" } ], - "modified": "2021-02-01 15:42:26.261540", + "modified": "2021-03-31 15:42:26.261540", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice", diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 4076be724b..21d550a4d3 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -76,7 +76,7 @@ class SalesInvoice(SellingController): if not self.is_pos: self.so_dn_required() - + self.set_tax_withholding() self.validate_proj_cust() @@ -390,6 +390,7 @@ class SalesInvoice(SellingController): if validate_against_credit_limit: check_credit_limit(self.customer, self.company, bypass_credit_limit_check_at_sales_order) + @frappe.whitelist() def set_missing_values(self, for_validate=False): pos = self.set_pos_fields(for_validate) @@ -729,6 +730,7 @@ class SalesInvoice(SellingController): else: self.calculate_billing_amount_for_timesheet() + @frappe.whitelist() def add_timesheet_data(self): self.set('timesheets', []) if self.project: @@ -1286,6 +1288,7 @@ class SalesInvoice(SellingController): break # Healthcare + @frappe.whitelist() def set_healthcare_services(self, checked_values): self.set("items", []) from erpnext.stock.get_item_details import get_item_details diff --git a/erpnext/accounts/doctype/sales_invoice/test_records.json b/erpnext/accounts/doctype/sales_invoice/test_records.json index e00a58f864..3781f8ccc9 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_records.json +++ b/erpnext/accounts/doctype/sales_invoice/test_records.json @@ -31,7 +31,7 @@ "base_grand_total": 561.8, "grand_total": 561.8, "is_pos": 0, - "naming_series": "_T-Sales Invoice-", + "naming_series": "T-SINV-", "base_net_total": 500.0, "taxes": [ { @@ -104,7 +104,7 @@ "base_grand_total": 630.0, "grand_total": 630.0, "is_pos": 0, - "naming_series": "_T-Sales Invoice-", + "naming_series": "T-SINV-", "base_net_total": 500.0, "taxes": [ { @@ -175,7 +175,7 @@ ], "grand_total": 0, "is_pos": 0, - "naming_series": "_T-Sales Invoice-", + "naming_series": "T-SINV-", "taxes": [ { "account_head": "_Test Account Shipping Charges - _TC", @@ -301,7 +301,7 @@ ], "grand_total": 0, "is_pos": 0, - "naming_series": "_T-Sales Invoice-", + "naming_series": "T-SINV-", "taxes": [ { "account_head": "_Test Account Excise Duty - _TC", diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 1b9557839f..f09cc5af96 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -1800,6 +1800,15 @@ class TestSalesInvoice(unittest.TestCase): si.selling_price_list = "_Test Price List Rest of the World" si.update_stock = 1 si.items[0].target_warehouse = 'Work In Progress - TCP1' + + # Add stock to stores for succesful stock transfer + make_stock_entry( + target="Stores - TCP1", + company = "_Test Company with perpetual inventory", + qty=1, + basic_rate=100 + ) + add_taxes(si) si.save() @@ -2106,6 +2115,7 @@ def create_sales_invoice(**args): si.return_against = args.return_against si.currency=args.currency or "INR" si.conversion_rate = args.conversion_rate or 1 + si.naming_series = args.naming_series or "T-SINV-" si.append("items", { "item_code": args.item or args.item_code or "_Test Item", @@ -2269,4 +2279,4 @@ def add_taxes(doc): "cost_center": "Main - TCP1", "description": "Excise Duty", "rate": 12 - }) \ No newline at end of file + }) diff --git a/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py b/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py index 429a9f3591..52d19d54a8 100644 --- a/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py +++ b/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py @@ -46,5 +46,5 @@ def validate_disabled(doc): frappe.throw(_("Disabled template must not be default template")) def validate_for_tax_category(doc): - if frappe.db.exists(doc.doctype, {"company": doc.company, "tax_category": doc.tax_category, "disabled": 0}): + if frappe.db.exists(doc.doctype, {"company": doc.company, "tax_category": doc.tax_category, "disabled": 0, "name": ["!=", doc.name]}): frappe.throw(_("A template with tax category {0} already exists. Only one template is allowed with each tax category").format(frappe.bold(doc.tax_category))) 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 9ce8e3fe83..dd3b49aa04 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 @@ -177,7 +177,7 @@ def cancel_invoices(): for d in purchase_invoices: frappe.get_doc('Purchase Invoice', d).cancel() - + for d in sales_invoices: frappe.get_doc('Sales Invoice', d).cancel() @@ -229,7 +229,8 @@ def create_sales_invoice(**args): 'qty': args.qty or 1, 'rate': args.rate or 10000, 'cost_center': 'Main - _TC', - 'expense_account': 'Cost of Goods Sold - _TC' + 'expense_account': 'Cost of Goods Sold - _TC', + 'warehouse': args.warehouse or '_Test Warehouse - _TC' }] }) @@ -353,4 +354,4 @@ def create_tax_with_holding_category(): 'company': '_Test Company', 'account': 'TDS - _TC' }] - }).insert() \ No newline at end of file + }).insert() diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 51fc7ec49a..444b40ed79 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -364,7 +364,7 @@ class ReceivablePayableReport(object): payment_terms_details = frappe.db.sql(""" select si.name, si.party_account_currency, si.currency, si.conversion_rate, - ps.due_date, ps.payment_amount, ps.description, ps.paid_amount + ps.due_date, ps.payment_amount, ps.description, ps.paid_amount, ps.discounted_amount from `tab{0}` si, `tabPayment Schedule` ps where si.name = ps.parent and @@ -395,13 +395,13 @@ class ReceivablePayableReport(object): "invoiced": invoiced, "invoice_grand_total": row.invoiced, "payment_term": d.description, - "paid": d.paid_amount, + "paid": d.paid_amount + d.discounted_amount, "credit_note": 0.0, - "outstanding": invoiced - d.paid_amount + "outstanding": invoiced - d.paid_amount - d.discounted_amount })) if d.paid_amount: - row['paid'] -= d.paid_amount + row['paid'] -= d.paid_amount + d.discounted_amount def allocate_closing_to_term(self, row, term, key): if row[key]: diff --git a/erpnext/accounts/report/financial_statements.py b/erpnext/accounts/report/financial_statements.py index 7dfce85629..14efa1f8fc 100644 --- a/erpnext/accounts/report/financial_statements.py +++ b/erpnext/accounts/report/financial_statements.py @@ -51,7 +51,11 @@ def get_period_list(from_fiscal_year, to_fiscal_year, period_start_date, period_ "from_date": start_date }) - to_date = add_months(start_date, months_to_add) + if i==0 and filter_based_on == 'Date Range': + to_date = add_months(get_first_day(start_date), months_to_add) + else: + to_date = add_months(start_date, months_to_add) + start_date = to_date # Subtract one day from to_date, as it may be first day in next fiscal year or month diff --git a/erpnext/agriculture/doctype/crop_cycle/crop_cycle.py b/erpnext/agriculture/doctype/crop_cycle/crop_cycle.py index afbd9b4e6e..9000dea913 100644 --- a/erpnext/agriculture/doctype/crop_cycle/crop_cycle.py +++ b/erpnext/agriculture/doctype/crop_cycle/crop_cycle.py @@ -71,6 +71,7 @@ class CropCycle(Document): "exp_end_date": add_days(start_date, crop_task.get("end_day") - 1) }).insert() + @frappe.whitelist() def reload_linked_analysis(self): linked_doctypes = ['Soil Texture', 'Soil Analysis', 'Plant Analysis'] required_fields = ['location', 'name', 'collection_datetime'] @@ -87,6 +88,7 @@ class CropCycle(Document): frappe.publish_realtime("List of Linked Docs", output, user=frappe.session.user) + @frappe.whitelist() def append_to_child(self, obj_to_append): for doctype in obj_to_append: for doc_name in set(obj_to_append[doctype]): diff --git a/erpnext/agriculture/doctype/fertilizer/fertilizer.py b/erpnext/agriculture/doctype/fertilizer/fertilizer.py index dc2781cf00..9cb492aff1 100644 --- a/erpnext/agriculture/doctype/fertilizer/fertilizer.py +++ b/erpnext/agriculture/doctype/fertilizer/fertilizer.py @@ -7,6 +7,7 @@ import frappe from frappe.model.document import Document class Fertilizer(Document): + @frappe.whitelist() def load_contents(self): docs = frappe.get_all("Agriculture Analysis Criteria", filters={'linked_doctype':'Fertilizer'}) for doc in docs: diff --git a/erpnext/agriculture/doctype/plant_analysis/plant_analysis.py b/erpnext/agriculture/doctype/plant_analysis/plant_analysis.py index 304727e04f..2806cc6523 100644 --- a/erpnext/agriculture/doctype/plant_analysis/plant_analysis.py +++ b/erpnext/agriculture/doctype/plant_analysis/plant_analysis.py @@ -8,6 +8,7 @@ from frappe.model.naming import make_autoname from frappe.model.document import Document class PlantAnalysis(Document): + @frappe.whitelist() def load_contents(self): docs = frappe.get_all("Agriculture Analysis Criteria", filters={'linked_doctype':'Plant Analysis'}) for doc in docs: diff --git a/erpnext/agriculture/doctype/soil_analysis/soil_analysis.py b/erpnext/agriculture/doctype/soil_analysis/soil_analysis.py index 17b96a0ac1..37835f8c7b 100644 --- a/erpnext/agriculture/doctype/soil_analysis/soil_analysis.py +++ b/erpnext/agriculture/doctype/soil_analysis/soil_analysis.py @@ -7,6 +7,7 @@ import frappe from frappe.model.document import Document class SoilAnalysis(Document): + @frappe.whitelist() def load_contents(self): docs = frappe.get_all("Agriculture Analysis Criteria", filters={'linked_doctype':'Soil Analysis'}) for doc in docs: diff --git a/erpnext/agriculture/doctype/soil_texture/soil_texture.py b/erpnext/agriculture/doctype/soil_texture/soil_texture.py index 8c1d7ed5ac..209b2c8598 100644 --- a/erpnext/agriculture/doctype/soil_texture/soil_texture.py +++ b/erpnext/agriculture/doctype/soil_texture/soil_texture.py @@ -13,6 +13,7 @@ class SoilTexture(Document): soil_edit_order = [2, 1, 0] soil_types = ['clay_composition', 'sand_composition', 'silt_composition'] + @frappe.whitelist() def load_contents(self): docs = frappe.get_all("Agriculture Analysis Criteria", filters={'linked_doctype':'Soil Texture'}) for doc in docs: @@ -26,6 +27,7 @@ class SoilTexture(Document): if sum(self.get(soil_type) for soil_type in self.soil_types) != 100: frappe.throw(_('Soil compositions do not add up to 100')) + @frappe.whitelist() def update_soil_edit(self, soil_type): self.soil_edit_order[self.soil_types.index(soil_type)] = max(self.soil_edit_order)+1 self.soil_type = self.get_soil_type() @@ -35,8 +37,8 @@ class SoilTexture(Document): if sum(self.soil_edit_order) < 5: return last_edit_index = self.soil_edit_order.index(min(self.soil_edit_order)) - # set composition of the last edited soil - self.set( self.soil_types[last_edit_index], + # set composition of the last edited soil + self.set(self.soil_types[last_edit_index], 100 - sum(cint(self.get(soil_type)) for soil_type in self.soil_types) + cint(self.get(self.soil_types[last_edit_index]))) # calculate soil type @@ -67,4 +69,4 @@ class SoilTexture(Document): elif (c >= 40 and sa <= 45 and si < 40): return 'Clay' else: - return 'Select' \ No newline at end of file + return 'Select' diff --git a/erpnext/agriculture/doctype/water_analysis/water_analysis.py b/erpnext/agriculture/doctype/water_analysis/water_analysis.py index 88f1fbd9cc..d9f007cea1 100644 --- a/erpnext/agriculture/doctype/water_analysis/water_analysis.py +++ b/erpnext/agriculture/doctype/water_analysis/water_analysis.py @@ -9,11 +9,13 @@ from frappe.model.document import Document from frappe import _ class WaterAnalysis(Document): + @frappe.whitelist() def load_contents(self): docs = frappe.get_all("Agriculture Analysis Criteria", filters={'linked_doctype':'Water Analysis'}) for doc in docs: self.append('water_analysis_criteria', {'title': str(doc.name)}) + @frappe.whitelist() def update_lab_result_date(self): if not self.result_datetime: self.result_datetime = self.laboratory_testing_datetime diff --git a/erpnext/agriculture/doctype/weather/weather.py b/erpnext/agriculture/doctype/weather/weather.py index 938daa207e..235e684e51 100644 --- a/erpnext/agriculture/doctype/weather/weather.py +++ b/erpnext/agriculture/doctype/weather/weather.py @@ -7,6 +7,7 @@ import frappe from frappe.model.document import Document class Weather(Document): + @frappe.whitelist() def load_contents(self): docs = frappe.get_all("Agriculture Analysis Criteria", filters={'linked_doctype':'Weather'}) for doc in docs: diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index e8e8ec6cc0..9aff1440d6 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -553,6 +553,7 @@ class Asset(AccountsController): make_gl_entries(gl_entries) self.db_set('booked_fixed_asset', 1) + @frappe.whitelist() def get_depreciation_rate(self, args, on_validate=False): if isinstance(args, string_types): args = json.loads(args) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index d32e98e8d9..90d064604e 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -133,6 +133,7 @@ class PurchaseOrder(BuyingController): d.material_request_item, "schedule_date") + @frappe.whitelist() def get_last_purchase_rate(self): """get last purchase rates for all items""" @@ -252,6 +253,7 @@ class PurchaseOrder(BuyingController): self.update_prevdoc_status() # Must be called after updating ordered qty in Material Request + # bin uses Material Request Items to recalculate & update self.update_requested_qty() self.update_ordered_qty() diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index 604c88682f..3c4f908ee4 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -90,6 +90,50 @@ class TestPurchaseOrder(unittest.TestCase): frappe.db.set_value('Item', '_Test Item', 'over_billing_allowance', 0) frappe.db.set_value("Accounts Settings", None, "over_billing_allowance", 0) + def test_update_remove_child_linked_to_mr(self): + """Test impact on linked PO and MR on deleting/updating row.""" + mr = make_material_request(qty=10) + po = make_purchase_order(mr.name) + po.supplier = "_Test Supplier" + po.save() + po.submit() + + first_item_of_po = po.get("items")[0] + existing_ordered_qty = get_ordered_qty() # 10 + existing_requested_qty = get_requested_qty() # 0 + + # decrease ordered qty by 3 (10 -> 7) and add item + trans_item = json.dumps([ + { + 'item_code': first_item_of_po.item_code, + 'rate': first_item_of_po.rate, + 'qty': 7, + 'docname': first_item_of_po.name + }, + {'item_code' : '_Test Item 2', 'rate' : 200, 'qty' : 2} + ]) + update_child_qty_rate('Purchase Order', trans_item, po.name) + mr.reload() + + # requested qty increases as ordered qty decreases + self.assertEqual(get_requested_qty(), existing_requested_qty + 3) # 3 + self.assertEqual(mr.items[0].ordered_qty, 7) + + self.assertEqual(get_ordered_qty(), existing_ordered_qty - 3) # 7 + + # delete first item linked to Material Request + trans_item = json.dumps([ + {'item_code' : '_Test Item 2', 'rate' : 200, 'qty' : 2} + ]) + update_child_qty_rate('Purchase Order', trans_item, po.name) + mr.reload() + + # requested qty increases as ordered qty is 0 (deleted row) + self.assertEqual(get_requested_qty(), existing_requested_qty + 10) # 10 + self.assertEqual(mr.items[0].ordered_qty, 0) + + # ordered qty decreases as ordered qty is 0 (deleted row) + self.assertEqual(get_ordered_qty(), existing_ordered_qty - 10) # 0 def test_update_child(self): mr = make_material_request(qty=10) @@ -120,7 +164,6 @@ class TestPurchaseOrder(unittest.TestCase): self.assertEqual(po.get("items")[0].amount, 1400) self.assertEqual(get_ordered_qty(), existing_ordered_qty + 3) - def test_update_child_adding_new_item(self): po = create_purchase_order(do_not_save=1) po.items[0].qty = 4 @@ -129,6 +172,7 @@ class TestPurchaseOrder(unittest.TestCase): pr = make_pr_against_po(po.name, 2) po.load_from_db() + existing_ordered_qty = get_ordered_qty() first_item_of_po = po.get("items")[0] trans_item = json.dumps([ @@ -145,7 +189,8 @@ class TestPurchaseOrder(unittest.TestCase): po.reload() self.assertEquals(len(po.get('items')), 2) self.assertEqual(po.status, 'To Receive and Bill') - + # ordered qty should increase on row addition + self.assertEqual(get_ordered_qty(), existing_ordered_qty + 7) def test_update_child_removing_item(self): po = create_purchase_order(do_not_save=1) @@ -156,6 +201,7 @@ class TestPurchaseOrder(unittest.TestCase): po.reload() first_item_of_po = po.get("items")[0] + existing_ordered_qty = get_ordered_qty() # add an item trans_item = json.dumps([ { @@ -168,6 +214,10 @@ class TestPurchaseOrder(unittest.TestCase): update_child_qty_rate('Purchase Order', trans_item, po.name) po.reload() + + # ordered qty should increase on row addition + self.assertEqual(get_ordered_qty(), existing_ordered_qty + 7) + # check if can remove received item trans_item = json.dumps([{'item_code' : '_Test Item', 'rate' : 200, 'qty' : 7, 'docname': po.get("items")[1].name}]) self.assertRaises(frappe.ValidationError, update_child_qty_rate, 'Purchase Order', trans_item, po.name) @@ -187,6 +237,9 @@ class TestPurchaseOrder(unittest.TestCase): self.assertEquals(len(po.get('items')), 1) self.assertEqual(po.status, 'To Receive and Bill') + # ordered qty should decrease (back to initial) on row deletion + self.assertEqual(get_ordered_qty(), existing_ordered_qty) + def test_update_child_perm(self): po = create_purchase_order(item_code= "_Test Item", qty=4) @@ -230,11 +283,13 @@ class TestPurchaseOrder(unittest.TestCase): new_item_with_tax = frappe.get_doc("Item", "Test Item with Tax") - new_item_with_tax.append("taxes", { - "item_tax_template": "Test Update Items Template - _TC", - "valid_from": nowdate() - }) - new_item_with_tax.save() + if not frappe.db.exists("Item Tax", + {"item_tax_template": "Test Update Items Template - _TC", "parent": "Test Item with Tax"}): + new_item_with_tax.append("taxes", { + "item_tax_template": "Test Update Items Template - _TC", + "valid_from": nowdate() + }) + new_item_with_tax.save() tax_template = "_Test Account Excise Duty @ 10 - _TC" item = "_Test Item Home Desktop 100" diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py index 7cf22f87e4..b530d1ab24 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py @@ -66,6 +66,7 @@ class RequestforQuotation(BuyingController): def on_cancel(self): frappe.db.set(self, 'status', 'Cancelled') + @frappe.whitelist() def get_supplier_email_preview(self, supplier): """Returns formatted email preview as string.""" rfq_suppliers = list(filter(lambda row: row.supplier == supplier, self.suppliers)) diff --git a/erpnext/buying/doctype/supplier_scorecard/test_supplier_scorecard.py b/erpnext/buying/doctype/supplier_scorecard/test_supplier_scorecard.py index 6e6eaed95d..2528240549 100644 --- a/erpnext/buying/doctype/supplier_scorecard/test_supplier_scorecard.py +++ b/erpnext/buying/doctype/supplier_scorecard/test_supplier_scorecard.py @@ -9,9 +9,7 @@ import unittest class TestSupplierScorecard(unittest.TestCase): def test_create_scorecard(self): - delete_test_scorecards() - my_doc = make_supplier_scorecard() - doc = my_doc.insert() + doc = make_supplier_scorecard().insert() self.assertEqual(doc.name, valid_scorecard[0].get("supplier")) def test_criteria_weight(self): @@ -121,7 +119,8 @@ valid_scorecard = [ { "weight":100.0, "doctype":"Supplier Scorecard Scoring Criteria", - "criteria_name":"Delivery" + "criteria_name":"Delivery", + "formula": "100" } ], "supplier":"_Test Supplier", diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 12a81c7887..36d399cf19 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -26,7 +26,8 @@ from erpnext.controllers.print_settings import set_print_templates_for_item_tabl class AccountMissingError(frappe.ValidationError): pass -force_item_fields = ("item_group", "brand", "stock_uom", "is_fixed_asset", "item_tax_rate", "pricing_rules") +force_item_fields = ("item_group", "brand", "stock_uom", "is_fixed_asset", "item_tax_rate", + "pricing_rules", "weight_per_unit", "weight_uom", "total_weight") class AccountsController(TransactionBase): def __init__(self, *args, **kwargs): @@ -516,6 +517,7 @@ class AccountsController(TransactionBase): frappe.db.sql("""delete from `tab%s` where parentfield=%s and parent = %s and allocated_amount = 0""" % (childtype, '%s', '%s'), (parentfield, self.name)) + @frappe.whitelist() def apply_shipping_rule(self): if self.shipping_rule: shipping_rule = frappe.get_doc("Shipping Rule", self.shipping_rule) @@ -536,6 +538,7 @@ class AccountsController(TransactionBase): return {} + @frappe.whitelist() def set_advances(self): """Returns list of advances against Account, Party, Reference""" @@ -920,7 +923,8 @@ class AccountsController(TransactionBase): else: for d in self.get("payment_schedule"): if d.invoice_portion: - d.payment_amount = flt(grand_total * flt(d.invoice_portion) / 100, d.precision('payment_amount')) + d.payment_amount = flt(grand_total * flt(d.invoice_portion / 100), d.precision('payment_amount')) + d.outstanding = d.payment_amount def set_due_date(self): due_dates = [d.due_date for d in self.get("payment_schedule") if d.due_date] @@ -1235,18 +1239,24 @@ def get_payment_term_details(term, posting_date=None, grand_total=None, bill_dat term_details.description = term.description term_details.invoice_portion = term.invoice_portion term_details.payment_amount = flt(term.invoice_portion) * flt(grand_total) / 100 + term_details.discount_type = term.discount_type + term_details.discount = term.discount + # term_details.discounted_amount = flt(grand_total) * (term.discount / 100) if term.discount_type == 'Percentage' else discount + term_details.outstanding = term_details.payment_amount + term_details.mode_of_payment = term.mode_of_payment + if bill_date: term_details.due_date = get_due_date(term, bill_date) + term_details.discount_date = get_discount_date(term, bill_date) elif posting_date: term_details.due_date = get_due_date(term, posting_date) + term_details.discount_date = get_discount_date(term, posting_date) if getdate(term_details.due_date) < getdate(posting_date): term_details.due_date = posting_date - term_details.mode_of_payment = term.mode_of_payment return term_details - def get_due_date(term, posting_date=None, bill_date=None): due_date = None date = bill_date or posting_date @@ -1258,6 +1268,16 @@ def get_due_date(term, posting_date=None, bill_date=None): due_date = add_months(get_last_day(date), term.credit_months) return due_date +def get_discount_date(term, posting_date=None, bill_date=None): + discount_validity = None + date = bill_date or posting_date + if term.discount_validity_based_on == "Day(s) after invoice date": + discount_validity = add_days(date, term.discount_validity) + elif term.discount_validity_based_on == "Day(s) after the end of the invoice month": + discount_validity = add_days(get_last_day(date), term.discount_validity) + elif term.discount_validity_based_on == "Month(s) after the end of the invoice month": + discount_validity = add_months(get_last_day(date), term.discount_validity) + return discount_validity def get_supplier_block_status(party_name): """ @@ -1316,25 +1336,63 @@ def set_order_defaults(parent_doctype, parent_doctype_name, child_doctype, child p_doc = frappe.get_doc(parent_doctype, parent_doctype_name) child_item = frappe.new_doc(child_doctype, p_doc, child_docname) item = frappe.get_doc("Item", trans_item.get('item_code')) + for field in ("item_code", "item_name", "description", "item_group"): - child_item.update({field: item.get(field)}) + child_item.update({field: item.get(field)}) + date_fieldname = "delivery_date" if child_doctype == "Sales Order Item" else "schedule_date" child_item.update({date_fieldname: trans_item.get(date_fieldname) or p_doc.get(date_fieldname)}) + child_item.stock_uom = item.stock_uom child_item.uom = trans_item.get("uom") or item.stock_uom + child_item.warehouse = get_item_warehouse(item, p_doc, overwrite_warehouse=True) conversion_factor = flt(get_conversion_factor(item.item_code, child_item.uom).get("conversion_factor")) child_item.conversion_factor = flt(trans_item.get('conversion_factor')) or conversion_factor + if child_doctype == "Purchase Order Item": - child_item.base_rate = 1 # Initiallize value will update in parent validation - child_item.base_amount = 1 # Initiallize value will update in parent validation + # Initialized value will update in parent validation + child_item.base_rate = 1 + child_item.base_amount = 1 if child_doctype == "Sales Order Item": child_item.warehouse = get_item_warehouse(item, p_doc, overwrite_warehouse=True) if not child_item.warehouse: frappe.throw(_("Cannot find {} for item {}. Please set the same in Item Master or Stock Settings.") .format(frappe.bold("default warehouse"), frappe.bold(item.item_code))) + set_child_tax_template_and_map(item, child_item, p_doc) add_taxes_from_tax_template(child_item, p_doc) return child_item +def validate_child_on_delete(row, parent): + """Check if partially transacted item (row) is being deleted.""" + if parent.doctype == "Sales Order": + if flt(row.delivered_qty): + frappe.throw(_("Row #{0}: Cannot delete item {1} which has already been delivered").format(row.idx, row.item_code)) + if flt(row.work_order_qty): + frappe.throw(_("Row #{0}: Cannot delete item {1} which has work order assigned to it.").format(row.idx, row.item_code)) + if flt(row.ordered_qty): + frappe.throw(_("Row #{0}: Cannot delete item {1} which is assigned to customer's purchase order.").format(row.idx, row.item_code)) + + if parent.doctype == "Purchase Order" and flt(row.received_qty): + frappe.throw(_("Row #{0}: Cannot delete item {1} which has already been received").format(row.idx, row.item_code)) + + if flt(row.billed_amt): + frappe.throw(_("Row #{0}: Cannot delete item {1} which has already been billed.").format(row.idx, row.item_code)) + +def update_bin_on_delete(row, doctype): + """Update bin for deleted item (row).""" + from erpnext.stock.stock_balance import update_bin_qty, get_reserved_qty, get_ordered_qty, get_indented_qty + qty_dict = {} + + if doctype == "Sales Order": + qty_dict["reserved_qty"] = get_reserved_qty(row.item_code, row.warehouse) + else: + if row.material_request_item: + qty_dict["indented_qty"] = get_indented_qty(row.item_code, row.warehouse) + + qty_dict["ordered_qty"] = get_ordered_qty(row.item_code, row.warehouse) + + update_bin_qty(row.item_code, row.warehouse, qty_dict) + def validate_and_delete_children(parent, data): deleted_children = [] updated_item_names = [d.get("docname") for d in data] @@ -1343,23 +1401,17 @@ def validate_and_delete_children(parent, data): deleted_children.append(item) for d in deleted_children: - if parent.doctype == "Sales Order": - if flt(d.delivered_qty): - frappe.throw(_("Row #{0}: Cannot delete item {1} which has already been delivered").format(d.idx, d.item_code)) - if flt(d.work_order_qty): - frappe.throw(_("Row #{0}: Cannot delete item {1} which has work order assigned to it.").format(d.idx, d.item_code)) - if flt(d.ordered_qty): - frappe.throw(_("Row #{0}: Cannot delete item {1} which is assigned to customer's purchase order.").format(d.idx, d.item_code)) - - if parent.doctype == "Purchase Order" and flt(d.received_qty): - frappe.throw(_("Row #{0}: Cannot delete item {1} which has already been received").format(d.idx, d.item_code)) - - if flt(d.billed_amt): - frappe.throw(_("Row #{0}: Cannot delete item {1} which has already been billed.").format(d.idx, d.item_code)) - + validate_child_on_delete(d, parent) d.cancel() d.delete() + # need to update ordered qty in Material Request first + # bin uses Material Request Items to recalculate & update + parent.update_prevdoc_status() + + for d in deleted_children: + update_bin_on_delete(d, parent.doctype) + @frappe.whitelist() def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, child_docname="items"): def check_doc_permissions(doc, perm_type='create'): @@ -1394,7 +1446,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil ) def get_new_child_item(item_row): - child_doctype = "Sales Order Item" if parent_doctype == "Sales Order" else "Purchase Order Item" + child_doctype = "Sales Order Item" if parent_doctype == "Sales Order" else "Purchase Order Item" return set_order_defaults(parent_doctype, parent_doctype_name, child_doctype, child_docname, item_row) def validate_quantity(child_item, d): diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 11ac703311..f352bae30e 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -495,7 +495,7 @@ class StockController(AccountsController): "voucher_no": self.name, "company": self.company }) - if check_if_future_sle_exists(args): + if future_sle_exists(args): create_repost_item_valuation_entry(args) elif not is_reposting_pending(): check_if_stock_and_account_balance_synced(self.posting_date, @@ -506,37 +506,42 @@ def is_reposting_pending(): {'docstatus': 1, 'status': ['in', ['Queued','In Progress']]}) -def check_if_future_sle_exists(args): - sl_entries = frappe.db.get_all("Stock Ledger Entry", +def future_sle_exists(args): + sl_entries = frappe.get_all("Stock Ledger Entry", filters={"voucher_type": args.voucher_type, "voucher_no": args.voucher_no}, fields=["item_code", "warehouse"], order_by="creation asc") - distinct_item_warehouses = list(set([(d.item_code, d.warehouse) for d in sl_entries])) + if not sl_entries: + return - sle_exists = False - for item_code, warehouse in distinct_item_warehouses: - args.update({ - "item_code": item_code, - "warehouse": warehouse - }) - if get_sle(args): - sle_exists = True - break - return sle_exists + warehouse_items_map = {} + for entry in sl_entries: + if entry.warehouse not in warehouse_items_map: + warehouse_items_map[entry.warehouse] = set() + + warehouse_items_map[entry.warehouse].add(entry.item_code) + + or_conditions = [] + for warehouse, items in warehouse_items_map.items(): + or_conditions.append( + "warehouse = '{}' and item_code in ({})".format( + warehouse, + ", ".join(frappe.db.escape(item) for item in items) + ) + ) -def get_sle(args): return frappe.db.sql(""" select name from `tabStock Ledger Entry` where - item_code=%(item_code)s - and warehouse=%(warehouse)s - and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s) + ({}) + and timestamp(posting_date, posting_time) + >= timestamp(%(posting_date)s, %(posting_time)s) and voucher_no != %(voucher_no)s and is_cancelled = 0 limit 1 - """, args) + """.format(" or ".join(or_conditions)), args) def create_repost_item_valuation_entry(args): args = frappe._dict(args) @@ -554,4 +559,4 @@ def create_repost_item_valuation_entry(args): repost_entry.allow_zero_rate = args.allow_zero_rate repost_entry.flags.ignore_links = True repost_entry.save() - repost_entry.submit() \ No newline at end of file + repost_entry.submit() diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index aab5770a94..e329b325b3 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -113,7 +113,10 @@ class calculate_taxes_and_totals(object): item.rate_with_margin, item.base_rate_with_margin = self.calculate_margin(item) if flt(item.rate_with_margin) > 0: item.rate = flt(item.rate_with_margin * (1.0 - (item.discount_percentage / 100.0)), item.precision("rate")) - item.discount_amount = item.rate_with_margin - item.rate + if not item.discount_amount: + item.discount_amount = item.rate_with_margin - item.rate + elif not item.discount_percentage: + item.rate -= item.discount_amount elif flt(item.price_list_rate) > 0: item.discount_amount = item.price_list_rate - item.rate elif flt(item.price_list_rate) > 0 and not item.discount_amount: @@ -795,7 +798,7 @@ class init_landed_taxes_and_totals(object): for d in self.doc.get(self.tax_field): if d.account_currency == company_currency: d.exchange_rate = 1 - elif not d.exchange_rate or d.exchange_rate == 1 or self.doc.posting_date: + elif not d.exchange_rate: d.exchange_rate = get_exchange_rate(self.doc.posting_date, account=d.expense_account, account_currency=d.account_currency, company=self.doc.company) diff --git a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py index 377e061fdf..d8c6fb4f90 100644 --- a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py +++ b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py @@ -11,7 +11,8 @@ from frappe.utils.file_manager import get_file, get_file_path from six.moves.urllib.parse import urlencode class LinkedInSettings(Document): - def get_authorization_url(self): + @frappe.whitelist() + def get_authorization_url(self): params = urlencode({ "response_type":"code", "client_id": self.consumer_key, @@ -35,7 +36,7 @@ class LinkedInSettings(Document): headers = { "Content-Type": "application/x-www-form-urlencoded" } - + response = self.http_post(url=url, data=body, headers=headers) response = frappe.parse_json(response.content.decode()) self.db_set("access_token", response["access_token"]) diff --git a/erpnext/crm/doctype/opportunity/opportunity.py b/erpnext/crm/doctype/opportunity/opportunity.py index 47b05f306b..23ad98a282 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.py +++ b/erpnext/crm/doctype/opportunity/opportunity.py @@ -85,6 +85,7 @@ class Opportunity(TransactionBase): self.opportunity_from = "Lead" self.party_name = lead_name + @frappe.whitelist() def declare_enquiry_lost(self, lost_reasons_list, detailed_reason=None): if not self.has_active_quotation(): frappe.db.set(self, 'status', 'Lost') @@ -248,7 +249,6 @@ def make_quotation(source_name, target_doc=None): "doctype": "Quotation", "field_map": { "opportunity_from": "quotation_to", - "opportunity_type": "order_type", "name": "enq_no", } }, diff --git a/erpnext/crm/doctype/twitter_settings/twitter_settings.py b/erpnext/crm/doctype/twitter_settings/twitter_settings.py index 976a23dfc7..1e1beab2d2 100644 --- a/erpnext/crm/doctype/twitter_settings/twitter_settings.py +++ b/erpnext/crm/doctype/twitter_settings/twitter_settings.py @@ -11,6 +11,7 @@ from frappe.utils import get_url_to_form, get_link_to_form from tweepy.error import TweepError class TwitterSettings(Document): + @frappe.whitelist() def get_authorize_url(self): callback_url = "{0}/api/method/erpnext.crm.doctype.twitter_settings.twitter_settings.callback?".format(frappe.utils.get_url()) auth = tweepy.OAuthHandler(self.consumer_key, self.get_password(fieldname="consumer_secret"), callback_url) @@ -21,12 +22,12 @@ class TwitterSettings(Document): frappe.msgprint(_("Error! Failed to get request token.")) frappe.throw(_('Invalid {0} or {1}').format(frappe.bold("Consumer Key"), frappe.bold("Consumer Secret Key"))) - + def get_access_token(self, oauth_token, oauth_verifier): auth = tweepy.OAuthHandler(self.consumer_key, self.get_password(fieldname="consumer_secret")) - auth.request_token = { + auth.request_token = { 'oauth_token' : oauth_token, - 'oauth_token_secret' : oauth_verifier + 'oauth_token_secret' : oauth_verifier } try: @@ -50,10 +51,10 @@ class TwitterSettings(Document): 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) + # 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) return tweepy.API(auth) @@ -64,7 +65,7 @@ class TwitterSettings(Document): if media: media_id = self.upload_image(media) return self.send_tweet(text, media_id) - + def upload_image(self, media): media = get_file_path(media) api = self.get_api(self.access_token, self.access_token_secret) diff --git a/erpnext/education/doctype/course_scheduling_tool/course_scheduling_tool.py b/erpnext/education/doctype/course_scheduling_tool/course_scheduling_tool.py index 97c29ab667..6a0dcf460a 100644 --- a/erpnext/education/doctype/course_scheduling_tool/course_scheduling_tool.py +++ b/erpnext/education/doctype/course_scheduling_tool/course_scheduling_tool.py @@ -13,6 +13,7 @@ from erpnext.education.utils import OverlapError class CourseSchedulingTool(Document): + @frappe.whitelist() def schedule_course(self): """Creates course schedules as per specified parameters""" diff --git a/erpnext/education/doctype/fee_schedule/fee_schedule.py b/erpnext/education/doctype/fee_schedule/fee_schedule.py index 1543acdca9..0b025c7534 100644 --- a/erpnext/education/doctype/fee_schedule/fee_schedule.py +++ b/erpnext/education/doctype/fee_schedule/fee_schedule.py @@ -52,6 +52,7 @@ class FeeSchedule(Document): self.grand_total = no_of_students*self.total_amount self.grand_total_in_words = money_in_words(self.grand_total) + @frappe.whitelist() def create_fees(self): self.db_set("fee_creation_status", "In Process") frappe.publish_realtime("fee_schedule_progress", diff --git a/erpnext/education/doctype/program_enrollment/program_enrollment.py b/erpnext/education/doctype/program_enrollment/program_enrollment.py index d18c0f9625..b282babd0f 100644 --- a/erpnext/education/doctype/program_enrollment/program_enrollment.py +++ b/erpnext/education/doctype/program_enrollment/program_enrollment.py @@ -91,6 +91,8 @@ class ProgramEnrollment(Document): (fee, fee) for fee in fee_list] msgprint(_("Fee Records Created - {0}").format(comma_and(fee_list))) + + @frappe.whitelist() def get_courses(self): return frappe.db.sql('''select course from `tabProgram Course` where parent = %s and required = 1''', (self.program), as_dict=1) diff --git a/erpnext/education/doctype/program_enrollment_tool/program_enrollment_tool.py b/erpnext/education/doctype/program_enrollment_tool/program_enrollment_tool.py index 8180102c58..5833b67f9b 100644 --- a/erpnext/education/doctype/program_enrollment_tool/program_enrollment_tool.py +++ b/erpnext/education/doctype/program_enrollment_tool/program_enrollment_tool.py @@ -14,6 +14,7 @@ class ProgramEnrollmentTool(Document): academic_term_reqd = cint(frappe.db.get_single_value('Education Settings', 'academic_term_reqd')) self.set_onload("academic_term_reqd", academic_term_reqd) + @frappe.whitelist() def get_students(self): students = [] if not self.get_students_from: @@ -49,6 +50,7 @@ class ProgramEnrollmentTool(Document): else: frappe.throw(_("No students Found")) + @frappe.whitelist() def enroll_students(self): total = len(self.students) for i, stud in enumerate(self.students): diff --git a/erpnext/education/doctype/student_attendance/student_attendance.json b/erpnext/education/doctype/student_attendance/student_attendance.json index 55384b9e53..e6e46d1c1b 100644 --- a/erpnext/education/doctype/student_attendance/student_attendance.json +++ b/erpnext/education/doctype/student_attendance/student_attendance.json @@ -10,6 +10,7 @@ "naming_series", "student", "student_name", + "student_mobile_number", "course_schedule", "student_group", "column_break_3", @@ -93,11 +94,19 @@ "options": "Student Attendance", "print_hide": 1, "read_only": 1 + }, + { + "fetch_from": "student.student_mobile_number", + "fieldname": "student_mobile_number", + "fieldtype": "Read Only", + "label": "Student Mobile Number", + "options": "Phone" } ], + "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-07-08 13:55:42.580181", + "modified": "2021-03-24 00:02:11.005895", "modified_by": "Administrator", "module": "Education", "name": "Student Attendance", diff --git a/erpnext/education/doctype/student_group_creation_tool/student_group_creation_tool.py b/erpnext/education/doctype/student_group_creation_tool/student_group_creation_tool.py index d7645e30cd..dc8667ec06 100644 --- a/erpnext/education/doctype/student_group_creation_tool/student_group_creation_tool.py +++ b/erpnext/education/doctype/student_group_creation_tool/student_group_creation_tool.py @@ -9,6 +9,7 @@ from frappe.model.document import Document from erpnext.education.doctype.student_group.student_group import get_students class StudentGroupCreationTool(Document): + @frappe.whitelist() def get_courses(self): group_list = [] @@ -42,6 +43,7 @@ class StudentGroupCreationTool(Document): return group_list + @frappe.whitelist() def create_student_groups(self): if not self.courses: frappe.throw(_("""No Student Groups created.""")) diff --git a/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.py b/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.py index b5718026c1..fdfaa1b054 100644 --- a/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.py +++ b/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.py @@ -59,9 +59,10 @@ class MpesaSettings(Document): request_amounts.append(amount) else: request_amounts = [request_amount] - + return request_amounts + @frappe.whitelist() def get_account_balance_info(self): payload = dict( reference_doctype="Mpesa Settings", @@ -198,7 +199,7 @@ def get_completed_integration_requests_info(reference_doctype, reference_docname completed_mpesa_receipt = fetch_param_value(item_response, "MpesaReceiptNumber", "Name") completed_payments.append(completed_amount) mpesa_receipts.append(completed_mpesa_receipt) - + return mpesa_receipts, completed_payments def get_account_balance(request_payload): diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py index 21f6fee79c..16c65733f0 100644 --- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py +++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py @@ -15,6 +15,7 @@ from frappe.utils import add_months, formatdate, getdate, today class PlaidSettings(Document): @staticmethod + @frappe.whitelist() def get_link_token(): plaid = PlaidConnector() return plaid.get_link_token() diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/test_plaid_settings.py b/erpnext/erpnext_integrations/doctype/plaid_settings/test_plaid_settings.py index 3c906374c4..e2243eabde 100644 --- a/erpnext/erpnext_integrations/doctype/plaid_settings/test_plaid_settings.py +++ b/erpnext/erpnext_integrations/doctype/plaid_settings/test_plaid_settings.py @@ -23,14 +23,9 @@ class TestPlaidSettings(unittest.TestCase): doc.cancel() doc.delete() - for ba in frappe.get_all("Bank Account"): - frappe.get_doc("Bank Account", ba.name).delete() - - for at in frappe.get_all("Bank Account Type"): - frappe.get_doc("Bank Account Type", at.name).delete() - - for ast in frappe.get_all("Bank Account Subtype"): - frappe.get_doc("Bank Account Subtype", ast.name).delete() + for doctype in ("Bank Account", "Bank Account Type", "Bank Account Subtype"): + for d in frappe.get_all(doctype): + frappe.delete_doc(doctype, d.name, force=True) def test_plaid_disabled(self): frappe.db.set_value("Plaid Settings", None, "enabled", 0) diff --git a/erpnext/erpnext_integrations/doctype/quickbooks_migrator/quickbooks_migrator.py b/erpnext/erpnext_integrations/doctype/quickbooks_migrator/quickbooks_migrator.py index 96a533ee10..866ea66278 100644 --- a/erpnext/erpnext_integrations/doctype/quickbooks_migrator/quickbooks_migrator.py +++ b/erpnext/erpnext_integrations/doctype/quickbooks_migrator/quickbooks_migrator.py @@ -54,6 +54,7 @@ class QuickBooksMigrator(Document): self.authorization_url = self.oauth.authorization_url(self.authorization_endpoint)[0] + @frappe.whitelist() def migrate(self): frappe.enqueue_doc("QuickBooks Migrator", "QuickBooks Migrator", "_migrate", queue="long") diff --git a/erpnext/erpnext_integrations/doctype/shopify_settings/test_shopify_settings.py b/erpnext/erpnext_integrations/doctype/shopify_settings/test_shopify_settings.py index 74ad456ea6..5f471ab2e7 100644 --- a/erpnext/erpnext_integrations/doctype/shopify_settings/test_shopify_settings.py +++ b/erpnext/erpnext_integrations/doctype/shopify_settings/test_shopify_settings.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import frappe import unittest, os, json -from frappe.utils import cstr +from frappe.utils import cstr, cint from erpnext.erpnext_integrations.connectors.shopify_connection import create_order from erpnext.erpnext_integrations.doctype.shopify_settings.sync_product import make_item from erpnext.erpnext_integrations.doctype.shopify_settings.sync_customer import create_customer @@ -13,9 +13,14 @@ from frappe.core.doctype.data_import.data_import import import_doc class ShopifySettings(unittest.TestCase): - def setUp(self): + @classmethod + def setUpClass(cls): frappe.set_user("Administrator") + cls.allow_negative_stock = cint(frappe.db.get_value('Stock Settings', None, 'allow_negative_stock')) + if not cls.allow_negative_stock: + frappe.db.set_value('Stock Settings', None, 'allow_negative_stock', 1) + # use the fixture data import_doc(frappe.get_app_path("erpnext", "erpnext_integrations/doctype/shopify_settings/test_data/custom_field.json")) @@ -24,9 +29,15 @@ class ShopifySettings(unittest.TestCase): frappe.reload_doctype("Delivery Note") frappe.reload_doctype("Sales Invoice") - self.setup_shopify() + cls.setup_shopify() - def setup_shopify(self): + @classmethod + def tearDownClass(cls): + if not cls.allow_negative_stock: + frappe.db.set_value('Stock Settings', None, 'allow_negative_stock', 0) + + @classmethod + def setup_shopify(cls): shopify_settings = frappe.get_doc("Shopify Settings") shopify_settings.taxes = [] @@ -56,21 +67,20 @@ class ShopifySettings(unittest.TestCase): "delivery_note_series": "DN-" }).save(ignore_permissions=True) - self.shopify_settings = shopify_settings + cls.shopify_settings = shopify_settings def test_order(self): - ### Create Customer ### + # Create Customer with open (os.path.join(os.path.dirname(__file__), "test_data", "shopify_customer.json")) as shopify_customer: shopify_customer = json.load(shopify_customer) create_customer(shopify_customer.get("customer"), self.shopify_settings) - ### Create Item ### + # Create Item with open (os.path.join(os.path.dirname(__file__), "test_data", "shopify_item.json")) as shopify_item: shopify_item = json.load(shopify_item) make_item("_Test Warehouse - _TC", shopify_item.get("product")) - - ### Create Order ### + # Create Order with open (os.path.join(os.path.dirname(__file__), "test_data", "shopify_order.json")) as shopify_order: shopify_order = json.load(shopify_order) @@ -80,17 +90,17 @@ class ShopifySettings(unittest.TestCase): self.assertEqual(cstr(shopify_order.get("order").get("id")), sales_order.shopify_order_id) - #check for customer + # Check for customer shopify_order_customer_id = cstr(shopify_order.get("order").get("customer").get("id")) sales_order_customer_id = frappe.get_value("Customer", sales_order.customer, "shopify_customer_id") self.assertEqual(shopify_order_customer_id, sales_order_customer_id) - #check sales invoice + # Check sales invoice sales_invoice = frappe.get_doc("Sales Invoice", {"shopify_order_id": sales_order.shopify_order_id}) self.assertEqual(sales_invoice.rounded_total, sales_order.rounded_total) - #check delivery note + # Check delivery note delivery_note_count = frappe.db.sql("""select count(*) from `tabDelivery Note` where shopify_order_id = %s""", sales_order.shopify_order_id)[0][0] diff --git a/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.py b/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.py index 462685f5e7..907a22333b 100644 --- a/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.py +++ b/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.py @@ -594,18 +594,22 @@ class TallyMigration(Document): frappe.db.set_value("Price List", "Tally Price List", "enabled", 0) frappe.flags.in_migrate = False + @frappe.whitelist() def process_master_data(self): self.set_status("Processing Master Data") frappe.enqueue_doc(self.doctype, self.name, "_process_master_data", queue="long", timeout=3600) + @frappe.whitelist() def import_master_data(self): self.set_status("Importing Master Data") frappe.enqueue_doc(self.doctype, self.name, "_import_master_data", queue="long", timeout=3600) + @frappe.whitelist() def process_day_book_data(self): self.set_status("Processing Day Book Data") frappe.enqueue_doc(self.doctype, self.name, "_process_day_book_data", queue="long", timeout=3600) + @frappe.whitelist() def import_day_book_data(self): self.set_status("Importing Day Book Data") frappe.enqueue_doc(self.doctype, self.name, "_import_day_book_data", queue="long", timeout=3600) diff --git a/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.py b/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.py index 325c2094fb..cbf89ee3bd 100644 --- a/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.py +++ b/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.py @@ -54,6 +54,7 @@ class ClinicalProcedure(Document): def set_title(self): self.title = _('{0} - {1}').format(self.patient_name or self.patient, self.procedure_template)[:100] + @frappe.whitelist() def complete_procedure(self): if self.consume_stock and self.items: stock_entry = make_stock_entry(self) @@ -96,6 +97,7 @@ class ClinicalProcedure(Document): if self.consume_stock and self.items: return stock_entry + @frappe.whitelist() def start_procedure(self): allow_start = self.set_actual_qty() if allow_start: @@ -116,6 +118,7 @@ class ClinicalProcedure(Document): return allow_start + @frappe.whitelist() def make_material_receipt(self, submit=False): stock_entry = frappe.new_doc('Stock Entry') diff --git a/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.py b/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.py index e7319085e4..3a299eda26 100644 --- a/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.py +++ b/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.py @@ -14,6 +14,7 @@ class InpatientMedicationEntry(Document): def validate(self): self.validate_medication_orders() + @frappe.whitelist() def get_medication_orders(self): # pull inpatient medication orders based on selected filters orders = get_pending_medication_orders(self) diff --git a/erpnext/healthcare/doctype/inpatient_medication_order/inpatient_medication_order.py b/erpnext/healthcare/doctype/inpatient_medication_order/inpatient_medication_order.py index 33cbbec812..b379e98fe1 100644 --- a/erpnext/healthcare/doctype/inpatient_medication_order/inpatient_medication_order.py +++ b/erpnext/healthcare/doctype/inpatient_medication_order/inpatient_medication_order.py @@ -57,6 +57,7 @@ class InpatientMedicationOrder(Document): self.db_set('status', status) + @frappe.whitelist() def add_order_entries(self, order): if order.get('drug_code'): dosage = frappe.get_doc('Prescription Dosage', order.get('dosage')) diff --git a/erpnext/healthcare/doctype/inpatient_record/inpatient_record.json b/erpnext/healthcare/doctype/inpatient_record/inpatient_record.json index 5ced845c1b..aaf0e855d4 100644 --- a/erpnext/healthcare/doctype/inpatient_record/inpatient_record.json +++ b/erpnext/healthcare/doctype/inpatient_record/inpatient_record.json @@ -53,7 +53,7 @@ "discharge_ordered_date", "discharge_practitioner", "discharge_encounter", - "discharge_date", + "discharge_datetime", "cb_discharge", "discharge_instructions", "followup_date", @@ -404,14 +404,15 @@ "permlevel": 1 }, { - "fieldname": "discharge_date", - "fieldtype": "Date", + "fieldname": "discharge_datetime", + "fieldtype": "Datetime", "label": "Discharge Date", "read_only": 1 } ], + "index_web_pages_for_search": 1, "links": [], - "modified": "2020-05-21 02:26:22.144575", + "modified": "2021-03-18 14:44:11.689956", "modified_by": "Administrator", "module": "Healthcare", "name": "Inpatient Record", diff --git a/erpnext/healthcare/doctype/inpatient_record/inpatient_record.py b/erpnext/healthcare/doctype/inpatient_record/inpatient_record.py index 88d7f0b233..f4d1eaf2e3 100644 --- a/erpnext/healthcare/doctype/inpatient_record/inpatient_record.py +++ b/erpnext/healthcare/doctype/inpatient_record/inpatient_record.py @@ -53,12 +53,15 @@ class InpatientRecord(Document): + """ {0}""".format(ip_record[0].name)) frappe.throw(msg) + @frappe.whitelist() def admit(self, service_unit, check_in, expected_discharge=None): admit_patient(self, service_unit, check_in, expected_discharge) + @frappe.whitelist() def discharge(self): discharge_patient(self) + @frappe.whitelist() def transfer(self, service_unit, check_in, leave_from): if leave_from: patient_leave_service_unit(self, check_in, leave_from) @@ -151,7 +154,7 @@ def check_out_inpatient(inpatient_record): def discharge_patient(inpatient_record): validate_inpatient_invoicing(inpatient_record) - inpatient_record.discharge_date = today() + inpatient_record.discharge_datetime = now_datetime() inpatient_record.status = "Discharged" inpatient_record.save(ignore_permissions = True) diff --git a/erpnext/healthcare/doctype/patient/patient.py b/erpnext/healthcare/doctype/patient/patient.py index 8603f974c3..789d452c07 100644 --- a/erpnext/healthcare/doctype/patient/patient.py +++ b/erpnext/healthcare/doctype/patient/patient.py @@ -111,6 +111,7 @@ class Patient(Document): age_str = str(age.years) + ' ' + _("Years(s)") + ' ' + str(age.months) + ' ' + _("Month(s)") + ' ' + str(age.days) + ' ' + _("Day(s)") return age_str + @frappe.whitelist() def invoice_patient_registration(self): if frappe.db.get_single_value('Healthcare Settings', 'registration_fee'): company = frappe.defaults.get_user_default('company') diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py index 1f76cd624c..cdd4ad39c8 100755 --- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py +++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py @@ -113,6 +113,7 @@ class PatientAppointment(Document): if fee_validity: frappe.msgprint(_('{0} has fee validity till {1}').format(self.patient, fee_validity.valid_till)) + @frappe.whitelist() def get_therapy_types(self): if not self.therapy_plan: return diff --git a/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py index 2e8c994c3d..887d58a2e0 100644 --- a/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py +++ b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py @@ -34,6 +34,7 @@ class PatientHistorySettings(Document): frappe.throw(_('Row #{0}: Field {1} in Document Type {2} is not a Date / Datetime field.').format( entry.idx, frappe.bold(entry.date_fieldname), frappe.bold(entry.document_type))) + @frappe.whitelist() def get_doctype_fields(self, document_type, fields): multicheck_fields = [] doc_fields = frappe.get_meta(document_type).fields @@ -49,6 +50,7 @@ class PatientHistorySettings(Document): return multicheck_fields + @frappe.whitelist() def get_date_field_for_dt(self, document_type): meta = frappe.get_meta(document_type) date_fields = meta.get('fields', { diff --git a/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py b/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py index ac01c604dd..e209660434 100644 --- a/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py +++ b/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py @@ -33,6 +33,7 @@ class TherapyPlan(Document): self.db_set('total_sessions', total_sessions) self.db_set('total_sessions_completed', total_sessions_completed) + @frappe.whitelist() def set_therapy_details_from_template(self): # Add therapy types in the child table self.set('therapy_plan_details', []) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 4b3597afd7..c2798a36b6 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -195,6 +195,10 @@ sounds = [ {"name": "call-disconnect", "src": "/assets/erpnext/sounds/call-disconnect.mp3", "volume": 0.2}, ] +has_upload_permission = { + "Employee": "erpnext.hr.doctype.employee.employee.has_upload_permission" +} + has_website_permission = { "Sales Order": "erpnext.controllers.website_list_for_contact.has_website_permission", "Quotation": "erpnext.controllers.website_list_for_contact.has_website_permission", @@ -324,6 +328,7 @@ scheduler_events = { "erpnext.hr.doctype.shift_type.shift_type.process_auto_attendance_for_all_shifts", "erpnext.support.doctype.issue.issue.set_service_level_agreement_variance", "erpnext.erpnext_integrations.connectors.shopify_connection.sync_old_orders", + "erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.repost_entries" ], "daily": [ "erpnext.stock.reorder_item.reorder_item", diff --git a/erpnext/hr/doctype/employee/employee.py b/erpnext/hr/doctype/employee/employee.py index d0e7d0537b..629bc57118 100755 --- a/erpnext/hr/doctype/employee/employee.py +++ b/erpnext/hr/doctype/employee/employee.py @@ -8,7 +8,7 @@ from frappe.utils import getdate, validate_email_address, today, add_years, form 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 + 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 @@ -66,7 +66,7 @@ class Employee(NestedSet): def validate_user_details(self): data = frappe.db.get_value('User', self.user_id, ['enabled', 'user_image'], as_dict=1) - if data.get("user_image"): + if data.get("user_image") and self.image == '': self.image = data.get("user_image") self.validate_for_enabled_user_id(data.get("enabled", 0)) self.validate_duplicate_user_id() @@ -501,3 +501,10 @@ def has_user_permission_for_employee(user_name, employee_name): 'allow': 'Employee', 'for_value': employee_name }) + +def has_upload_permission(doc, ptype='read', user=None): + if not user: + user = frappe.session.user + if get_doc_permissions(doc, user=user, ptype=ptype).get(ptype): + return True + return doc.user_id == user \ No newline at end of file diff --git a/erpnext/hr/doctype/employee_advance/employee_advance.json b/erpnext/hr/doctype/employee_advance/employee_advance.json index cf6b5404ec..04f98d1441 100644 --- a/erpnext/hr/doctype/employee_advance/employee_advance.json +++ b/erpnext/hr/doctype/employee_advance/employee_advance.json @@ -181,7 +181,6 @@ "read_only": 1 }, { - "default": "Company:company:default_currency", "depends_on": "eval:(doc.docstatus==1 || doc.employee)", "fieldname": "currency", "fieldtype": "Link", @@ -201,7 +200,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2020-11-25 12:01:55.980721", + "modified": "2021-03-31 14:42:47.321368", "modified_by": "Administrator", "module": "HR", "name": "Employee Advance", diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.py b/erpnext/hr/doctype/expense_claim/expense_claim.py index bf893d5fab..e7bb6dcd48 100644 --- a/erpnext/hr/doctype/expense_claim/expense_claim.py +++ b/erpnext/hr/doctype/expense_claim/expense_claim.py @@ -211,6 +211,7 @@ class ExpenseClaim(AccountsController): self.total_claimed_amount += flt(d.amount) self.total_sanctioned_amount += flt(d.sanctioned_amount) + @frappe.whitelist() def calculate_taxes(self): self.total_taxes_and_charges = 0 for tax in self.taxes: diff --git a/erpnext/hr/doctype/job_applicant/test_job_applicant.py b/erpnext/hr/doctype/job_applicant/test_job_applicant.py index 6d275c82d9..872834230e 100644 --- a/erpnext/hr/doctype/job_applicant/test_job_applicant.py +++ b/erpnext/hr/doctype/job_applicant/test_job_applicant.py @@ -13,11 +13,21 @@ class TestJobApplicant(unittest.TestCase): def create_job_applicant(**args): args = frappe._dict(args) - job_applicant = frappe.get_doc({ - "doctype": "Job Applicant", + + filters = { "applicant_name": args.applicant_name or "_Test Applicant", "email_id": args.email_id or "test_applicant@example.com", + } + + if frappe.db.exists("Job Applicant", filters): + return frappe.get_doc("Job Applicant", filters) + + job_applicant = frappe.get_doc({ + "doctype": "Job Applicant", "status": args.status or "Open" }) + + job_applicant.update(filters) job_applicant.save() - return job_applicant \ No newline at end of file + + return job_applicant diff --git a/erpnext/hr/doctype/job_offer/test_job_offer.py b/erpnext/hr/doctype/job_offer/test_job_offer.py index 8886596450..690a692ddc 100644 --- a/erpnext/hr/doctype/job_offer/test_job_offer.py +++ b/erpnext/hr/doctype/job_offer/test_job_offer.py @@ -13,14 +13,15 @@ from erpnext.hr.doctype.staffing_plan.test_staffing_plan import make_company class TestJobOffer(unittest.TestCase): def test_job_offer_creation_against_vacancies(self): - create_staffing_plan(staffing_details=[{ - "designation": "Designer", + frappe.db.set_value("HR Settings", None, "check_vacancies", 1) + job_applicant = create_job_applicant(email_id="test_job_offer@example.com") + job_offer = create_job_offer(job_applicant=job_applicant.name, designation="UX Designer") + + create_staffing_plan(name='Test No Vacancies', staffing_details=[{ + "designation": "UX Designer", "vacancies": 0, "estimated_cost_per_position": 5000 }]) - frappe.db.set_value("HR Settings", None, "check_vacancies", 1) - job_applicant = create_job_applicant(email_id="test_job_offer@example.com") - job_offer = create_job_offer(job_applicant=job_applicant.name, designation="Researcher") self.assertRaises(frappe.ValidationError, job_offer.submit) # test creation of job offer when vacancies are not present diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation.py b/erpnext/hr/doctype/leave_allocation/leave_allocation.py index 69d605d063..11302cad75 100755 --- a/erpnext/hr/doctype/leave_allocation/leave_allocation.py +++ b/erpnext/hr/doctype/leave_allocation/leave_allocation.py @@ -99,6 +99,7 @@ class LeaveAllocation(Document): .format(formatdate(future_allocation[0].from_date), future_allocation[0].name), BackDatedAllocationError) + @frappe.whitelist() def set_total_leaves_allocated(self): self.unused_leaves = get_carry_forwarded_leaves(self.employee, self.leave_type, self.from_date, self.carry_forward) diff --git a/erpnext/hr/doctype/leave_encashment/leave_encashment.json b/erpnext/hr/doctype/leave_encashment/leave_encashment.json index 83eeae3adb..dcb587407d 100644 --- a/erpnext/hr/doctype/leave_encashment/leave_encashment.json +++ b/erpnext/hr/doctype/leave_encashment/leave_encashment.json @@ -130,7 +130,6 @@ "read_only": 1 }, { - "default": "Company:company:default_currency", "depends_on": "eval:(doc.docstatus==1 || doc.employee)", "fieldname": "currency", "fieldtype": "Link", @@ -155,7 +154,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2020-11-25 11:56:06.777241", + "modified": "2021-03-31 14:45:27.948207", "modified_by": "Administrator", "module": "HR", "name": "Leave Encashment", diff --git a/erpnext/hr/doctype/leave_encashment/leave_encashment.py b/erpnext/hr/doctype/leave_encashment/leave_encashment.py index 4c1a46522f..e041b7fb8f 100644 --- a/erpnext/hr/doctype/leave_encashment/leave_encashment.py +++ b/erpnext/hr/doctype/leave_encashment/leave_encashment.py @@ -63,6 +63,7 @@ class LeaveEncashment(Document): frappe.db.get_value('Leave Allocation', self.leave_allocation, 'total_leaves_encashed') - self.encashable_days) self.create_leave_ledger_entry(submit=False) + @frappe.whitelist() def get_leave_details_for_encashment(self): salary_structure = get_assigned_salary_structure(self.employee, self.encashment_date or getdate(nowdate())) if not salary_structure: diff --git a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py index 4064c56e44..462b81df1d 100644 --- a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py +++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py @@ -36,6 +36,7 @@ class LeavePolicyAssignment(Document): frappe.throw(_("Leave Policy: {0} already assigned for Employee {1} for period {2} to {3}") .format(bold(self.leave_policy), bold(self.employee), bold(formatdate(self.effective_from)), bold(formatdate(self.effective_to)))) + @frappe.whitelist() def grant_leave_alloc_for_employee(self): if self.leaves_allocated: frappe.throw(_("Leave already have been assigned for this Leave Policy Assignment")) diff --git a/erpnext/hr/doctype/shift_type/shift_type.py b/erpnext/hr/doctype/shift_type/shift_type.py index 054e7e3688..d5fdda8094 100644 --- a/erpnext/hr/doctype/shift_type/shift_type.py +++ b/erpnext/hr/doctype/shift_type/shift_type.py @@ -15,6 +15,7 @@ from erpnext.hr.doctype.attendance.attendance import mark_attendance from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee class ShiftType(Document): + @frappe.whitelist() def process_auto_attendance(self): if not cint(self.enable_auto_attendance) or not self.process_attendance_after or not self.last_sync_of_checkin: return diff --git a/erpnext/hr/doctype/staffing_plan/staffing_plan.py b/erpnext/hr/doctype/staffing_plan/staffing_plan.py index 5b84d00bd6..533149a823 100644 --- a/erpnext/hr/doctype/staffing_plan/staffing_plan.py +++ b/erpnext/hr/doctype/staffing_plan/staffing_plan.py @@ -39,6 +39,7 @@ class StaffingPlan(Document): detail.current_count = designation_counts['employee_count'] detail.current_openings = designation_counts['job_openings'] + detail.total_estimated_cost = 0 if detail.number_of_positions > 0: if detail.vacancies > 0 and detail.estimated_cost_per_position: detail.total_estimated_cost = cint(detail.vacancies) * flt(detail.estimated_cost_per_position) diff --git a/erpnext/hr/report/recruitment_analytics/recruitment_analytics.py b/erpnext/hr/report/recruitment_analytics/recruitment_analytics.py index e961114ac2..f5fece8049 100644 --- a/erpnext/hr/report/recruitment_analytics/recruitment_analytics.py +++ b/erpnext/hr/report/recruitment_analytics/recruitment_analytics.py @@ -31,7 +31,7 @@ def get_columns(): "fieldtype": "Link", "fieldname": "job_opening", "options": "Job Opening", - "width": 100 + "width": 105 }, { "label": _("Job Applicant"), @@ -44,13 +44,13 @@ def get_columns(): "label": _("Applicant name"), "fieldtype": "data", "fieldname": "applicant_name", - "width": 120 + "width": 130 }, { "label": _("Application Status"), "fieldtype": "Data", "fieldname": "application_status", - "width": 100 + "width": 150 }, { "label": _("Job Offer"), diff --git a/erpnext/loan_management/doctype/loan/test_loan.py b/erpnext/loan_management/doctype/loan/test_loan.py index 13a209418d..4b9a89486a 100644 --- a/erpnext/loan_management/doctype/loan/test_loan.py +++ b/erpnext/loan_management/doctype/loan/test_loan.py @@ -275,6 +275,11 @@ class TestLoan(unittest.TestCase): frappe.db.sql(""" UPDATE `tabLoan Security Price` SET loan_security_price = 250 where loan_security='Test Security 2'""") + create_process_loan_security_shortfall() + loan_security_shortfall = frappe.get_doc("Loan Security Shortfall", {"loan": loan.name}) + self.assertEquals(loan_security_shortfall.status, "Completed") + self.assertEquals(loan_security_shortfall.shortfall_amount, 0) + def test_loan_security_unpledge(self): pledge = [{ "loan_security": "Test Security 1", diff --git a/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py b/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py index 6469806884..b5e78981d0 100644 --- a/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py +++ b/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py @@ -55,6 +55,9 @@ def check_for_ltv_shortfall(process_loan_security_shortfall): 'total_interest_payable', 'disbursed_amount', 'status'], filters={'status': ('in',['Disbursed','Partially Disbursed']), 'is_secured_loan': 1}) + loan_shortfall_map = frappe._dict(frappe.get_all("Loan Security Shortfall", + fields=["loan", "name"], filters={"status": "Pending"}, as_list=1)) + loan_security_map = {} for loan in loans: @@ -71,14 +74,19 @@ def check_for_ltv_shortfall(process_loan_security_shortfall): for security, qty in pledged_securities.items(): if not ltv_ratio: ltv_ratio = get_ltv_ratio(security) - security_value += loan_security_price_map.get(security) * qty + security_value += flt(loan_security_price_map.get(security)) * flt(qty) - current_ratio = (outstanding_amount/security_value) * 100 + current_ratio = (outstanding_amount/security_value) * 100 if security_value else 0 if current_ratio > ltv_ratio: shortfall_amount = outstanding_amount - ((security_value * ltv_ratio) / 100) create_loan_security_shortfall(loan.name, outstanding_amount, security_value, shortfall_amount, process_loan_security_shortfall) + elif loan_shortfall_map.get(loan.name): + shortfall_amount = outstanding_amount - ((security_value * ltv_ratio) / 100) + if shortfall_amount <= 0: + shortfall = loan_shortfall_map.get(loan.name) + update_pending_shortfall(shortfall) def create_loan_security_shortfall(loan, loan_amount, security_value, shortfall_amount, process_loan_security_shortfall): existing_shortfall = frappe.db.get_value("Loan Security Shortfall", {"loan": loan, "status": "Pending"}, "name") @@ -101,3 +109,11 @@ def get_ltv_ratio(loan_security): ltv_ratio = frappe.db.get_value('Loan Security Type', loan_security_type, 'loan_to_value_ratio') return ltv_ratio +def update_pending_shortfall(shortfall): + # Get all pending loan security shortfall + frappe.db.set_value("Loan Security Shortfall", shortfall, + { + "status": "Completed", + "shortfall_amount": 0 + }) + diff --git a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py index cba6a2d014..0aefe19c8d 100644 --- a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py +++ b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py @@ -12,6 +12,7 @@ from erpnext.stock.utils import get_valid_serial_nos from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee class MaintenanceSchedule(TransactionBase): + @frappe.whitelist() def generate_schedule(self): self.set('schedules', []) frappe.db.sql("""delete from `tabMaintenance Schedule Detail` diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 03beedb663..979f7ca312 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -113,6 +113,7 @@ class BOM(WebsiteGenerator): return item + @frappe.whitelist() def get_routing(self): if self.routing: self.set("operations", []) @@ -145,6 +146,7 @@ class BOM(WebsiteGenerator): if not item.get(r): item.set(r, ret[r]) + @frappe.whitelist() def get_bom_material_detail(self, args=None): """ Get raw material details like uom, desc and rate""" if not args: @@ -210,6 +212,7 @@ class BOM(WebsiteGenerator): .format(self.rm_cost_as_per, arg["item_code"]), alert=True) return flt(rate) * flt(self.plc_conversion_rate or 1) / (self.conversion_rate or 1) + @frappe.whitelist() def update_cost(self, update_parent=True, from_child_bom=False, save=True): if self.docstatus == 2: return diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index 662a06b1ee..8aa0ffd774 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -164,6 +164,7 @@ class JobCard(Document): "time_in_mins": time_diff_in_minutes(row.planned_end_time, row.planned_start_time), }) + @frappe.whitelist() def get_required_items(self): if not self.get('work_order'): return @@ -255,6 +256,9 @@ class JobCard(Document): data.actual_operation_time = time_in_mins data.actual_start_time = time_data[0].start_time if time_data else None data.actual_end_time = time_data[0].end_time if time_data else None + if data.get("workstation") != self.workstation: + # workstations can change in a job card + data.workstation = self.workstation wo.flags.ignore_validate_update_after_submit = True wo.update_operation_status() diff --git a/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json b/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json index f93b244a50..6c60bbde86 100644 --- a/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json +++ b/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json @@ -11,10 +11,14 @@ "from_warehouse", "warehouse", "column_break_4", + "required_bom_qty", "quantity", "uom", "projected_qty", "actual_qty", + "ordered_qty", + "reserved_qty_for_production", + "safety_stock", "item_details", "description", "min_order_qty", @@ -129,11 +133,40 @@ "fieldtype": "Link", "label": "From Warehouse", "options": "Warehouse" + }, + { + "fetch_from": "item_code.safety_stock", + "fieldname": "safety_stock", + "fieldtype": "Float", + "label": "Safety Stock", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "ordered_qty", + "fieldtype": "Float", + "label": "Ordered Qty", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "reserved_qty_for_production", + "fieldtype": "Float", + "label": "Reserved Qty for Production", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "required_bom_qty", + "fieldtype": "Float", + "label": "Required Qty as per BOM", + "no_copy": 1, + "read_only": 1 } ], "istable": 1, "links": [], - "modified": "2020-02-03 12:22:29.913302", + "modified": "2021-03-26 12:41:13.013149", "modified_by": "Administrator", "module": "Manufacturing", "name": "Material Request Plan Item", diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js index b723387a09..288c1d0cd6 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.js +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js @@ -25,6 +25,16 @@ frappe.ui.form.on('Production Plan', { } }); + frm.set_query('material_request', 'material_requests', function() { + return { + filters: { + material_request_type: "Manufacture", + docstatus: 1, + status: ["!=", "Stopped"], + } + }; + }); + frm.fields_dict['po_items'].grid.get_field('item_code').get_query = function(doc) { return { query: "erpnext.controllers.queries.item_query", @@ -251,7 +261,8 @@ frappe.ui.form.on('Production Plan', { get_items_for_material_requests: function(frm, warehouses) { const set_fields = ['actual_qty', 'item_code','item_name', 'description', 'uom', 'from_warehouse', - 'min_order_qty', 'quantity', 'sales_order', 'warehouse', 'projected_qty', 'material_request_type']; + 'min_order_qty', 'required_bom_qty', 'quantity', 'sales_order', 'warehouse', 'projected_qty', 'ordered_qty', + 'reserved_qty_for_production', 'material_request_type']; frappe.call({ method: "erpnext.manufacturing.doctype.production_plan.production_plan.get_items_for_material_requests", @@ -369,4 +380,4 @@ cur_frm.fields_dict['sales_orders'].grid.get_field("sales_order").get_query = fu ['Sales Order','docstatus', '=' ,1] ] } -}; \ No newline at end of file +}; diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.json b/erpnext/manufacturing/doctype/production_plan/production_plan.json index 7daf7069f3..f11470086a 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.json +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.json @@ -32,6 +32,7 @@ "material_request_planning", "include_non_stock_items", "include_subcontracted_items", + "include_safety_stock", "ignore_existing_ordered_qty", "column_break_25", "for_warehouse", @@ -309,13 +310,19 @@ "fieldtype": "Select", "label": "Sales Order Status", "options": "\nTo Deliver and Bill\nTo Bill\nTo Deliver" + }, + { + "default": "0", + "fieldname": "include_safety_stock", + "fieldtype": "Check", + "label": "Include Safety Stock in Required Qty Calculation" } ], "icon": "fa fa-calendar", "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-11-10 18:01:54.991970", + "modified": "2021-03-08 11:17:25.470147", "modified_by": "Administrator", "module": "Manufacturing", "name": "Production Plan", diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 8f9dd05217..cef2d8be7a 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -29,6 +29,7 @@ class ProductionPlan(Document): if not flt(d.planned_qty): frappe.throw(_("Please enter Planned Qty for Item {0} at row {1}").format(d.item_code, d.idx)) + @frappe.whitelist() def get_open_sales_orders(self): """ Pull sales orders which are pending to deliver based on criteria selected""" open_so = get_sales_orders(self) @@ -50,6 +51,7 @@ class ProductionPlan(Document): 'grand_total': data.base_grand_total }) + @frappe.whitelist() def get_pending_material_requests(self): """ Pull Material Requests that are pending based on criteria selected""" mr_filter = item_filter = "" @@ -68,7 +70,7 @@ class ProductionPlan(Document): from `tabMaterial Request` mr, `tabMaterial Request Item` mr_item where mr_item.parent = mr.name and mr.material_request_type = "Manufacture" - and mr.docstatus = 1 and mr.company = %(company)s + and mr.docstatus = 1 and mr.status != "Stopped" and mr.company = %(company)s and mr_item.qty > ifnull(mr_item.ordered_qty,0) {0} {1} and (exists (select name from `tabBOM` bom where bom.item=mr_item.item_code and bom.is_active = 1)) @@ -92,6 +94,7 @@ class ProductionPlan(Document): 'material_request_date': data.transaction_date }) + @frappe.whitelist() def get_items(self): if self.get_items_from == "Sales Order": self.get_so_items() @@ -219,6 +222,7 @@ class ProductionPlan(Document): filters = {'docstatus': 0, 'production_plan': ("=", self.name)}): frappe.delete_doc('Work Order', d.name) + @frappe.whitelist() def set_status(self, close=None): self.status = { 0: 'Draft', @@ -302,6 +306,7 @@ class ProductionPlan(Document): return item_dict + @frappe.whitelist() def make_work_order(self): wo_list = [] self.validate_data() @@ -367,6 +372,7 @@ class ProductionPlan(Document): except OverProductionError: pass + @frappe.whitelist() def make_material_request(self): '''Create Material Requests grouped by Sales Order and Material Request Type''' material_request_list = [] @@ -434,12 +440,14 @@ def download_raw_materials(doc): if isinstance(doc, string_types): doc = frappe._dict(json.loads(doc)) - item_list = [['Item Code', 'Description', 'Stock UOM', 'Required Qty', 'Warehouse', - 'projected Qty', 'Actual Qty']] + item_list = [['Item Code', 'Description', 'Stock UOM', 'Warehouse', 'Required Qty as per BOM', + 'Projected Qty', 'Actual Qty', 'Ordered Qty', 'Reserved Qty for Production', + 'Safety Stock', 'Required Qty']] for d in get_items_for_material_requests(doc): - item_list.append([d.get('item_code'), d.get('description'), d.get('stock_uom'), d.get('quantity'), - d.get('warehouse'), d.get('projected_qty'), d.get('actual_qty')]) + item_list.append([d.get('item_code'), d.get('description'), d.get('stock_uom'), d.get('warehouse'), + d.get('required_bom_qty'), d.get('projected_qty'), d.get('actual_qty'), d.get('ordered_qty'), + d.get('reserved_qty_for_production'), d.get('safety_stock'), d.get('quantity')]) if not doc.get('for_warehouse'): row = {'item_code': d.get('item_code')} @@ -447,8 +455,9 @@ def download_raw_materials(doc): if d.get("warehouse") == bin_dict.get('warehouse'): continue - item_list.append(['', '', '', '', bin_dict.get('warehouse'), - bin_dict.get('projected_qty', 0), bin_dict.get('actual_qty', 0)]) + item_list.append(['', '', '', bin_dict.get('warehouse'), '', + bin_dict.get('projected_qty', 0), bin_dict.get('actual_qty', 0), + bin_dict.get('ordered_qty', 0), bin_dict.get('reserved_qty_for_production', 0)]) build_csv_response(item_list, doc.name) @@ -482,7 +491,7 @@ def get_subitems(doc, data, item_details, bom_no, company, include_non_stock_ite ifnull(%(parent_qty)s * sum(bom_item.stock_qty/ifnull(bom.quantity, 1)) * %(planned_qty)s, 0) as qty, item.is_sub_contracted_item as is_sub_contracted, bom_item.source_warehouse, item.default_bom as default_bom, bom_item.description as description, - bom_item.stock_uom as stock_uom, item.min_order_qty as min_order_qty, + bom_item.stock_uom as stock_uom, item.min_order_qty as min_order_qty, item.safety_stock as safety_stock, item_default.default_warehouse, item.purchase_uom, item_uom.conversion_factor FROM `tabBOM Item` bom_item @@ -518,8 +527,8 @@ def get_subitems(doc, data, item_details, bom_no, company, include_non_stock_ite include_non_stock_items, include_subcontracted_items, d.qty) return item_details -def get_material_request_items(row, sales_order, - company, ignore_existing_ordered_qty, warehouse, bin_dict): +def get_material_request_items(row, sales_order, company, + ignore_existing_ordered_qty, include_safety_stock, warehouse, bin_dict): total_qty = row['qty'] required_qty = 0 @@ -543,17 +552,24 @@ def get_material_request_items(row, sales_order, if frappe.db.get_value("UOM", row['purchase_uom'], "must_be_whole_number"): required_qty = ceil(required_qty) + if include_safety_stock: + required_qty += flt(row['safety_stock']) + if required_qty > 0: return { 'item_code': row.item_code, 'item_name': row.item_name, 'quantity': required_qty, + 'required_bom_qty': total_qty, 'description': row.description, 'stock_uom': row.get("stock_uom"), 'warehouse': warehouse or row.get('source_warehouse') \ or row.get('default_warehouse') or item_group_defaults.get("default_warehouse"), + 'safety_stock': row.safety_stock, 'actual_qty': bin_dict.get("actual_qty", 0), 'projected_qty': bin_dict.get("projected_qty", 0), + 'ordered_qty': bin_dict.get("ordered_qty", 0), + 'reserved_qty_for_production': bin_dict.get("reserved_qty_for_production", 0), 'min_order_qty': row['min_order_qty'], 'material_request_type': row.get("default_material_request_type"), 'sales_order': sales_order, @@ -620,7 +636,8 @@ def get_bin_details(row, company, for_warehouse=None, all_warehouse=False): """.format(lft, rgt, company) return frappe.db.sql(""" select ifnull(sum(projected_qty),0) as projected_qty, - ifnull(sum(actual_qty),0) as actual_qty, warehouse from `tabBin` + ifnull(sum(actual_qty),0) as actual_qty, ifnull(sum(ordered_qty),0) as ordered_qty, + ifnull(sum(reserved_qty_for_production),0) as reserved_qty_for_production, warehouse from `tabBin` where item_code = %(item_code)s {conditions} group by item_code, warehouse """.format(conditions=conditions), { "item_code": row['item_code'] }, as_dict=1) @@ -660,6 +677,7 @@ def get_items_for_material_requests(doc, warehouses=None): company = doc.get('company') ignore_existing_ordered_qty = doc.get('ignore_existing_ordered_qty') + include_safety_stock = doc.get('include_safety_stock') so_item_details = frappe._dict() for data in po_items: @@ -711,6 +729,7 @@ def get_items_for_material_requests(doc, warehouses=None): 'description' : item_master.description, 'stock_uom' : item_master.stock_uom, 'conversion_factor' : conversion_factor, + 'safety_stock': item_master.safety_stock } ) @@ -732,7 +751,7 @@ def get_items_for_material_requests(doc, warehouses=None): if details.qty > 0: items = get_material_request_items(details, sales_order, company, - ignore_existing_ordered_qty, warehouse, bin_dict) + ignore_existing_ordered_qty, include_safety_stock, warehouse, bin_dict) if items: mr_items.append(items) diff --git a/erpnext/manufacturing/doctype/routing/test_routing.py b/erpnext/manufacturing/doctype/routing/test_routing.py index 73d05a6157..7071bc1ab0 100644 --- a/erpnext/manufacturing/doctype/routing/test_routing.py +++ b/erpnext/manufacturing/doctype/routing/test_routing.py @@ -74,7 +74,7 @@ def setup_bom(**args): }) if not args.raw_materials: - if not frappe.db.exists('Item', "Test Extra Item 1"): + if not frappe.db.exists('Item', "Test Extra Item N-1"): make_item("Test Extra Item N-1", { 'is_stock_item': 1, }) @@ -88,4 +88,4 @@ def setup_bom(**args): else: bom_doc = frappe.get_doc("BOM", name) - return bom_doc \ No newline at end of file + return bom_doc diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index 00e8c5418a..08291d1eae 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -82,7 +82,7 @@ class TestWorkOrder(unittest.TestCase): wo_order.set_work_order_operations() self.assertEqual(wo_order.planned_operating_cost, cost*2) - def test_resered_qty_for_partial_completion(self): + def test_reserved_qty_for_partial_completion(self): item = "_Test Item" warehouse = create_warehouse("Test Warehouse for reserved_qty - _TC") @@ -109,7 +109,7 @@ class TestWorkOrder(unittest.TestCase): s.submit() bin1_at_completion = get_bin(item, warehouse) - + self.assertEqual(cint(bin1_at_completion.reserved_qty_for_production), reserved_qty_on_submission - 1) @@ -592,6 +592,55 @@ class TestWorkOrder(unittest.TestCase): frappe.db.set_value("Manufacturing Settings", None, "backflush_raw_materials_based_on", "BOM") + def test_make_stock_entry_for_customer_provided_item(self): + finished_item = 'Test Item for Make Stock Entry 1' + make_item(finished_item, { + "include_item_in_manufacturing": 1, + "is_stock_item": 1 + }) + + customer_provided_item = 'CUST-0987' + make_item(customer_provided_item, { + 'is_purchase_item': 0, + 'is_customer_provided_item': 1, + "is_stock_item": 1, + "include_item_in_manufacturing": 1, + 'customer': '_Test Customer' + }) + + if not frappe.db.exists('BOM', {'item': finished_item}): + make_bom(item=finished_item, raw_materials=[customer_provided_item], rm_qty=1) + + company = "_Test Company with perpetual inventory" + customer_warehouse = create_warehouse("Test Customer Provided Warehouse", company=company) + wo = make_wo_order_test_record(item=finished_item, qty=1, source_warehouse=customer_warehouse, + company=company) + + ste = frappe.get_doc(make_stock_entry(wo.name, purpose='Material Transfer for Manufacture')) + ste.insert() + + self.assertEqual(len(ste.items), 1) + for item in ste.items: + self.assertEqual(item.allow_zero_valuation_rate, 1) + self.assertEqual(item.valuation_rate, 0) + + def test_valuation_rate_missing_on_make_stock_entry(self): + item_name = 'Test Valuation Rate Missing' + make_item(item_name, { + "is_stock_item": 1, + "include_item_in_manufacturing": 1, + }) + + if not frappe.db.get_value('BOM', {'item': item_name}): + make_bom(item=item_name, raw_materials=[item_name], rm_qty=1) + + company = "_Test Company with perpetual inventory" + source_warehouse = create_warehouse("Test Valuation Rate Missing Warehouse", company=company) + wo = make_wo_order_test_record(item=item_name, qty=1, source_warehouse=source_warehouse, + company=company) + + self.assertRaises(frappe.ValidationError, make_stock_entry, wo.name, 'Material Transfer for Manufacture') + def get_scrap_item_details(bom_no): scrap_items = {} for item in frappe.db.sql("""select item_code, stock_qty from `tabBOM Scrap Item` @@ -609,6 +658,15 @@ def allow_overproduction(fieldname, percentage): def make_wo_order_test_record(**args): args = frappe._dict(args) + if args.company and args.company != "_Test Company": + warehouse_map = { + "fg_warehouse": "_Test FG Warehouse", + "wip_warehouse": "_Test WIP Warehouse" + } + + for attr, wh_name in warehouse_map.items(): + if not args.get(attr): + args[attr] = create_warehouse(wh_name, company=args.company) wo_order = frappe.new_doc("Work Order") wo_order.production_item = args.production_item or args.item or args.item_code or "_Test FG Item" diff --git a/erpnext/manufacturing/doctype/work_order/work_order.json b/erpnext/manufacturing/doctype/work_order/work_order.json index 585a09db2b..cd9edeeea8 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.json +++ b/erpnext/manufacturing/doctype/work_order/work_order.json @@ -333,8 +333,7 @@ "fieldname": "operations", "fieldtype": "Table", "label": "Operations", - "options": "Work Order Operation", - "read_only": 1 + "options": "Work Order Operation" }, { "depends_on": "operations", @@ -496,7 +495,7 @@ "image_field": "image", "is_submittable": 1, "links": [], - "modified": "2020-05-05 19:32:43.323054", + "modified": "2021-03-16 13:27:51.116484", "modified_by": "Administrator", "module": "Manufacturing", "name": "Work Order", diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 3d64ad4318..8507f5eb34 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -509,6 +509,7 @@ class WorkOrder(Document): stock_bin = get_bin(d.item_code, d.source_warehouse) stock_bin.update_reserved_qty_for_production() + @frappe.whitelist() def get_items_and_operations_from_bom(self): self.set_required_items() self.set_work_order_operations() @@ -613,6 +614,7 @@ class WorkOrder(Document): item.db_set('consumed_qty', flt(consumed_qty), update_modified=False) + @frappe.whitelist() def make_bom(self): data = frappe.db.sql(""" select sed.item_code, sed.qty, sed.s_warehouse from `tabStock Entry Detail` sed, `tabStock Entry` se diff --git a/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.py b/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.py index 2ca9f1694b..fc27d35598 100644 --- a/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.py +++ b/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.py @@ -61,7 +61,7 @@ class ForecastingReport(ExponentialSmoothingForecast): from_date = add_years(self.filters.from_date, cint(self.filters.no_of_years) * -1) self.period_list = get_period_list(from_date, self.filters.to_date, - from_date, self.filters.to_date, None, self.filters.periodicity, ignore_fiscal_year=True) + from_date, self.filters.to_date, "Date Range", self.filters.periodicity, ignore_fiscal_year=True) order_data = self.get_data_for_forecast() or [] diff --git a/erpnext/non_profit/doctype/donation/donation.py b/erpnext/non_profit/doctype/donation/donation.py index 6a2a06dbc8..4fd1a30ab9 100644 --- a/erpnext/non_profit/doctype/donation/donation.py +++ b/erpnext/non_profit/doctype/donation/donation.py @@ -42,7 +42,7 @@ class Donation(Document): self.load_from_db() self.create_payment_entry() - def create_payment_entry(self): + def create_payment_entry(self, date=None): settings = frappe.get_doc('Non Profit Settings') if not settings.automate_donation_payment_entries: return @@ -58,8 +58,9 @@ class Donation(Document): frappe.flags.ignore_account_permission = False pe.paid_from = settings.donation_debit_account pe.paid_to = settings.donation_payment_account + pe.posting_date = date or getdate() pe.reference_no = self.name - pe.reference_date = getdate() + pe.reference_date = date or getdate() pe.flags.ignore_mandatory = True pe.insert() pe.submit() diff --git a/erpnext/non_profit/doctype/member/member.py b/erpnext/non_profit/doctype/member/member.py index 3ba2ee71c6..efc072ee97 100644 --- a/erpnext/non_profit/doctype/member/member.py +++ b/erpnext/non_profit/doctype/member/member.py @@ -53,6 +53,7 @@ class Member(Document): return subscription + @frappe.whitelist() def make_customer_and_link(self): if self.customer: frappe.msgprint(_("A customer is already linked to this Member")) diff --git a/erpnext/non_profit/doctype/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py index c41a2f5165..e8ae6187b7 100644 --- a/erpnext/non_profit/doctype/membership/membership.py +++ b/erpnext/non_profit/doctype/membership/membership.py @@ -48,7 +48,7 @@ class Membership(Document): last_membership = erpnext.get_last_membership(self.member) # if person applied for offline membership - if last_membership and last_membership != self.name and not frappe.session.user == "Administrator": + if last_membership and last_membership.name != self.name and not frappe.session.user == "Administrator": # if last membership does not expire in 30 days, then do not allow to renew if getdate(add_days(last_membership.to_date, -30)) > getdate(nowdate()) : frappe.throw(_("You can only renew if your membership expires within 30 days")) @@ -74,6 +74,7 @@ class Membership(Document): self.generate_invoice(with_payment_entry=settings.automate_membership_payment_entries, save=True) + @frappe.whitelist() def generate_invoice(self, save=True, with_payment_entry=False): if not (self.paid or self.currency or self.amount): frappe.throw(_("The payment for this membership is not paid. To generate invoice fill the payment details")) @@ -90,6 +91,7 @@ class Membership(Document): self.validate_membership_type_and_settings(plan, settings) invoice = make_invoice(self, member, plan, settings) + self.reload() self.invoice = invoice.name if with_payment_entry: @@ -129,6 +131,7 @@ class Membership(Document): pe.save() pe.submit() + @frappe.whitelist() def send_acknowlement(self): settings = frappe.get_doc("Non Profit Settings") if not settings.send_email: @@ -284,6 +287,7 @@ def trigger_razorpay_subscription(*args, **kwargs): settings = frappe.get_doc("Non Profit Settings") if settings.allow_invoicing and settings.automate_membership_invoicing: + membership.reload() membership.generate_invoice(with_payment_entry=settings.automate_membership_payment_entries, save=True) except Exception as e: diff --git a/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.js b/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.js index cff92b42ab..4c4ca9834b 100644 --- a/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.js +++ b/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.js @@ -19,7 +19,7 @@ frappe.ui.form.on("Non Profit Settings", { }; }); - frm.set_query("debit_account", function() { + frm.set_query("membership_debit_account", function() { return { filters: { "account_type": "Receivable", @@ -29,6 +29,16 @@ frappe.ui.form.on("Non Profit Settings", { }; }); + frm.set_query("donation_debit_account", function() { + return { + filters: { + "account_type": "Receivable", + "is_group": 0, + "company": frm.doc.donation_company + } + }; + }); + frm.set_query("membership_payment_account", function () { var account_types = ["Bank", "Cash"]; return { @@ -40,6 +50,17 @@ frappe.ui.form.on("Non Profit Settings", { }; }); + frm.set_query("donation_payment_account", function () { + var account_types = ["Bank", "Cash"]; + return { + filters: { + "account_type": ["in", account_types], + "is_group": 0, + "company": frm.doc.donation_company + } + }; + }); + let docs_url = "https://docs.erpnext.com/docs/user/manual/en/non_profit/membership"; frm.set_intro(__("You can learn more about memberships in the manual. ") + `${__('ERPNext Docs')}`, true); diff --git a/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.py b/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.py index 108554c6a0..a84cc2cdb5 100644 --- a/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.py +++ b/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.py @@ -9,6 +9,7 @@ from frappe.integrations.utils import get_payment_gateway_controller from frappe.model.document import Document class NonProfitSettings(Document): + @frappe.whitelist() def generate_webhook_secret(self, field="membership_webhook_secret"): key = frappe.generate_hash(length=20) self.set(field, key) @@ -21,6 +22,7 @@ class NonProfitSettings(Document): _("Webhook Secret") ) + @frappe.whitelist() def revoke_key(self, key): self.set(key, None) self.save() diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 7016ecdd96..aabefb85e7 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -99,7 +99,7 @@ execute:frappe.delete_doc("DocType", "Purchase Request") execute:frappe.delete_doc("DocType", "Purchase Request Item") erpnext.patches.v4_2.recalculate_bom_cost erpnext.patches.v4_2.fix_gl_entries_for_stock_transactions -erpnext.patches.v4_2.update_requested_and_ordered_qty +erpnext.patches.v4_2.update_requested_and_ordered_qty #2021-03-31 execute:frappe.rename_doc("DocType", "Support Ticket", "Issue", force=True) erpnext.patches.v4_4.make_email_accounts execute:frappe.delete_doc("DocType", "Contact Control") @@ -208,7 +208,7 @@ erpnext.patches.v5_7.update_item_description_based_on_item_master erpnext.patches.v5_7.item_template_attributes execute:frappe.delete_doc_if_exists("DocType", "Manage Variants") execute:frappe.delete_doc_if_exists("DocType", "Manage Variants Item") -erpnext.patches.v4_2.repost_reserved_qty #2016-04-15 +erpnext.patches.v4_2.repost_reserved_qty #2021-03-31 erpnext.patches.v5_4.update_purchase_cost_against_project erpnext.patches.v5_8.update_order_reference_in_return_entries erpnext.patches.v5_8.add_credit_note_print_heading @@ -720,7 +720,7 @@ erpnext.patches.v13_0.delete_report_requested_items_to_order erpnext.patches.v12_0.update_item_tax_template_company erpnext.patches.v13_0.move_branch_code_to_bank_account erpnext.patches.v13_0.healthcare_lab_module_rename_doctypes -erpnext.patches.v13_0.add_standard_navbar_items #4 +erpnext.patches.v13_0.add_standard_navbar_items #2021-03-24 erpnext.patches.v13_0.stock_entry_enhancements erpnext.patches.v12_0.update_state_code_for_daman_and_diu erpnext.patches.v12_0.rename_lost_reason_detail @@ -752,6 +752,7 @@ erpnext.patches.v13_0.set_company_in_leave_ledger_entry erpnext.patches.v13_0.convert_qi_parameter_to_link_field erpnext.patches.v13_0.setup_patient_history_settings_for_standard_doctypes erpnext.patches.v13_0.add_naming_series_to_old_projects # 1-02-2021 +erpnext.patches.v13_0.update_payment_terms_outstanding erpnext.patches.v12_0.add_state_code_for_ladakh erpnext.patches.v13_0.item_reposting_for_incorrect_sl_and_gl erpnext.patches.v13_0.delete_old_bank_reconciliation_doctypes @@ -759,4 +760,6 @@ erpnext.patches.v13_0.update_vehicle_no_reqd_condition erpnext.patches.v13_0.setup_fields_for_80g_certificate_and_donation erpnext.patches.v13_0.rename_membership_settings_to_non_profit_settings erpnext.patches.v13_0.setup_gratuity_rule_for_india_and_uae +erpnext.patches.v13_0.setup_uae_vat_fields execute:frappe.db.set_value('System Settings', None, 'app_name', 'ERPNext') +erpnext.patches.v13_0.rename_discharge_date_in_ip_record diff --git a/erpnext/patches/v13_0/rename_discharge_date_in_ip_record.py b/erpnext/patches/v13_0/rename_discharge_date_in_ip_record.py new file mode 100644 index 0000000000..491dc82f78 --- /dev/null +++ b/erpnext/patches/v13_0/rename_discharge_date_in_ip_record.py @@ -0,0 +1,8 @@ +from __future__ import unicode_literals +import frappe +from frappe.model.utils.rename_field import rename_field + +def execute(): + frappe.reload_doc("Healthcare", "doctype", "Inpatient Record") + if frappe.db.has_column("Inpatient Record", "discharge_date"): + rename_field("Inpatient Record", "discharge_date", "discharge_datetime") diff --git a/erpnext/patches/v13_0/setup_uae_vat_fields.py b/erpnext/patches/v13_0/setup_uae_vat_fields.py new file mode 100644 index 0000000000..d7a5c682df --- /dev/null +++ b/erpnext/patches/v13_0/setup_uae_vat_fields.py @@ -0,0 +1,12 @@ +# Copyright (c) 2019, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +import frappe +from erpnext.regional.united_arab_emirates.setup import setup + +def execute(): + company = frappe.get_all('Company', filters = {'country': 'United Arab Emirates'}) + if not company: + return + + setup() \ No newline at end of file diff --git a/erpnext/patches/v13_0/update_payment_terms_outstanding.py b/erpnext/patches/v13_0/update_payment_terms_outstanding.py new file mode 100644 index 0000000000..4816b40250 --- /dev/null +++ b/erpnext/patches/v13_0/update_payment_terms_outstanding.py @@ -0,0 +1,15 @@ +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# MIT License. See license.txt + +from __future__ import unicode_literals +import frappe + +def execute(): + frappe.reload_doc("accounts", "doctype", "Payment Schedule") + if frappe.db.count('Payment Schedule'): + frappe.db.sql(''' + UPDATE + `tabPayment Schedule` ps + SET + ps.outstanding = (ps.payment_amount - ps.paid_amount) + ''') diff --git a/erpnext/payroll/doctype/additional_salary/additional_salary.json b/erpnext/payroll/doctype/additional_salary/additional_salary.json index 2b29f667fb..61ae7e4c2f 100644 --- a/erpnext/payroll/doctype/additional_salary/additional_salary.json +++ b/erpnext/payroll/doctype/additional_salary/additional_salary.json @@ -163,7 +163,6 @@ "read_only": 1 }, { - "default": "Company:company:default_currency", "depends_on": "eval:(doc.docstatus==1 || doc.employee)", "fieldname": "currency", "fieldtype": "Link", @@ -176,7 +175,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2020-10-20 17:51:13.419716", + "modified": "2021-03-31 14:45:48.566756", "modified_by": "Administrator", "module": "Payroll", "name": "Additional Salary", diff --git a/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.json b/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.json index 4c45580bf0..c6f764ccdb 100644 --- a/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.json +++ b/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.json @@ -124,7 +124,6 @@ "read_only": 1 }, { - "default": "Company:company:default_currency", "depends_on": "eval:(doc.docstatus==1 || doc.employee)", "fieldname": "currency", "fieldtype": "Link", @@ -148,7 +147,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2020-12-14 15:52:08.566418", + "modified": "2021-03-31 14:46:22.465521", "modified_by": "Administrator", "module": "Payroll", "name": "Employee Benefit Application", diff --git a/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.js b/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.js index ea9ccd5205..e1f8431ec5 100644 --- a/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.js +++ b/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.js @@ -21,7 +21,6 @@ frappe.ui.form.on('Employee Benefit Claim', { callback: function(r) { if (r.message) { frm.set_value('currency', r.message); - frm.set_df_property('currency', 'hidden', 0); } } }); diff --git a/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.json b/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.json index da24aacda1..e331b7af93 100644 --- a/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.json +++ b/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.json @@ -125,10 +125,9 @@ "label": "Attachments" }, { - "default": "Company:company:default_currency", + "depends_on": "eval: doc.employee", "fieldname": "currency", "fieldtype": "Link", - "hidden": 1, "label": "Currency", "options": "Currency", "read_only": 1, @@ -145,7 +144,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2020-11-25 11:49:56.097352", + "modified": "2021-03-31 15:51:51.489269", "modified_by": "Administrator", "module": "Payroll", "name": "Employee Benefit Claim", diff --git a/erpnext/payroll/doctype/employee_incentive/employee_incentive.json b/erpnext/payroll/doctype/employee_incentive/employee_incentive.json index e5b1052b3a..51346c6c7d 100644 --- a/erpnext/payroll/doctype/employee_incentive/employee_incentive.json +++ b/erpnext/payroll/doctype/employee_incentive/employee_incentive.json @@ -75,7 +75,6 @@ "reqd": 1 }, { - "default": "Company:company:default_currency", "depends_on": "eval:(doc.docstatus==1 || doc.employee)", "fieldname": "currency", "fieldtype": "Link", @@ -95,7 +94,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2020-10-20 17:22:16.468042", + "modified": "2021-03-31 14:48:00.919839", "modified_by": "Administrator", "module": "Payroll", "name": "Employee Incentive", diff --git a/erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.js b/erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.js index 0e0c9b5a1a..fb11875e96 100644 --- a/erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.js +++ b/erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.js @@ -47,5 +47,26 @@ frappe.ui.form.on('Employee Tax Exemption Declaration', { }); }).addClass("btn-primary"); } + }, + + employee: function(frm) { + if (frm.doc.employee) { + frm.trigger('get_employee_currency'); + } + }, + + get_employee_currency: function(frm) { + frappe.call({ + method: "erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment.get_employee_currency", + args: { + employee: frm.doc.employee, + }, + callback: function(r) { + if (r.message) { + frm.set_value('currency', r.message); + frm.refresh_fields(); + } + } + }); } }); diff --git a/erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.json b/erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.json index 83d4ae53df..873bf887bf 100644 --- a/erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.json +++ b/erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.json @@ -108,7 +108,7 @@ "read_only": 1 }, { - "default": "Company:company:default_currency", + "depends_on": "eval: doc.employee", "fieldname": "currency", "fieldtype": "Link", "label": "Currency", @@ -119,7 +119,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2020-10-20 16:42:24.493761", + "modified": "2021-03-31 20:41:57.387749", "modified_by": "Administrator", "module": "Payroll", "name": "Employee Tax Exemption Declaration", diff --git a/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.js b/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.js index 497f35c41e..4fb0a3771e 100644 --- a/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.js +++ b/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.js @@ -58,5 +58,26 @@ frappe.ui.form.on('Employee Tax Exemption Proof Submission', { currency: function(frm) { frm.refresh_fields(); - } + }, + + employee: function(frm) { + if (frm.doc.employee) { + frm.trigger('get_employee_currency'); + } + }, + + get_employee_currency: function(frm) { + frappe.call({ + method: "erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment.get_employee_currency", + args: { + employee: frm.doc.employee, + }, + callback: function(r) { + if (r.message) { + frm.set_value('currency', r.message); + frm.refresh_fields(); + } + } + }); + }, }); diff --git a/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.json b/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.json index 53f18cb1fe..f32202a3bd 100644 --- a/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.json +++ b/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.json @@ -131,7 +131,7 @@ "read_only": 1 }, { - "default": "Company:company:default_currency", + "depends_on": "eval: doc.employee", "fieldname": "currency", "fieldtype": "Link", "label": "Currency", @@ -142,7 +142,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2020-10-20 16:47:03.410020", + "modified": "2021-03-31 20:48:32.639885", "modified_by": "Administrator", "module": "Payroll", "name": "Employee Tax Exemption Proof Submission", diff --git a/erpnext/payroll/doctype/income_tax_slab/income_tax_slab.json b/erpnext/payroll/doctype/income_tax_slab/income_tax_slab.json index 9fa261dea2..c343a44326 100644 --- a/erpnext/payroll/doctype/income_tax_slab/income_tax_slab.json +++ b/erpnext/payroll/doctype/income_tax_slab/income_tax_slab.json @@ -93,7 +93,7 @@ "options": "Income Tax Slab Other Charges" }, { - "default": "Company:company:default_currency", + "fetch_from": "company.default_currency", "fieldname": "currency", "fieldtype": "Link", "label": "Currency", @@ -104,7 +104,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2020-10-19 13:54:24.728075", + "modified": "2021-03-31 20:53:33.323712", "modified_by": "Administrator", "module": "Payroll", "name": "Income Tax Slab", diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py index 6bcd4e0c00..78904710a8 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py @@ -95,6 +95,7 @@ class PayrollEntry(Document): return emp_list + @frappe.whitelist() def fill_employee_details(self): self.set('employees', []) employees = self.get_emp_list() @@ -142,6 +143,7 @@ class PayrollEntry(Document): if not self.get(fieldname): frappe.throw(_("Please set {0}").format(self.meta.get_label(fieldname))) + @frappe.whitelist() def create_salary_slips(self): """ Creates salary slip for selected employees if already not created @@ -329,6 +331,7 @@ class PayrollEntry(Document): amount = flt(amount) * flt(conversion_rate) return exchange_rate, amount + @frappe.whitelist() def make_payment_entry(self): self.check_permission('write') diff --git a/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py index e098ec79b0..84c381489c 100644 --- a/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py @@ -12,7 +12,7 @@ from erpnext.hr.doctype.employee.test_employee import make_employee from erpnext.payroll.doctype.salary_slip.test_salary_slip import get_salary_component_account, \ make_earning_salary_component, make_deduction_salary_component, create_account, make_employee_salary_slip from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure, create_salary_structure_assignment -from erpnext.loan_management.doctype.loan.test_loan import create_loan, make_loan_disbursement_entry +from erpnext.loan_management.doctype.loan.test_loan import create_loan, make_loan_disbursement_entry, create_loan_type, create_loan_accounts from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import process_loan_interest_accrual_for_term_loans class TestPayrollEntry(unittest.TestCase): @@ -168,15 +168,23 @@ class TestPayrollEntry(unittest.TestCase): salary_structure = "Test Salary Structure for Loan" make_salary_structure(salary_structure, "Monthly", employee=employee_doc.name, company="_Test Company", currency=company_doc.default_currency) + if not frappe.db.exists("Loan Type", "Car Loan"): + create_loan_accounts() + create_loan_type("Car Loan", 500000, 8.4, + is_term_loan=1, + mode_of_payment='Cash', + payment_account='Payment Account - _TC', + loan_account='Loan Account - _TC', + interest_income_account='Interest Income Account - _TC', + penalty_income_account='Penalty Income Account - _TC') + loan = create_loan(applicant, "Car Loan", 280000, "Repay Over Number of Periods", 20, posting_date=add_months(nowdate(), -1)) loan.repay_from_salary = 1 loan.submit() make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=add_months(nowdate(), -1)) - process_loan_interest_accrual_for_term_loans(posting_date=nowdate()) - dates = get_start_end_dates('Monthly', nowdate()) make_payroll_entry(company="_Test Company", start_date=dates.start_date, payable_account=company_doc.default_payroll_payable_account, currency=company_doc.default_currency, end_date=dates.end_date, branch=branch, cost_center="Main - _TC", payment_account="Cash - _TC") @@ -267,4 +275,4 @@ def get_salary_slip(user, period, salary_structure): salary_slip.calculate_net_pay() salary_slip.db_update() - return salary_slip \ No newline at end of file + return salary_slip diff --git a/erpnext/payroll/doctype/retention_bonus/retention_bonus.json b/erpnext/payroll/doctype/retention_bonus/retention_bonus.json index 6647230078..cd563bc404 100644 --- a/erpnext/payroll/doctype/retention_bonus/retention_bonus.json +++ b/erpnext/payroll/doctype/retention_bonus/retention_bonus.json @@ -93,7 +93,6 @@ "reqd": 1 }, { - "default": "Company:company:default_currency", "depends_on": "eval:(doc.docstatus==1 || doc.employee)", "fieldname": "currency", "fieldtype": "Link", @@ -106,7 +105,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2020-10-20 17:27:47.003134", + "modified": "2021-03-31 14:50:29.401020", "modified_by": "Administrator", "module": "Payroll", "name": "Retention Bonus", diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.js b/erpnext/payroll/doctype/salary_slip/salary_slip.js index 7460c75227..3e8a213ca9 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.js +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.js @@ -74,43 +74,46 @@ frappe.ui.form.on("Salary Slip", { if (!frm.doc.letter_head && company.default_letter_head) { frm.set_value('letter_head', company.default_letter_head); } + }, + + currency: function(frm) { frm.trigger("set_dynamic_labels"); }, set_dynamic_labels: function(frm) { var company_currency = frm.doc.company? erpnext.get_currency(frm.doc.company): frappe.defaults.get_default("currency"); - frappe.run_serially([ - () => frm.events.set_exchange_rate(frm, company_currency), - () => frm.events.change_form_labels(frm, company_currency), - () => frm.events.change_grid_labels(frm), - () => frm.refresh_fields() - ]); + if (frm.doc.employee && frm.doc.currency) { + frappe.run_serially([ + () => frm.events.set_exchange_rate(frm, company_currency), + () => frm.events.change_form_labels(frm, company_currency), + () => frm.events.change_grid_labels(frm), + () => frm.refresh_fields() + ]); + } }, set_exchange_rate: function(frm, company_currency) { - if (frm.doc.docstatus === 0) { - if (frm.doc.currency) { - var from_currency = frm.doc.currency; - if (from_currency != company_currency) { - frm.events.hide_loan_section(frm); - frappe.call({ - method: "erpnext.setup.utils.get_exchange_rate", - args: { - from_currency: from_currency, - to_currency: company_currency, - }, - callback: function(r) { - frm.set_value("exchange_rate", flt(r.message)); - frm.set_df_property('exchange_rate', 'hidden', 0); - frm.set_df_property("exchange_rate", "description", "1 " + frm.doc.currency - + " = [?] " + company_currency); - } - }); - } else { - frm.set_value("exchange_rate", 1.0); - frm.set_df_property('exchange_rate', 'hidden', 1); - frm.set_df_property("exchange_rate", "description", "" ); - } + if (frm.doc.currency) { + var from_currency = frm.doc.currency; + if (from_currency != company_currency) { + frm.events.hide_loan_section(frm); + frappe.call({ + method: "erpnext.setup.utils.get_exchange_rate", + args: { + from_currency: from_currency, + to_currency: company_currency, + }, + callback: function(r) { + frm.set_value("exchange_rate", flt(r.message)); + frm.set_df_property("exchange_rate", "hidden", 0); + frm.set_df_property("exchange_rate", "description", "1 " + frm.doc.currency + + " = [?] " + company_currency); + } + }); + } else { + frm.set_value("exchange_rate", 1.0); + frm.set_df_property("exchange_rate", "hidden", 1); + frm.set_df_property("exchange_rate", "description", ""); } } }, @@ -213,7 +216,7 @@ frappe.ui.form.on('Salary Slip Timesheet', { }); var set_totals = function(frm) { - if (frm.doc.docstatus === 0) { + if (frm.doc.docstatus === 0 && frm.doc.doctype === "Salary Slip") { if (frm.doc.earnings || frm.doc.deductions) { frappe.call({ method: "set_totals", diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.json b/erpnext/payroll/doctype/salary_slip/salary_slip.json index 6688368262..ec5607602d 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.json +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.json @@ -500,7 +500,6 @@ "fieldtype": "Column Break" }, { - "default": "Company:company:default_currency", "depends_on": "eval:(doc.docstatus==1 || doc.salary_structure)", "fetch_from": "salary_structure.currency", "fieldname": "currency", @@ -632,7 +631,7 @@ "idx": 9, "is_submittable": 1, "links": [], - "modified": "2021-02-19 11:48:05.383945", + "modified": "2021-03-31 15:39:28.817166", "modified_by": "Administrator", "module": "Payroll", "name": "Salary Slip", diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py index a04a635807..9abe57cd65 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -142,6 +142,7 @@ class SalarySlip(TransactionBase): self.start_date = date_details.start_date self.end_date = date_details.end_date + @frappe.whitelist() def get_emp_and_working_day_details(self): '''First time, load all the components from salary structure''' if self.employee: @@ -1114,10 +1115,12 @@ class SalarySlip(TransactionBase): self.bank_name = emp.bank_name self.bank_account_no = emp.bank_ac_no + @frappe.whitelist() def process_salary_based_on_working_days(self): self.get_working_days_details(lwp=self.leave_without_pay) self.calculate_net_pay() + @frappe.whitelist() def set_totals(self): self.gross_pay = 0.0 if self.salary_slip_based_on_timesheet == 1: diff --git a/erpnext/payroll/doctype/salary_structure/salary_structure.json b/erpnext/payroll/doctype/salary_structure/salary_structure.json index de56fc8457..5dd1d701f0 100644 --- a/erpnext/payroll/doctype/salary_structure/salary_structure.json +++ b/erpnext/payroll/doctype/salary_structure/salary_structure.json @@ -232,7 +232,7 @@ "idx": 1, "is_submittable": 1, "links": [], - "modified": "2020-09-30 11:30:32.190798", + "modified": "2021-03-31 15:41:12.342380", "modified_by": "Administrator", "module": "Payroll", "name": "Salary Structure", diff --git a/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.json b/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.json index 92bb347661..50fabedb42 100644 --- a/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.json +++ b/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.json @@ -125,7 +125,6 @@ "options": "Income Tax Slab" }, { - "default": "Company:company:default_currency", "depends_on": "eval:(doc.docstatus==1 || doc.salary_structure)", "fetch_from": "salary_structure.currency", "fieldname": "currency", @@ -146,7 +145,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2020-11-30 18:07:48.251311", + "modified": "2021-03-31 15:49:36.361253", "modified_by": "Administrator", "module": "Payroll", "name": "Salary Structure Assignment", diff --git a/erpnext/portal/product_configurator/test_product_configurator.py b/erpnext/portal/product_configurator/test_product_configurator.py index 97042dba92..3521e7e8bf 100644 --- a/erpnext/portal/product_configurator/test_product_configurator.py +++ b/erpnext/portal/product_configurator/test_product_configurator.py @@ -10,8 +10,38 @@ from erpnext.stock.doctype.item.test_item import make_item_variant test_dependencies = ["Item"] class TestProductConfigurator(unittest.TestCase): - def setUp(self): - self.create_variant_item() + @classmethod + def setUpClass(cls): + cls.create_variant_item() + + @classmethod + def create_variant_item(cls): + if not frappe.db.exists('Item', '_Test Variant Item - 2XL'): + frappe.get_doc({ + "description": "_Test Variant Item - 2XL", + "item_code": "_Test Variant Item - 2XL", + "item_name": "_Test Variant Item - 2XL", + "doctype": "Item", + "is_stock_item": 1, + "variant_of": "_Test Variant Item", + "item_group": "_Test Item Group", + "stock_uom": "_Test UOM", + "item_defaults": [{ + "company": "_Test Company", + "default_warehouse": "_Test Warehouse - _TC", + "expense_account": "_Test Account Cost for Goods Sold - _TC", + "buying_cost_center": "_Test Cost Center - _TC", + "selling_cost_center": "_Test Cost Center - _TC", + "income_account": "Sales - _TC" + }], + "attributes": [ + { + "attribute": "Test Size", + "attribute_value": "2XL" + } + ], + "show_variant_in_website": 1 + }).insert() def test_product_list(self): template_items = frappe.get_all('Item', {'show_in_website': 1}) @@ -46,39 +76,6 @@ class TestProductConfigurator(unittest.TestCase): def test_get_products_for_website(self): items = get_products_for_website(attribute_filters={ - 'Test Size': ['Medium'] + 'Test Size': ['2XL'] }) self.assertEqual(len(items), 1) - - - def create_variant_item(self): - if not frappe.db.exists('Item', '_Test Variant Item 1'): - frappe.get_doc({ - "description": "_Test Variant Item 12", - "doctype": "Item", - "is_stock_item": 1, - "variant_of": "_Test Variant Item", - "item_code": "_Test Variant Item 1", - "item_group": "_Test Item Group", - "item_name": "_Test Variant Item 1", - "stock_uom": "_Test UOM", - "item_defaults": [{ - "company": "_Test Company", - "default_warehouse": "_Test Warehouse - _TC", - "expense_account": "_Test Account Cost for Goods Sold - _TC", - "buying_cost_center": "_Test Cost Center - _TC", - "selling_cost_center": "_Test Cost Center - _TC", - "income_account": "Sales - _TC" - }], - "attributes": [ - { - "attribute": "Test Size", - "attribute_value": "Medium" - } - ], - "show_variant_in_website": 1 - }).insert() - - - def tearDown(self): - frappe.db.rollback() \ No newline at end of file diff --git a/erpnext/portal/product_configurator/utils.py b/erpnext/portal/product_configurator/utils.py index 21fd7c2878..d77eb2c396 100644 --- a/erpnext/portal/product_configurator/utils.py +++ b/erpnext/portal/product_configurator/utils.py @@ -298,7 +298,7 @@ def get_items_by_fields(field_filters): def get_items(filters=None, search=None): - start = frappe.form_dict.start or 0 + start = frappe.form_dict.get('start', 0) products_settings = get_product_settings() page_length = products_settings.products_per_page diff --git a/erpnext/projects/doctype/project/project.js b/erpnext/projects/doctype/project/project.js index 077011ace0..c5265e23c0 100644 --- a/erpnext/projects/doctype/project/project.js +++ b/erpnext/projects/doctype/project/project.js @@ -18,8 +18,8 @@ frappe.ui.form.on("Project", { }; }, onload: function (frm) { - var so = frappe.meta.get_docfield("Project", "sales_order"); - so.get_route_options_for_new_doc = function (field) { + const so = frm.get_docfield("sales_order"); + so.get_route_options_for_new_doc = () => { if (frm.is_new()) return; return { "customer": frm.doc.customer, diff --git a/erpnext/projects/doctype/project/test_project.py b/erpnext/projects/doctype/project/test_project.py index 15a2873ade..a92bad1d5a 100644 --- a/erpnext/projects/doctype/project/test_project.py +++ b/erpnext/projects/doctype/project/test_project.py @@ -4,12 +4,14 @@ from __future__ import unicode_literals import frappe, unittest -test_records = frappe.get_test_records('Project') -test_ignore = ["Sales Order"] +from frappe.utils import getdate, nowdate, add_days from erpnext.projects.doctype.project_template.test_project_template import make_project_template from erpnext.projects.doctype.task.test_task import create_task -from frappe.utils import getdate, nowdate, add_days + +test_records = frappe.get_test_records('Project') +test_ignore = ["Sales Order"] + class TestProject(unittest.TestCase): def test_project_with_template_having_no_parent_and_depend_tasks(self): @@ -112,7 +114,8 @@ def make_project(args): doctype = 'Project', project_name = args.project_name, status = 'Open', - expected_start_date = args.start_date + expected_start_date = args.start_date, + company= args.company or '_Test Company' )) if args.project_template_name: diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 1c0abdffcf..21a20a7bce 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -577,7 +577,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ var d = locals[cdt][cdn]; me.add_taxes_from_item_tax_template(d.item_tax_rate); if (d.free_item_data) { - me.apply_product_discount(d.free_item_data); + me.apply_product_discount(d); } }, () => { @@ -738,21 +738,15 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ } else { var valid_serial_nos = []; - + var serialnos = []; // Replacing all occurences of comma with carriage return - var serial_nos = item.serial_no.trim().replace(/,/g, '\n'); - - serial_nos = serial_nos.trim().split('\n'); - - // Trim each string and push unique string to new list - for (var x=0; x<=serial_nos.length - 1; x++) { - if (serial_nos[x].trim() != "" && valid_serial_nos.indexOf(serial_nos[x].trim()) == -1) { - valid_serial_nos.push(serial_nos[x].trim()); + item.serial_no = item.serial_no.replace(/,/g, '\n'); + serialnos = item.serial_no.split("\n"); + for (var i = 0; i < serialnos.length; i++) { + if (serialnos[i] != "") { + valid_serial_nos.push(serialnos[i]); } } - - // Add the new list to the serial no. field in grid with each in new line - item.serial_no = valid_serial_nos.join('\n'); item.conversion_factor = item.conversion_factor || 1; refresh_field("serial_no", item.name, item.parentfield); @@ -1173,6 +1167,11 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ this.calculate_net_weight(); } + // for handling customization not to fetch price list rate + if(frappe.flags.dont_fetch_price_list_rate) { + return + } + if (!dont_fetch_price_list_rate && frappe.meta.has_field(doc.doctype, "price_list_currency")) { this.apply_price_list(item, true); @@ -1204,7 +1203,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ calculate_stock_uom_rate: function(doc, cdt, cdn) { let item = frappe.get_doc(cdt, cdn); - item.stock_uom_rate = flt(item.rate)/flt(item.conversion_factor); + item.stock_uom_rate = flt(item.rate)/flt(item.conversion_factor); refresh_field("stock_uom_rate", item.name, item.parentfield); }, service_stop_date: function(frm, cdt, cdn) { @@ -1533,7 +1532,10 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ if(k=="price_list_rate") { if(flt(v) != flt(d.price_list_rate)) price_list_rate_changed = true; } - frappe.model.set_value(d.doctype, d.name, k, v); + + if (k !== 'free_item_data') { + frappe.model.set_value(d.doctype, d.name, k, v); + } } } @@ -1545,7 +1547,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ } if (d.free_item_data) { - me.apply_product_discount(d.free_item_data); + me.apply_product_discount(d); } if (d.apply_rule_on_other_items) { @@ -1579,20 +1581,31 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ } }, - apply_product_discount: function(free_item_data) { - const items = this.frm.doc.items.filter(d => (d.item_code == free_item_data.item_code - && d.is_free_item)) || []; + apply_product_discount: function(args) { + const items = this.frm.doc.items.filter(d => (d.is_free_item)) || []; - if (!items.length) { - let row_to_modify = frappe.model.add_child(this.frm.doc, - this.frm.doc.doctype + ' Item', 'items'); + const exist_items = items.map(row => (row.item_code, row.pricing_rules)); - for (let key in free_item_data) { - row_to_modify[key] = free_item_data[key]; + args.free_item_data.forEach(pr_row => { + let row_to_modify = {}; + if (!items || !in_list(exist_items, (pr_row.item_code, pr_row.pricing_rules))) { + + row_to_modify = frappe.model.add_child(this.frm.doc, + this.frm.doc.doctype + ' Item', 'items'); + + } else if(items) { + row_to_modify = items.filter(d => (d.item_code === pr_row.item_code + && d.pricing_rules === pr_row.pricing_rules))[0]; } - } if (items && items.length && free_item_data) { - items[0].qty = free_item_data.qty - } + + for (let key in pr_row) { + row_to_modify[key] = pr_row[key]; + } + }); + + // free_item_data is a temporary variable + args.free_item_data = ''; + refresh_field('items'); }, apply_price_list: function(item, reset_plc_conversion) { diff --git a/erpnext/public/js/help_links.js b/erpnext/public/js/help_links.js index 472c5374f5..e78992302f 100644 --- a/erpnext/public/js/help_links.js +++ b/erpnext/public/js/help_links.js @@ -1,466 +1,1051 @@ -frappe.provide('frappe.help.help_links'); +frappe.provide("frappe.help.help_links"); -const docsUrl = 'https://erpnext.com/docs/'; +const docsUrl = "https://erpnext.com/docs/"; -frappe.help.help_links['rename tool'] = [ - { label: 'Bulk Rename', url: docsUrl + 'user/manual/en/setting-up/data/bulk-rename' }, -] +frappe.help.help_links["Form/Rename Tool"] = [ + { + label: "Bulk Rename", + url: docsUrl + "user/manual/en/setting-up/data/bulk-rename", + }, +]; //Setup -frappe.help.help_links['user'] = [ - { label: 'New User', url: docsUrl + 'user/manual/en/setting-up/users-and-permissions/adding-users' }, - { label: 'Rename User', url: docsUrl + 'user/manual/en/setting-up/articles/rename-user' }, -] +frappe.help.help_links["List/User"] = [ + { + label: "New User", + url: + docsUrl + + "user/manual/en/setting-up/users-and-permissions/adding-users", + }, + { + label: "Rename User", + url: docsUrl + "user/manual/en/setting-up/articles/rename-user", + }, +]; -frappe.help.help_links['permission-manager'] = [ - { label: 'Role Permissions Manager', url: docsUrl + 'user/manual/en/setting-up/users-and-permissions/role-based-permissions' }, - { label: 'Managing Perm Level in Permissions Manager', url: docsUrl + 'user/manual/en/setting-up/articles/managing-perm-level' }, - { label: 'User Permissions', url: docsUrl + 'user/manual/en/setting-up/users-and-permissions/user-permissions' }, - { label: 'Sharing', url: docsUrl + 'user/manual/en/setting-up/users-and-permissions/sharing' }, - { label: 'Password', url: docsUrl + 'user/manual/en/setting-up/articles/change-password' }, -] +frappe.help.help_links["permission-manager"] = [ + { + label: "Role Permissions Manager", + url: + docsUrl + + "user/manual/en/setting-up/users-and-permissions/role-based-permissions", + }, + { + label: "Managing Perm Level in Permissions Manager", + url: docsUrl + "user/manual/en/setting-up/articles/managing-perm-level", + }, + { + label: "User Permissions", + url: + docsUrl + + "user/manual/en/setting-up/users-and-permissions/user-permissions", + }, + { + label: "Sharing", + url: + docsUrl + "user/manual/en/setting-up/users-and-permissions/sharing", + }, + { + label: "Password", + url: docsUrl + "user/manual/en/setting-up/articles/change-password", + }, +]; -frappe.help.help_links['system-settings'] = [ - { label: 'Naming Series', url: docsUrl + 'user/manual/en/setting-up/settings/system-settings' }, -] +frappe.help.help_links["Form/System Settings"] = [ + { + label: "Naming Series", + url: docsUrl + "user/manual/en/setting-up/settings/system-settings", + }, +]; -frappe.help.help_links['data-import-tool'] = [ - { label: 'Importing and Exporting Data', url: docsUrl + 'user/manual/en/setting-up/data/data-import-tool' }, - { label: 'Overwriting Data from Data Import Tool', url: docsUrl + 'user/manual/en/setting-up/articles/overwriting-data-from-data-import-tool' }, -] +frappe.help.help_links["data-import-tool"] = [ + { + label: "Importing and Exporting Data", + url: docsUrl + "user/manual/en/setting-up/data/data-import-tool", + }, + { + label: "Overwriting Data from Data Import Tool", + url: + docsUrl + + "user/manual/en/setting-up/articles/overwriting-data-from-data-import-tool", + }, +]; -frappe.help.help_links['naming-series'] = [ - { label: 'Naming Series', url: docsUrl + 'user/manual/en/setting-up/settings/naming-series' }, - { label: 'Setting the Current Value for Naming Series', url: docsUrl + 'user/manual/en/setting-up/articles/naming-series-current-value' }, -] +frappe.help.help_links["module_setup"] = [ + { + label: "Role Permissions Manager", + url: + docsUrl + + "user/manual/en/setting-up/users-and-permissions/role-based-permissions", + }, +]; -frappe.help.help_links['global-defaults'] = [ - { label: 'Global Settings', url: docsUrl + 'user/manual/en/setting-up/settings/global-defaults' }, -] +frappe.help.help_links["Form/Naming Series"] = [ + { + label: "Naming Series", + url: docsUrl + "user/manual/en/setting-up/settings/naming-series", + }, + { + label: "Setting the Current Value for Naming Series", + url: + docsUrl + + "user/manual/en/setting-up/articles/naming-series-current-value", + }, +]; -frappe.help.help_links['email-digest'] = [ - { label: 'Email Digest', url: docsUrl + 'user/manual/en/setting-up/email/email-digest' }, -] +frappe.help.help_links["Form/Global Defaults"] = [ + { + label: "Global Settings", + url: docsUrl + "user/manual/en/setting-up/settings/global-defaults", + }, +]; -frappe.help.help_links['print-heading'] = [ - { label: 'Print Heading', url: docsUrl + 'user/manual/en/setting-up/print/print-headings' }, -] +frappe.help.help_links["Form/Email Digest"] = [ + { + label: "Email Digest", + url: docsUrl + "user/manual/en/setting-up/email/email-digest", + }, +]; -frappe.help.help_links['letter-head'] = [ - { label: 'Letter Head', url: docsUrl + 'user/manual/en/setting-up/print/letter-head' }, -] +frappe.help.help_links["List/Print Heading"] = [ + { + label: "Print Heading", + url: docsUrl + "user/manual/en/setting-up/print/print-headings", + }, +]; -frappe.help.help_links['address-template'] = [ - { label: 'Address Template', url: docsUrl + 'user/manual/en/setting-up/print/address-template' }, -] +frappe.help.help_links["List/Letter Head"] = [ + { + label: "Letter Head", + url: docsUrl + "user/manual/en/setting-up/print/letter-head", + }, +]; -frappe.help.help_links['terms-and-conditions'] = [ - { label: 'Terms and Conditions', url: docsUrl + 'user/manual/en/setting-up/print/terms-and-conditions' }, -] +frappe.help.help_links["List/Address Template"] = [ + { + label: "Address Template", + url: docsUrl + "user/manual/en/setting-up/print/address-template", + }, +]; -frappe.help.help_links['cheque-print-template'] = [ - { label: 'Cheque Print Template', url: docsUrl + 'user/manual/en/setting-up/print/cheque-print-template' }, -] +frappe.help.help_links["List/Terms and Conditions"] = [ + { + label: "Terms and Conditions", + url: docsUrl + "user/manual/en/setting-up/print/terms-and-conditions", + }, +]; -frappe.help.help_links['email-account'] = [ - { label: 'Email Account', url: docsUrl + 'user/manual/en/setting-up/email/email-account' }, -] +frappe.help.help_links["List/Cheque Print Template"] = [ + { + label: "Cheque Print Template", + url: docsUrl + "user/manual/en/setting-up/print/cheque-print-template", + }, +]; -frappe.help.help_links['notification'] = [ - { label: 'Notification', url: docsUrl + 'user/manual/en/setting-up/email/notifications' }, -] +frappe.help.help_links["List/Email Account"] = [ + { + label: "Email Account", + url: docsUrl + "user/manual/en/setting-up/email/email-account", + }, +]; -frappe.help.help_links['notification'] = [ - { label: 'Notification', url: docsUrl + 'user/manual/en/setting-up/email/notifications' }, -] +frappe.help.help_links["List/Notification"] = [ + { + label: "Notification", + url: docsUrl + "user/manual/en/setting-up/email/notifications", + }, +]; -frappe.help.help_links['email-digest'] = [ - { label: 'Email Digest', url: docsUrl + 'user/manual/en/setting-up/email/email-digest' }, -] +frappe.help.help_links["Form/Notification"] = [ + { + label: "Notification", + url: docsUrl + "user/manual/en/setting-up/email/notifications", + }, +]; -frappe.help.help_links['auto-email-report'] = [ - { label: 'Auto Email Reports', url: docsUrl + 'user/manual/en/setting-up/email/email-reports' }, -] +frappe.help.help_links["List/Email Digest"] = [ + { + label: "Email Digest", + url: docsUrl + "user/manual/en/setting-up/email/email-digest", + }, +]; -frappe.help.help_links['print-settings'] = [ - { label: 'Print Settings', url: docsUrl + 'user/manual/en/setting-up/print/print-settings' }, -] +frappe.help.help_links["List/Auto Email Report"] = [ + { + label: "Auto Email Reports", + url: docsUrl + "user/manual/en/setting-up/email/email-reports", + }, +]; -frappe.help.help_links['print-format-builder'] = [ - { label: 'Print Format Builder', url: docsUrl + 'user/manual/en/setting-up/print/print-settings' }, -] +frappe.help.help_links["Form/Print Settings"] = [ + { + label: "Print Settings", + url: docsUrl + "user/manual/en/setting-up/print/print-settings", + }, +]; -frappe.help.help_links['print-heading'] = [ - { label: 'Print Heading', url: docsUrl + 'user/manual/en/setting-up/print/print-headings' }, -] +frappe.help.help_links["print-format-builder"] = [ + { + label: "Print Format Builder", + url: docsUrl + "user/manual/en/setting-up/print/print-settings", + }, +]; + +frappe.help.help_links["List/Print Heading"] = [ + { + label: "Print Heading", + url: docsUrl + "user/manual/en/setting-up/print/print-headings", + }, +]; //setup-integrations -frappe.help.help_links['paypal-settings'] = [ - { label: 'PayPal Settings', url: docsUrl + 'user/manual/en/setting-up/integrations/paypal-integration' }, -] +frappe.help.help_links["Form/PayPal Settings"] = [ + { + label: "PayPal Settings", + url: + docsUrl + + "user/manual/en/setting-up/integrations/paypal-integration", + }, +]; -frappe.help.help_links['razorpay-settings'] = [ - { label: 'Razorpay Settings', url: docsUrl + 'user/manual/en/setting-up/integrations/razorpay-integration' }, -] +frappe.help.help_links["Form/Razorpay Settings"] = [ + { + label: "Razorpay Settings", + url: + docsUrl + + "user/manual/en/setting-up/integrations/razorpay-integration", + }, +]; -frappe.help.help_links['dropbox-settings'] = [ - { label: 'Dropbox Settings', url: docsUrl + 'user/manual/en/setting-up/integrations/dropbox-backup' }, -] +frappe.help.help_links["Form/Dropbox Settings"] = [ + { + label: "Dropbox Settings", + url: docsUrl + "user/manual/en/setting-up/integrations/dropbox-backup", + }, +]; -frappe.help.help_links['ldap-settings'] = [ - { label: 'LDAP Settings', url: docsUrl + 'user/manual/en/setting-up/integrations/ldap-integration' }, -] +frappe.help.help_links["Form/LDAP Settings"] = [ + { + label: "LDAP Settings", + url: + docsUrl + "user/manual/en/setting-up/integrations/ldap-integration", + }, +]; -frappe.help.help_links['stripe-settings'] = [ - { label: 'Stripe Settings', url: docsUrl + 'user/manual/en/setting-up/integrations/stripe-integration' }, -] +frappe.help.help_links["Form/Stripe Settings"] = [ + { + label: "Stripe Settings", + url: + docsUrl + + "user/manual/en/setting-up/integrations/stripe-integration", + }, +]; //Sales -frappe.help.help_links['quotation'] = [ - { label: 'Quotation', url: docsUrl + 'user/manual/en/selling/quotation' }, - { label: 'Applying Discount', url: docsUrl + 'user/manual/en/selling/articles/applying-discount' }, - { label: 'Sales Person', url: docsUrl + 'user/manual/en/selling/articles/sales-persons-in-the-sales-transactions' }, - { label: 'Applying Margin', url: docsUrl + 'user/manual/en/selling/articles/adding-margin' }, -] +frappe.help.help_links["Form/Quotation"] = [ + { label: "Quotation", url: docsUrl + "user/manual/en/selling/quotation" }, + { + label: "Applying Discount", + url: docsUrl + "user/manual/en/selling/articles/applying-discount", + }, + { + label: "Sales Person", + url: + docsUrl + + "user/manual/en/selling/articles/sales-persons-in-the-sales-transactions", + }, + { + label: "Applying Margin", + url: docsUrl + "user/manual/en/selling/articles/adding-margin", + }, +]; -frappe.help.help_links['customer'] = [ - { label: 'Customer', url: docsUrl + 'user/manual/en/CRM/customer' }, - { label: 'Credit Limit', url: docsUrl + 'user/manual/en/accounts/credit-limit' }, -] +frappe.help.help_links["List/Customer"] = [ + { label: "Customer", url: docsUrl + "user/manual/en/CRM/customer" }, + { + label: "Credit Limit", + url: docsUrl + "user/manual/en/accounts/credit-limit", + }, +]; -frappe.help.help_links['customer'] = [ - { label: 'Customer', url: docsUrl + 'user/manual/en/CRM/customer' }, - { label: 'Credit Limit', url: docsUrl + 'user/manual/en/accounts/credit-limit' }, -] +frappe.help.help_links["Form/Customer"] = [ + { label: "Customer", url: docsUrl + "user/manual/en/CRM/customer" }, + { + label: "Credit Limit", + url: docsUrl + "user/manual/en/accounts/credit-limit", + }, +]; -frappe.help.help_links['sales-taxes-and-charges-template'] = [ - { label: 'Setting Up Taxes', url: docsUrl + 'user/manual/en/setting-up/setting-up-taxes' }, -] +frappe.help.help_links["List/Sales Taxes and Charges Template"] = [ + { + label: "Setting Up Taxes", + url: docsUrl + "user/manual/en/setting-up/setting-up-taxes", + }, +]; -frappe.help.help_links['sales-taxes-and-charges-template'] = [ - { label: 'Setting Up Taxes', url: docsUrl + 'user/manual/en/setting-up/setting-up-taxes' }, -] +frappe.help.help_links["Form/Sales Taxes and Charges Template"] = [ + { + label: "Setting Up Taxes", + url: docsUrl + "user/manual/en/setting-up/setting-up-taxes", + }, +]; -frappe.help.help_links['sales-order'] = [ - { label: 'Sales Order', url: docsUrl + 'user/manual/en/selling/sales-order' }, - { label: 'Recurring Sales Order', url: docsUrl + 'user/manual/en/accounts/recurring-orders-and-invoices' }, - { label: 'Applying Discount', url: docsUrl + 'user/manual/en/selling/articles/applying-discount' }, - { label: 'Drop Shipping', url: docsUrl + 'user/manual/en/selling/articles/drop-shipping' }, - { label: 'Sales Person', url: docsUrl + 'user/manual/en/selling/articles/sales-persons-in-the-sales-transactions' }, - { label: 'Close Sales Order', url: docsUrl + 'user/manual/en/selling/articles/close-sales-order' }, - { label: 'Applying Margin', url: docsUrl + 'user/manual/en/selling/articles/adding-margin' }, -] +frappe.help.help_links["List/Sales Order"] = [ + { + label: "Sales Order", + url: docsUrl + "user/manual/en/selling/sales-order", + }, + { + label: "Recurring Sales Order", + url: docsUrl + "user/manual/en/accounts/recurring-orders-and-invoices", + }, + { + label: "Applying Discount", + url: docsUrl + "user/manual/en/selling/articles/applying-discount", + }, +]; -frappe.help.help_links['product-bundle'] = [ - { label: 'Product Bundle', url: docsUrl + 'user/manual/en/selling/setup/product-bundle' }, -] +frappe.help.help_links["Form/Sales Order"] = [ + { + label: "Sales Order", + url: docsUrl + "user/manual/en/selling/sales-order", + }, + { + label: "Recurring Sales Order", + url: docsUrl + "user/manual/en/accounts/recurring-orders-and-invoices", + }, + { + label: "Applying Discount", + url: docsUrl + "user/manual/en/selling/articles/applying-discount", + }, + { + label: "Drop Shipping", + url: docsUrl + "user/manual/en/selling/articles/drop-shipping", + }, + { + label: "Sales Person", + url: + docsUrl + + "user/manual/en/selling/articles/sales-persons-in-the-sales-transactions", + }, + { + label: "Close Sales Order", + url: docsUrl + "user/manual/en/selling/articles/close-sales-order", + }, + { + label: "Applying Margin", + url: docsUrl + "user/manual/en/selling/articles/adding-margin", + }, +]; -frappe.help.help_links['selling-settings'] = [ - { label: 'Selling Settings', url: docsUrl + 'user/manual/en/selling/setup/selling-settings' }, -] +frappe.help.help_links["Form/Product Bundle"] = [ + { + label: "Product Bundle", + url: docsUrl + "user/manual/en/selling/setup/product-bundle", + }, +]; + +frappe.help.help_links["Form/Selling Settings"] = [ + { + label: "Selling Settings", + url: docsUrl + "user/manual/en/selling/setup/selling-settings", + }, +]; //Buying -frappe.help.help_links['supplier'] = [ - { label: 'Supplier', url: docsUrl + 'user/manual/en/buying/supplier' }, -] +frappe.help.help_links["List/Supplier"] = [ + { label: "Supplier", url: docsUrl + "user/manual/en/buying/supplier" }, +]; -frappe.help.help_links['request-for-quotation'] = [ - { label: 'Request for Quotation', url: docsUrl + 'user/manual/en/buying/request-for-quotation' }, - { label: 'RFQ Video', url: docsUrl + 'user/videos/learn/request-for-quotation.html' }, -] +frappe.help.help_links["Form/Supplier"] = [ + { label: "Supplier", url: docsUrl + "user/manual/en/buying/supplier" }, +]; -frappe.help.help_links['supplier-quotation'] = [ - { label: 'Supplier Quotation', url: docsUrl + 'user/manual/en/buying/supplier-quotation' }, -] +frappe.help.help_links["Form/Request for Quotation"] = [ + { + label: "Request for Quotation", + url: docsUrl + "user/manual/en/buying/request-for-quotation", + }, + { + label: "RFQ Video", + url: docsUrl + "user/videos/learn/request-for-quotation.html", + }, +]; -frappe.help.help_links['buying-settings'] = [ - { label: 'Buying Settings', url: docsUrl + 'user/manual/en/buying/setup/buying-settings' }, -] +frappe.help.help_links["Form/Supplier Quotation"] = [ + { + label: "Supplier Quotation", + url: docsUrl + "user/manual/en/buying/supplier-quotation", + }, +]; -frappe.help.help_links['purchase-order'] = [ - { label: 'Purchase Order', url: docsUrl + 'user/manual/en/buying/purchase-order' }, - { label: 'Item UoM', url: docsUrl + 'user/manual/en/buying/articles/purchasing-in-different-unit' }, - { label: 'Supplier Item Code', url: docsUrl + 'user/manual/en/buying/articles/maintaining-suppliers-part-no-in-item' }, - { label: 'Recurring Purchase Order', url: docsUrl + 'user/manual/en/accounts/recurring-orders-and-invoices' }, - { label: 'Subcontracting', url: docsUrl + 'user/manual/en/manufacturing/subcontracting' }, -] +frappe.help.help_links["Form/Buying Settings"] = [ + { + label: "Buying Settings", + url: docsUrl + "user/manual/en/buying/setup/buying-settings", + }, +]; -frappe.help.help_links['purchase-taxes-and-charges-template'] = [ - { label: 'Setting Up Taxes', url: docsUrl + 'user/manual/en/setting-up/setting-up-taxes' }, -] +frappe.help.help_links["List/Purchase Order"] = [ + { + label: "Purchase Order", + url: docsUrl + "user/manual/en/buying/purchase-order", + }, + { + label: "Recurring Purchase Order", + url: docsUrl + "user/manual/en/accounts/recurring-orders-and-invoices", + }, +]; -frappe.help.help_links['pos-profile'] = [ - { label: 'POS Profile', url: docsUrl + 'user/manual/en/setting-up/pos-setting' }, -] +frappe.help.help_links["Form/Purchase Order"] = [ + { + label: "Purchase Order", + url: docsUrl + "user/manual/en/buying/purchase-order", + }, + { + label: "Item UoM", + url: + docsUrl + + "user/manual/en/buying/articles/purchasing-in-different-unit", + }, + { + label: "Supplier Item Code", + url: + docsUrl + + "user/manual/en/buying/articles/maintaining-suppliers-part-no-in-item", + }, + { + label: "Recurring Purchase Order", + url: docsUrl + "user/manual/en/accounts/recurring-orders-and-invoices", + }, + { + label: "Subcontracting", + url: docsUrl + "user/manual/en/manufacturing/subcontracting", + }, +]; -frappe.help.help_links['price-list'] = [ - { label: 'Price List', url: docsUrl + 'user/manual/en/setting-up/price-lists' }, -] +frappe.help.help_links["List/Purchase Taxes and Charges Template"] = [ + { + label: "Setting Up Taxes", + url: docsUrl + "user/manual/en/setting-up/setting-up-taxes", + }, +]; -frappe.help.help_links['authorization-rule'] = [ - { label: 'Authorization Rule', url: docsUrl + 'user/manual/en/setting-up/authorization-rule' }, -] +frappe.help.help_links["List/POS Profile"] = [ + { + label: "POS Profile", + url: docsUrl + "user/manual/en/setting-up/pos-setting", + }, +]; -frappe.help.help_links['sms-settings'] = [ - { label: 'SMS Settings', url: docsUrl + 'user/manual/en/setting-up/sms-setting' }, -] +frappe.help.help_links["List/Price List"] = [ + { + label: "Price List", + url: docsUrl + "user/manual/en/setting-up/price-lists", + }, +]; -frappe.help.help_links['stock-reconciliation'] = [ - { label: 'Stock Reconciliation', url: docsUrl + 'user/manual/en/setting-up/stock-reconciliation-for-non-serialized-item' }, -] +frappe.help.help_links["List/Authorization Rule"] = [ + { + label: "Authorization Rule", + url: docsUrl + "user/manual/en/setting-up/authorization-rule", + }, +]; -frappe.help.help_links['territory/view/tree'] = [ - { label: 'Territory', url: docsUrl + 'user/manual/en/setting-up/territory' }, -] +frappe.help.help_links["Form/SMS Settings"] = [ + { + label: "SMS Settings", + url: docsUrl + "user/manual/en/setting-up/sms-setting", + }, +]; -frappe.help.help_links['dropbox-backup'] = [ - { label: 'Dropbox Backup', url: docsUrl + 'user/manual/en/setting-up/third-party-backups' }, - { label: 'Setting Up Dropbox Backup', url: docsUrl + 'user/manual/en/setting-up/articles/setting-up-dropbox-backups' }, -] +frappe.help.help_links["List/Stock Reconciliation"] = [ + { + label: "Stock Reconciliation", + url: + docsUrl + + "user/manual/en/setting-up/stock-reconciliation-for-non-serialized-item", + }, +]; -frappe.help.help_links['workflow'] = [ - { label: 'Workflow', url: docsUrl + 'user/manual/en/setting-up/workflows' }, -] +frappe.help.help_links["Tree/Territory"] = [ + { + label: "Territory", + url: docsUrl + "user/manual/en/setting-up/territory", + }, +]; -frappe.help.help_links['company'] = [ - { label: 'Company', url: docsUrl + 'user/manual/en/setting-up/company-setup' }, - { label: 'Managing Multiple Companies', url: docsUrl + 'user/manual/en/setting-up/articles/managing-multiple-companies' }, - { label: 'Delete All Related Transactions for a Company', url: docsUrl + 'user/manual/en/setting-up/articles/delete-a-company-and-all-related-transactions' }, -] +frappe.help.help_links["Form/Dropbox Backup"] = [ + { + label: "Dropbox Backup", + url: docsUrl + "user/manual/en/setting-up/third-party-backups", + }, + { + label: "Setting Up Dropbox Backup", + url: + docsUrl + + "user/manual/en/setting-up/articles/setting-up-dropbox-backups", + }, +]; + +frappe.help.help_links["List/Workflow"] = [ + { label: "Workflow", url: docsUrl + "user/manual/en/setting-up/workflows" }, +]; + +frappe.help.help_links["List/Company"] = [ + { + label: "Company", + url: docsUrl + "user/manual/en/setting-up/company-setup", + }, + { + label: "Managing Multiple Companies", + url: + docsUrl + + "user/manual/en/setting-up/articles/managing-multiple-companies", + }, + { + label: "Delete All Related Transactions for a Company", + url: + docsUrl + + "user/manual/en/setting-up/articles/delete-a-company-and-all-related-transactions", + }, +]; //Accounts -frappe.help.help_links['accounts'] = [ - { label: 'Introduction to Accounts', url: docsUrl + 'user/manual/en/accounts/' }, - { label: 'Chart of Accounts', url: docsUrl + 'user/manual/en/accounts/chart-of-accounts.html' }, - { label: 'Multi Currency Accounting', url: docsUrl + 'user/manual/en/accounts/multi-currency-accounting' }, -] +frappe.help.help_links["modules/Accounts"] = [ + { + label: "Introduction to Accounts", + url: docsUrl + "user/manual/en/accounts/", + }, + { + label: "Chart of Accounts", + url: docsUrl + "user/manual/en/accounts/chart-of-accounts.html", + }, + { + label: "Multi Currency Accounting", + url: docsUrl + "user/manual/en/accounts/multi-currency-accounting", + }, +]; -frappe.help.help_links['account/view/tree'] = [ - { label: 'Chart of Accounts', url: docsUrl + 'user/manual/en/accounts/chart-of-accounts' }, - { label: 'Managing Tree Mastes', url: docsUrl + 'user/manual/en/setting-up/articles/managing-tree-structure-masters' }, -] +frappe.help.help_links["Tree/Account"] = [ + { + label: "Chart of Accounts", + url: docsUrl + "user/manual/en/accounts/chart-of-accounts", + }, + { + label: "Managing Tree Mastes", + url: + docsUrl + + "user/manual/en/setting-up/articles/managing-tree-structure-masters", + }, +]; -frappe.help.help_links['sales-invoice'] = [ - { label: 'Sales Invoice', url: docsUrl + 'user/manual/en/accounts/sales-invoice' }, - { label: 'Accounts Opening Balance', url: docsUrl + 'user/manual/en/accounts/opening-accounts' }, - { label: 'Sales Return', url: docsUrl + 'user/manual/en/stock/sales-return' }, - { label: 'Recurring Sales Invoice', url: docsUrl + 'user/manual/en/accounts/recurring-orders-and-invoices' }, -] +frappe.help.help_links["Form/Sales Invoice"] = [ + { + label: "Sales Invoice", + url: docsUrl + "user/manual/en/accounts/sales-invoice", + }, + { + label: "Accounts Opening Balance", + url: docsUrl + "user/manual/en/accounts/opening-accounts", + }, + { + label: "Sales Return", + url: docsUrl + "user/manual/en/stock/sales-return", + }, + { + label: "Recurring Sales Invoice", + url: docsUrl + "user/manual/en/accounts/recurring-orders-and-invoices", + }, +]; -frappe.help.help_links['sales-invoice'] = [ - { label: 'Sales Invoice', url: docsUrl + 'user/manual/en/accounts/sales-invoice' }, - { label: 'Accounts Opening Balance', url: docsUrl + 'user/manual/en/accounts/opening-accounts' }, - { label: 'Sales Return', url: docsUrl + 'user/manual/en/stock/sales-return' }, - { label: 'Recurring Sales Invoice', url: docsUrl + 'user/manual/en/accounts/recurring-orders-and-invoices' }, -] +frappe.help.help_links["List/Sales Invoice"] = [ + { + label: "Sales Invoice", + url: docsUrl + "user/manual/en/accounts/sales-invoice", + }, + { + label: "Accounts Opening Balance", + url: docsUrl + "user/manual/en/accounts/opening-accounts", + }, + { + label: "Sales Return", + url: docsUrl + "user/manual/en/stock/sales-return", + }, + { + label: "Recurring Sales Invoice", + url: docsUrl + "user/manual/en/accounts/recurring-orders-and-invoices", + }, +]; -frappe.help.help_links['pos'] = [ - { label: 'Point of Sale Invoice', url: docsUrl + 'user/manual/en/accounts/point-of-sale-pos-invoice' }, -] +frappe.help.help_links["pos"] = [ + { + label: "Point of Sale Invoice", + url: docsUrl + "user/manual/en/accounts/point-of-sale-pos-invoice", + }, +]; -frappe.help.help_links['pos-profile'] = [ - { label: 'Point of Sale Profile', url: docsUrl + 'user/manual/en/setting-up/pos-setting' }, -] +frappe.help.help_links["List/POS Profile"] = [ + { + label: "Point of Sale Profile", + url: docsUrl + "user/manual/en/setting-up/pos-setting", + }, +]; -frappe.help.help_links['purchase-invoice'] = [ - { label: 'Purchase Invoice', url: docsUrl + 'user/manual/en/accounts/purchase-invoice' }, - { label: 'Accounts Opening Balance', url: docsUrl + 'user/manual/en/accounts/opening-accounts' }, - { label: 'Recurring Purchase Invoice', url: docsUrl + 'user/manual/en/accounts/recurring-orders-and-invoices' }, -] +frappe.help.help_links["List/Purchase Invoice"] = [ + { + label: "Purchase Invoice", + url: docsUrl + "user/manual/en/accounts/purchase-invoice", + }, + { + label: "Accounts Opening Balance", + url: docsUrl + "user/manual/en/accounts/opening-accounts", + }, + { + label: "Recurring Purchase Invoice", + url: docsUrl + "user/manual/en/accounts/recurring-orders-and-invoices", + }, +]; -frappe.help.help_links['journal-entry'] = [ - { label: 'Journal Entry', url: docsUrl + 'user/manual/en/accounts/journal-entry' }, - { label: 'Advance Payment Entry', url: docsUrl + 'user/manual/en/accounts/advance-payment-entry' }, - { label: 'Accounts Opening Balance', url: docsUrl + 'user/manual/en/accounts/opening-accounts' }, -] +frappe.help.help_links["List/Journal Entry"] = [ + { + label: "Journal Entry", + url: docsUrl + "user/manual/en/accounts/journal-entry", + }, + { + label: "Advance Payment Entry", + url: docsUrl + "user/manual/en/accounts/advance-payment-entry", + }, + { + label: "Accounts Opening Balance", + url: docsUrl + "user/manual/en/accounts/opening-accounts", + }, +]; -frappe.help.help_links['payment-entry'] = [ - { label: 'Payment Entry', url: docsUrl + 'user/manual/en/accounts/payment-entry' }, -] +frappe.help.help_links["List/Payment Entry"] = [ + { + label: "Payment Entry", + url: docsUrl + "user/manual/en/accounts/payment-entry", + }, +]; -frappe.help.help_links['payment-request'] = [ - { label: 'Payment Request', url: docsUrl + 'user/manual/en/accounts/payment-request' }, -] +frappe.help.help_links["List/Payment Request"] = [ + { + label: "Payment Request", + url: docsUrl + "user/manual/en/accounts/payment-request", + }, +]; -frappe.help.help_links['asset'] = [ - { label: 'Managing Fixed Assets', url: docsUrl + 'user/manual/en/accounts/managing-fixed-assets' }, -] +frappe.help.help_links["List/Asset"] = [ + { + label: "Managing Fixed Assets", + url: docsUrl + "user/manual/en/accounts/managing-fixed-assets", + }, +]; -frappe.help.help_links['asset-category'] = [ - { label: 'Asset Category', url: docsUrl + 'user/manual/en/accounts/managing-fixed-assets' }, -] +frappe.help.help_links["List/Asset Category"] = [ + { + label: "Asset Category", + url: docsUrl + "user/manual/en/accounts/managing-fixed-assets", + }, +]; -frappe.help.help_links['cost-center/view/tree'] = [ - { label: 'Budgeting', url: docsUrl + 'user/manual/en/accounts/budgeting' }, -] +frappe.help.help_links["Tree/Cost Center"] = [ + { label: "Budgeting", url: docsUrl + "user/manual/en/accounts/budgeting" }, +]; -frappe.help.help_links['item'] = [ - { label: 'Item', url: docsUrl + 'user/manual/en/stock/item' }, - { label: 'Item Price', url: docsUrl + 'user/manual/en/stock/item/item-price' }, - { label: 'Barcode', url: docsUrl + 'user/manual/en/stock/articles/track-items-using-barcode' }, - { label: 'Item Wise Taxation', url: docsUrl + 'user/manual/en/accounts/item-wise-taxation' }, - { label: 'Managing Fixed Assets', url: docsUrl + 'user/manual/en/accounts/managing-fixed-assets' }, - { label: 'Item Codification', url: docsUrl + 'user/manual/en/stock/item/item-codification' }, - { label: 'Item Variants', url: docsUrl + 'user/manual/en/stock/item/item-variants' }, - { label: 'Item Valuation', url: docsUrl + 'user/manual/en/stock/item/item-valuation-fifo-and-moving-average' }, -] +frappe.help.help_links["List/Item"] = [ + { label: "Item", url: docsUrl + "user/manual/en/stock/item" }, + { + label: "Item Price", + url: docsUrl + "user/manual/en/stock/item/item-price", + }, + { + label: "Barcode", + url: + docsUrl + "user/manual/en/stock/articles/track-items-using-barcode", + }, + { + label: "Item Wise Taxation", + url: docsUrl + "user/manual/en/accounts/item-wise-taxation", + }, + { + label: "Managing Fixed Assets", + url: docsUrl + "user/manual/en/accounts/managing-fixed-assets", + }, + { + label: "Item Codification", + url: docsUrl + "user/manual/en/stock/item/item-codification", + }, + { + label: "Item Variants", + url: docsUrl + "user/manual/en/stock/item/item-variants", + }, + { + label: "Item Valuation", + url: + docsUrl + + "user/manual/en/stock/item/item-valuation-fifo-and-moving-average", + }, +]; -frappe.help.help_links['purchase-receipt'] = [ - { label: 'Purchase Receipt', url: docsUrl + 'user/manual/en/stock/purchase-receipt' }, - { label: 'Barcode', url: docsUrl + 'user/manual/en/stock/articles/track-items-using-barcode' }, -] +frappe.help.help_links["Form/Item"] = [ + { label: "Item", url: docsUrl + "user/manual/en/stock/item" }, + { + label: "Item Price", + url: docsUrl + "user/manual/en/stock/item/item-price", + }, + { + label: "Barcode", + url: + docsUrl + "user/manual/en/stock/articles/track-items-using-barcode", + }, + { + label: "Item Wise Taxation", + url: docsUrl + "user/manual/en/accounts/item-wise-taxation", + }, + { + label: "Managing Fixed Assets", + url: docsUrl + "user/manual/en/accounts/managing-fixed-assets", + }, + { + label: "Item Codification", + url: docsUrl + "user/manual/en/stock/item/item-codification", + }, + { + label: "Item Variants", + url: docsUrl + "user/manual/en/stock/item/item-variants", + }, + { + label: "Item Valuation", + url: + docsUrl + + "user/manual/en/stock/item/item-valuation-fifo-and-moving-average", + }, +]; -frappe.help.help_links['delivery-note'] = [ - { label: 'Delivery Note', url: docsUrl + 'user/manual/en/stock/delivery-note' }, - { label: 'Barcode', url: docsUrl + 'user/manual/en/stock/articles/track-items-using-barcode' }, - { label: 'Sales Return', url: docsUrl + 'user/manual/en/stock/sales-return' }, -] +frappe.help.help_links["List/Purchase Receipt"] = [ + { + label: "Purchase Receipt", + url: docsUrl + "user/manual/en/stock/purchase-receipt", + }, + { + label: "Barcode", + url: + docsUrl + "user/manual/en/stock/articles/track-items-using-barcode", + }, +]; -frappe.help.help_links['delivery-note'] = [ - { label: 'Delivery Note', url: docsUrl + 'user/manual/en/stock/delivery-note' }, - { label: 'Sales Return', url: docsUrl + 'user/manual/en/stock/sales-return' }, - { label: 'Barcode', url: docsUrl + 'user/manual/en/stock/articles/track-items-using-barcode' }, - { label: 'Subcontracting', url: docsUrl + 'user/manual/en/manufacturing/subcontracting' }, -] +frappe.help.help_links["List/Delivery Note"] = [ + { + label: "Delivery Note", + url: docsUrl + "user/manual/en/stock/delivery-note", + }, + { + label: "Barcode", + url: + docsUrl + "user/manual/en/stock/articles/track-items-using-barcode", + }, + { + label: "Sales Return", + url: docsUrl + "user/manual/en/stock/sales-return", + }, +]; -frappe.help.help_links['installation-note'] = [ - { label: 'Installation Note', url: docsUrl + 'user/manual/en/stock/installation-note' }, -] +frappe.help.help_links["Form/Delivery Note"] = [ + { + label: "Delivery Note", + url: docsUrl + "user/manual/en/stock/delivery-note", + }, + { + label: "Sales Return", + url: docsUrl + "user/manual/en/stock/sales-return", + }, + { + label: "Barcode", + url: + docsUrl + "user/manual/en/stock/articles/track-items-using-barcode", + }, + { + label: "Subcontracting", + url: docsUrl + "user/manual/en/manufacturing/subcontracting", + }, +]; +frappe.help.help_links["List/Installation Note"] = [ + { + label: "Installation Note", + url: docsUrl + "user/manual/en/stock/installation-note", + }, +]; +frappe.help.help_links["Tree"] = [ + { + label: "Managing Tree Structure Masters", + url: + docsUrl + + "user/manual/en/setting-up/articles/managing-tree-structure-masters", + }, +]; -frappe.help.help_links['budget'] = [ - { label: 'Budgeting', url: docsUrl + 'user/manual/en/accounts/budgeting' }, -] +frappe.help.help_links["List/Budget"] = [ + { label: "Budgeting", url: docsUrl + "user/manual/en/accounts/budgeting" }, +]; //Stock -frappe.help.help_links['material-request'] = [ - { label: 'Material Request', url: docsUrl + 'user/manual/en/stock/material-request' }, - { label: 'Auto-creation of Material Request', url: docsUrl + 'user/manual/en/stock/articles/auto-creation-of-material-request' }, -] +frappe.help.help_links["List/Material Request"] = [ + { + label: "Material Request", + url: docsUrl + "user/manual/en/stock/material-request", + }, + { + label: "Auto-creation of Material Request", + url: + docsUrl + + "user/manual/en/stock/articles/auto-creation-of-material-request", + }, +]; -frappe.help.help_links['stock-entry'] = [ - { label: 'Stock Entry', url: docsUrl + 'user/manual/en/stock/stock-entry' }, - { label: 'Stock Entry Types', url: docsUrl + 'user/manual/en/stock/articles/stock-entry-purpose' }, - { label: 'Repack Entry', url: docsUrl + 'user/manual/en/stock/articles/repack-entry' }, - { label: 'Opening Stock', url: docsUrl + 'user/manual/en/stock/opening-stock' }, - { label: 'Subcontracting', url: docsUrl + 'user/manual/en/manufacturing/subcontracting' }, -] +frappe.help.help_links["Form/Material Request"] = [ + { + label: "Material Request", + url: docsUrl + "user/manual/en/stock/material-request", + }, + { + label: "Auto-creation of Material Request", + url: + docsUrl + + "user/manual/en/stock/articles/auto-creation-of-material-request", + }, +]; -frappe.help.help_links['warehouse/view/tree'] = [ - { label: 'Warehouse', url: docsUrl + 'user/manual/en/stock/warehouse' }, -] +frappe.help.help_links["Form/Stock Entry"] = [ + { label: "Stock Entry", url: docsUrl + "user/manual/en/stock/stock-entry" }, + { + label: "Stock Entry Types", + url: docsUrl + "user/manual/en/stock/articles/stock-entry-purpose", + }, + { + label: "Repack Entry", + url: docsUrl + "user/manual/en/stock/articles/repack-entry", + }, + { + label: "Opening Stock", + url: docsUrl + "user/manual/en/stock/opening-stock", + }, + { + label: "Subcontracting", + url: docsUrl + "user/manual/en/manufacturing/subcontracting", + }, +]; -frappe.help.help_links['serial-no'] = [ - { label: 'Serial No', url: docsUrl + 'user/manual/en/stock/serial-no' }, -] +frappe.help.help_links["List/Stock Entry"] = [ + { label: "Stock Entry", url: docsUrl + "user/manual/en/stock/stock-entry" }, +]; -frappe.help.help_links['batch'] = [ - { label: 'Batch', url: docsUrl + 'user/manual/en/stock/batch' }, -] +frappe.help.help_links["Tree/Warehouse"] = [ + { label: "Warehouse", url: docsUrl + "user/manual/en/stock/warehouse" }, +]; -frappe.help.help_links['packing-slip'] = [ - { label: 'Packing Slip', url: docsUrl + 'user/manual/en/stock/tools/packing-slip' }, -] +frappe.help.help_links["List/Serial No"] = [ + { label: "Serial No", url: docsUrl + "user/manual/en/stock/serial-no" }, +]; -frappe.help.help_links['quality-inspection'] = [ - { label: 'Quality Inspection', url: docsUrl + 'user/manual/en/stock/tools/quality-inspection' }, -] +frappe.help.help_links["Form/Serial No"] = [ + { label: "Serial No", url: docsUrl + "user/manual/en/stock/serial-no" }, +]; -frappe.help.help_links['landed-cost-voucher'] = [ - { label: 'Landed Cost Voucher', url: docsUrl + 'user/manual/en/stock/tools/landed-cost-voucher' }, -] +frappe.help.help_links["Form/Batch"] = [ + { label: "Batch", url: docsUrl + "user/manual/en/stock/batch" }, +]; -frappe.help.help_links['item-group/view/tree'] = [ - { label: 'Item Group', url: docsUrl + 'user/manual/en/stock/setup/item-group' }, -] +frappe.help.help_links["Form/Packing Slip"] = [ + { + label: "Packing Slip", + url: docsUrl + "user/manual/en/stock/tools/packing-slip", + }, +]; -frappe.help.help_links['item-attribute'] = [ - { label: 'Item Attribute', url: docsUrl + 'user/manual/en/stock/setup/item-attribute' }, -] +frappe.help.help_links["Form/Quality Inspection"] = [ + { + label: "Quality Inspection", + url: docsUrl + "user/manual/en/stock/tools/quality-inspection", + }, +]; -frappe.help.help_links['uom'] = [ - { label: 'Fractions in UOM', url: docsUrl + 'user/manual/en/stock/articles/managing-fractions-in-uom' }, -] +frappe.help.help_links["Form/Landed Cost Voucher"] = [ + { + label: "Landed Cost Voucher", + url: docsUrl + "user/manual/en/stock/tools/landed-cost-voucher", + }, +]; -frappe.help.help_links['stock-reconciliation'] = [ - { label: 'Opening Stock Entry', url: docsUrl + 'user/manual/en/stock/opening-stock' }, -] +frappe.help.help_links["Tree/Item Group"] = [ + { + label: "Item Group", + url: docsUrl + "user/manual/en/stock/setup/item-group", + }, +]; + +frappe.help.help_links["Form/Item Attribute"] = [ + { + label: "Item Attribute", + url: docsUrl + "user/manual/en/stock/setup/item-attribute", + }, +]; + +frappe.help.help_links["Form/UOM"] = [ + { + label: "Fractions in UOM", + url: + docsUrl + "user/manual/en/stock/articles/managing-fractions-in-uom", + }, +]; + +frappe.help.help_links["Form/Stock Reconciliation"] = [ + { + label: "Opening Stock Entry", + url: docsUrl + "user/manual/en/stock/opening-stock", + }, +]; //CRM -frappe.help.help_links['lead'] = [ - { label: 'Lead', url: docsUrl + 'user/manual/en/CRM/lead' }, -] +frappe.help.help_links["Form/Lead"] = [ + { label: "Lead", url: docsUrl + "user/manual/en/CRM/lead" }, +]; -frappe.help.help_links['opportunity'] = [ - { label: 'Opportunity', url: docsUrl + 'user/manual/en/CRM/opportunity' }, -] +frappe.help.help_links["Form/Opportunity"] = [ + { label: "Opportunity", url: docsUrl + "user/manual/en/CRM/opportunity" }, +]; -frappe.help.help_links['address'] = [ - { label: 'Address', url: docsUrl + 'user/manual/en/CRM/address' }, -] +frappe.help.help_links["Form/Address"] = [ + { label: "Address", url: docsUrl + "user/manual/en/CRM/address" }, +]; -frappe.help.help_links['contact'] = [ - { label: 'Contact', url: docsUrl + 'user/manual/en/CRM/contact' }, -] +frappe.help.help_links["Form/Contact"] = [ + { label: "Contact", url: docsUrl + "user/manual/en/CRM/contact" }, +]; -frappe.help.help_links['newsletter'] = [ - { label: 'Newsletter', url: docsUrl + 'user/manual/en/CRM/newsletter' }, -] +frappe.help.help_links["Form/Newsletter"] = [ + { label: "Newsletter", url: docsUrl + "user/manual/en/CRM/newsletter" }, +]; -frappe.help.help_links['campaign'] = [ - { label: 'Campaign', url: docsUrl + 'user/manual/en/CRM/setup/campaign' }, -] +frappe.help.help_links["Form/Campaign"] = [ + { label: "Campaign", url: docsUrl + "user/manual/en/CRM/setup/campaign" }, +]; -frappe.help.help_links['sales-person/view/tree'] = [ - { label: 'Sales Person', url: docsUrl + 'user/manual/en/CRM/setup/sales-person' }, -] +frappe.help.help_links["Tree/Sales Person"] = [ + { + label: "Sales Person", + url: docsUrl + "user/manual/en/CRM/setup/sales-person", + }, +]; -frappe.help.help_links['sales-person'] = [ - { label: 'Sales Person Target', url: docsUrl + 'user/manual/en/selling/setup/sales-person-target-allocation' }, -] +frappe.help.help_links["Form/Sales Person"] = [ + { + label: "Sales Person Target", + url: + docsUrl + + "user/manual/en/selling/setup/sales-person-target-allocation", + }, +]; + +//Support + +frappe.help.help_links["List/Feedback Trigger"] = [ + { + label: "Feedback Trigger", + url: docsUrl + "user/manual/en/setting-up/feedback/setting-up-feedback", + }, +]; + +frappe.help.help_links["List/Feedback Request"] = [ + { + label: "Feedback Request", + url: docsUrl + "user/manual/en/setting-up/feedback/submit-feedback", + }, +]; + +frappe.help.help_links["List/Feedback Request"] = [ + { + label: "Feedback Request", + url: docsUrl + "user/manual/en/setting-up/feedback/submit-feedback", + }, +]; //Manufacturing -frappe.help.help_links['bom'] = [ - { label: 'Bill of Material', url: docsUrl + 'user/manual/en/manufacturing/bill-of-materials' }, - { label: 'Nested BOM Structure', url: docsUrl + 'user/manual/en/manufacturing/articles/nested-bom-structure' }, -] +frappe.help.help_links["Form/BOM"] = [ + { + label: "Bill of Material", + url: docsUrl + "user/manual/en/manufacturing/bill-of-materials", + }, + { + label: "Nested BOM Structure", + url: + docsUrl + + "user/manual/en/manufacturing/articles/nested-bom-structure", + }, +]; -frappe.help.help_links['work-order'] = [ - { label: 'Work Order', url: docsUrl + 'user/manual/en/manufacturing/work-order' }, -] +frappe.help.help_links["Form/Work Order"] = [ + { + label: "Work Order", + url: docsUrl + "user/manual/en/manufacturing/work-order", + }, +]; -frappe.help.help_links['workstation'] = [ - { label: 'Workstation', url: docsUrl + 'user/manual/en/manufacturing/workstation' }, -] +frappe.help.help_links["Form/Workstation"] = [ + { + label: "Workstation", + url: docsUrl + "user/manual/en/manufacturing/workstation", + }, +]; -frappe.help.help_links['operation'] = [ - { label: 'Operation', url: docsUrl + 'user/manual/en/manufacturing/operation' }, -] +frappe.help.help_links["Form/Operation"] = [ + { + label: "Operation", + url: docsUrl + "user/manual/en/manufacturing/operation", + }, +]; -frappe.help.help_links['bom-update-tool'] = [ - { label: 'BOM Update Tool', url: docsUrl + 'user/manual/en/manufacturing/tools/bom-update-tool' }, -] +frappe.help.help_links["Form/BOM Update Tool"] = [ + { + label: "BOM Update Tool", + url: docsUrl + "user/manual/en/manufacturing/tools/bom-update-tool", + }, +]; //Customize -frappe.help.help_links['customize-form'] = [ - { label: 'Custom Field', url: docsUrl + 'user/manual/en/customize-erpnext/custom-field' }, - { label: 'Customize Field', url: docsUrl + 'user/manual/en/customize-erpnext/customize-form' }, -] +frappe.help.help_links["Form/Customize Form"] = [ + { + label: "Custom Field", + url: docsUrl + "user/manual/en/customize-erpnext/custom-field", + }, + { + label: "Customize Field", + url: docsUrl + "user/manual/en/customize-erpnext/customize-form", + }, +]; -frappe.help.help_links['custom-field'] = [ - { label: 'Custom Field', url: docsUrl + 'user/manual/en/customize-erpnext/custom-field' }, -] +frappe.help.help_links["Form/Custom Field"] = [ + { + label: "Custom Field", + url: docsUrl + "user/manual/en/customize-erpnext/custom-field", + }, +]; -frappe.help.help_links['custom-field'] = [ - { label: 'Custom Field', url: docsUrl + 'user/manual/en/customize-erpnext/custom-field' }, -] +frappe.help.help_links["Form/Custom Field"] = [ + { + label: "Custom Field", + url: docsUrl + "user/manual/en/customize-erpnext/custom-field", + }, +]; diff --git a/erpnext/quality_management/doctype/quality_feedback/quality_feedback.py b/erpnext/quality_management/doctype/quality_feedback/quality_feedback.py index bf82cc080a..5a8ec73cfe 100644 --- a/erpnext/quality_management/doctype/quality_feedback/quality_feedback.py +++ b/erpnext/quality_management/doctype/quality_feedback/quality_feedback.py @@ -7,6 +7,7 @@ import frappe from frappe.model.document import Document class QualityFeedback(Document): + @frappe.whitelist() def set_parameters(self): if self.template and not getattr(self, 'parameters', []): for d in frappe.get_doc('Quality Feedback Template', self.template).parameters: diff --git a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.html b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.html index 888b2da48e..369a4001ef 100644 --- a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.html +++ b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.html @@ -109,7 +109,7 @@ - {{__("Suppliies made to Composition Taxable Persons")}} + {{__("Supplies made to Composition Taxable Persons")}} {% for row in data.inter_sup.comp_details %} {% if row %} diff --git a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py index 68c8a0d4d3..a5dd5a2e09 100644 --- a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py +++ b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py @@ -172,7 +172,6 @@ class GSTR3BReport(Document): self.json_output = frappe.as_json(self.report_dict) def set_inward_nil_exempt(self, inward_nil_exempt): - self.report_dict["inward_sup"]["isup_details"][0]["inter"] = flt(inward_nil_exempt.get("gst").get("inter"), 2) self.report_dict["inward_sup"]["isup_details"][0]["intra"] = flt(inward_nil_exempt.get("gst").get("intra"), 2) self.report_dict["inward_sup"]["isup_details"][1]["inter"] = flt(inward_nil_exempt.get("non_gst").get("inter"), 2) @@ -238,7 +237,6 @@ class GSTR3BReport(Document): self.report_dict[supply_type][supply_category]["txval"] += flt(txval, 2) def set_inter_state_supply(self, inter_state_supply): - osup_det = self.report_dict["sup_details"]["osup_det"] for key, value in iteritems(inter_state_supply): @@ -349,13 +347,20 @@ class GSTR3BReport(Document): return inter_state_supply_details def get_inward_nil_exempt(self, state): - inward_nil_exempt = frappe.db.sql(""" select p.place_of_supply, sum(i.base_amount) as base_amount, i.is_nil_exempt, i.is_non_gst from `tabPurchase Invoice` p , `tabPurchase Invoice Item` i where p.docstatus = 1 and p.name = i.parent - and i.is_nil_exempt = 1 or i.is_non_gst = 1 and + and p.gst_category != 'Registered Composition' + and (i.is_nil_exempt = 1 or i.is_non_gst = 1) and month(p.posting_date) = %s and year(p.posting_date) = %s and p.company = %s and p.company_gstin = %s - group by p.place_of_supply """, (self.month_no, self.year, self.company, self.gst_details.get("gstin")), as_dict=1) + group by p.place_of_supply, i.is_nil_exempt, i.is_non_gst""", (self.month_no, self.year, self.company, self.gst_details.get("gstin")), as_dict=1) + + inward_nil_exempt += frappe.db.sql("""SELECT sum(base_net_total) as base_amount, gst_category, place_of_supply + FROM `tabPurchase Invoice` + WHERE docstatus = 1 and gst_category = 'Registered Composition' + and month(posting_date) = %s and year(posting_date) = %s + and company = %s and company_gstin = %s + group by place_of_supply""", (self.month_no, self.year, self.company, self.gst_details.get("gstin")), as_dict=1) inward_nil_exempt_details = { "gst": { @@ -370,9 +375,11 @@ class GSTR3BReport(Document): for d in inward_nil_exempt: if d.place_of_supply: - if d.is_nil_exempt == 1 and state == d.place_of_supply.split("-")[1]: + if (d.is_nil_exempt == 1 or d.get('gst_category') == 'Registered Composition') \ + and state == d.place_of_supply.split("-")[1]: inward_nil_exempt_details["gst"]["intra"] += d.base_amount - elif d.is_nil_exempt == 1 and state != d.place_of_supply.split("-")[1]: + elif (d.is_nil_exempt == 1 or d.get('gst_category') == 'Registered Composition') \ + and state != d.place_of_supply.split("-")[1]: inward_nil_exempt_details["gst"]["inter"] += d.base_amount elif d.is_non_gst == 1 and state == d.place_of_supply.split("-")[1]: inward_nil_exempt_details["non_gst"]["intra"] += d.base_amount diff --git a/erpnext/regional/doctype/gstr_3b_report/test_gstr_3b_report.py b/erpnext/regional/doctype/gstr_3b_report/test_gstr_3b_report.py index 023b4ed22b..ef8af24c42 100644 --- a/erpnext/regional/doctype/gstr_3b_report/test_gstr_3b_report.py +++ b/erpnext/regional/doctype/gstr_3b_report/test_gstr_3b_report.py @@ -64,7 +64,7 @@ class TestGSTR3BReport(unittest.TestCase): self.assertEqual(output["sup_details"]["osup_zero"]["iamt"], 18), self.assertEqual(output["inter_sup"]["unreg_details"][0]["iamt"], 18), self.assertEqual(output["sup_details"]["osup_nil_exmp"]["txval"], 100), - self.assertEqual(output["inward_sup"]["isup_details"][0]["inter"], 250) + self.assertEqual(output["inward_sup"]["isup_details"][0]["intra"], 250) self.assertEqual(output["itc_elg"]["itc_avl"][4]["samt"], 22.50) self.assertEqual(output["itc_elg"]["itc_avl"][4]["camt"], 22.50) @@ -228,6 +228,19 @@ def create_purchase_invoices(): pi1.submit() + pi2 = make_purchase_invoice(company="_Test Company GST", + customer = '_Test Registered Supplier', + currency = 'INR', + item = 'Milk', + warehouse = 'Finished Goods - _GST', + expense_account = 'Cost of Goods Sold - _GST', + cost_center = 'Main - _GST', + rate=250, + qty=1, + do_not_save=1 + ) + pi2.submit() + def make_suppliers(): if not frappe.db.exists("Supplier", "_Test Registered Supplier"): frappe.get_doc({ diff --git a/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.py b/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.py index 5bbd5750f9..41a0f1193b 100644 --- a/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.py +++ b/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.py @@ -50,6 +50,7 @@ class TaxExemption80GCertificate(Document): frappe.throw(_('Please set the {0} for company {1}').format(frappe.bold('PAN Number'), get_link_to_form('Company', self.company))) + @frappe.whitelist() def set_company_address(self): address = get_company_address(self.company) self.company_address = address.company_address @@ -70,6 +71,7 @@ class TaxExemption80GCertificate(Document): else: self.title = self.donor_name + @frappe.whitelist() def get_payments(self): if not self.member: frappe.throw(_('Please select a Member first.')) @@ -81,7 +83,7 @@ class TaxExemption80GCertificate(Document): 'from_date': ['between', (fiscal_year.year_start_date, fiscal_year.year_end_date)], 'to_date': ['between', (fiscal_year.year_start_date, fiscal_year.year_end_date)], 'membership_status': ('!=', 'Cancelled') - }, ['from_date', 'amount', 'name', 'invoice', 'payment_id']) + }, ['from_date', 'amount', 'name', 'invoice', 'payment_id'], order_by='from_date') if not memberships: frappe.msgprint(_('No Membership Payments found against the Member {0}').format(self.member)) diff --git a/erpnext/regional/india/e_invoice/einv_validation.json b/erpnext/regional/india/e_invoice/einv_validation.json index 86290cfe52..f4a3542a60 100644 --- a/erpnext/regional/india/e_invoice/einv_validation.json +++ b/erpnext/regional/india/e_invoice/einv_validation.json @@ -919,7 +919,8 @@ "minLength": 1, "maxLength": 15, "pattern": "^([0-9A-Z/-]){1,15}$", - "description": "Tranport Document Number" + "description": "Tranport Document Number", + "validationMsg": "Transport Receipt No is invalid" }, "TransDocDt": { "type": "string", diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index ee49aae050..f7689cfa19 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -5,6 +5,7 @@ from __future__ import unicode_literals import frappe, os, json from frappe.custom.doctype.custom_field.custom_field import create_custom_fields +from frappe.custom.doctype.property_setter.property_setter import make_property_setter from frappe.permissions import add_permission, update_permission_property from erpnext.regional.india import states from erpnext.accounts.utils import get_fiscal_year, FiscalYearError @@ -18,6 +19,7 @@ def setup(company=None, patch=True): # TODO: for all countries def setup_company_independent_fixtures(): make_custom_fields() + make_property_setters() add_permissions() add_custom_roles_for_reports() frappe.enqueue('erpnext.regional.india.setup.add_hsn_sac_codes', now=frappe.flags.in_test) @@ -110,6 +112,11 @@ def add_print_formats(): frappe.db.set_value("Print Format", "GST Tax Invoice", "disabled", 0) frappe.db.set_value("Print Format", "GST E-Invoice", "disabled", 0) +def make_property_setters(): + # GST rules do not allow for an invoice no. bigger than 16 characters + make_property_setter('Sales Invoice', 'naming_series', 'options', 'SINV-.YY.-\nSRET-.YY.-', '') + make_property_setter('Purchase Invoice', 'naming_series', 'options', 'PINV-.YY.-\nPRET-.YY.-', '') + def make_custom_fields(update=True): hsn_sac_field = dict(fieldname='gst_hsn_code', label='HSN/SAC', fieldtype='Data', fetch_from='item_code.gst_hsn_code', insert_after='description', @@ -860,4 +867,4 @@ def create_gratuity_rule(): }) rule.flags.ignore_mandatory = True - rule.save() \ No newline at end of file + rule.save() diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index e24bd6c3d0..3637de438c 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -823,8 +823,11 @@ def get_regional_round_off_accounts(company, account_list): return gst_accounts = get_gst_accounts(company) - gst_account_list = gst_accounts.get('cgst_account') + gst_accounts.get('sgst_account') \ - + gst_accounts.get('igst_account') + + gst_account_list = [] + for account in ['cgst_account', 'sgst_account', 'igst_account']: + if account in gst_accounts: + gst_account_list += gst_accounts.get(account) account_list.extend(gst_account_list) diff --git a/erpnext/regional/italy/sales_invoice.js b/erpnext/regional/italy/sales_invoice.js index 586a52937b..b54ac53812 100644 --- a/erpnext/regional/italy/sales_invoice.js +++ b/erpnext/regional/italy/sales_invoice.js @@ -11,15 +11,10 @@ erpnext.setup_e_invoice_button = (doctype) => { callback: function(r) { frm.reload_doc(); if(r.message) { - var w = window.open( - frappe.urllib.get_full_url( - "/api/method/erpnext.regional.italy.utils.download_e_invoice_file?" - + "file_name=" + r.message - ) - ) - if (!w) { - frappe.msgprint(__("Please enable pop-ups")); return; - } + open_url_post(frappe.request.url, { + cmd: 'frappe.core.doctype.file.file.download_file', + file_url: r.message + }); } } }); diff --git a/erpnext/regional/italy/setup.py b/erpnext/regional/italy/setup.py index 95b92e76a6..a1f5bb9836 100644 --- a/erpnext/regional/italy/setup.py +++ b/erpnext/regional/italy/setup.py @@ -128,11 +128,8 @@ def make_custom_fields(update=True): fetch_from="company.vat_collectability"), dict(fieldname='sb_e_invoicing_reference', label='E-Invoicing', fieldtype='Section Break', insert_after='against_income_account', print_hide=1), - dict(fieldname='company_tax_id', label='Company Tax ID', - fieldtype='Data', insert_after='sb_e_invoicing_reference', print_hide=1, read_only=1, - fetch_from="company.tax_id"), dict(fieldname='company_fiscal_code', label='Company Fiscal Code', - fieldtype='Data', insert_after='company_tax_id', print_hide=1, read_only=1, + fieldtype='Data', insert_after='sb_e_invoicing_reference', print_hide=1, read_only=1, fetch_from="company.fiscal_code"), dict(fieldname='company_fiscal_regime', label='Company Fiscal Regime', fieldtype='Data', insert_after='company_fiscal_code', print_hide=1, read_only=1, @@ -217,4 +214,4 @@ def add_permissions(): update_permission_property(doctype, 'Accounts Manager', 0, 'delete', 1) add_permission(doctype, 'Accounts Manager', 1) update_permission_property(doctype, 'Accounts Manager', 1, 'write', 1) - update_permission_property(doctype, 'Accounts Manager', 1, 'create', 1) \ No newline at end of file + update_permission_property(doctype, 'Accounts Manager', 1, 'create', 1) diff --git a/erpnext/regional/italy/utils.py b/erpnext/regional/italy/utils.py index 6842fb2a61..08573cddcd 100644 --- a/erpnext/regional/italy/utils.py +++ b/erpnext/regional/italy/utils.py @@ -1,6 +1,8 @@ from __future__ import unicode_literals -import frappe, json, os +import io +import json +import frappe from frappe.utils import flt, cstr from erpnext.controllers.taxes_and_totals import get_itemised_tax from frappe import _ @@ -28,20 +30,22 @@ def update_itemised_tax_data(doc): @frappe.whitelist() def export_invoices(filters=None): - saved_xmls = [] + frappe.has_permission('Sales Invoice', throw=True) - invoices = frappe.get_all("Sales Invoice", filters=get_conditions(filters), fields=["*"]) + invoices = frappe.get_all( + "Sales Invoice", + filters=get_conditions(filters), + fields=["name", "company_tax_id"] + ) - for invoice in invoices: - attachments = get_e_invoice_attachments(invoice) - saved_xmls += [attachment.file_name for attachment in attachments] + attachments = get_e_invoice_attachments(invoices) - zip_filename = "{0}-einvoices.zip".format(frappe.utils.get_datetime().strftime("%Y%m%d_%H%M%S")) + zip_filename = "{0}-einvoices.zip".format( + frappe.utils.get_datetime().strftime("%Y%m%d_%H%M%S")) - download_zip(saved_xmls, zip_filename) + download_zip(attachments, zip_filename) -@frappe.whitelist() def prepare_invoice(invoice, progressive_number): #set company information company = frappe.get_doc("Company", invoice.company) @@ -98,7 +102,7 @@ def prepare_invoice(invoice, progressive_number): def get_conditions(filters): filters = json.loads(filters) - conditions = {"docstatus": 1} + conditions = {"docstatus": 1, "company_tax_id": ("!=", "")} if filters.get("company"): conditions["company"] = filters["company"] if filters.get("customer"): conditions["customer"] = filters["customer"] @@ -111,23 +115,22 @@ def get_conditions(filters): return conditions -#TODO: Use function from frappe once PR #6853 is merged. + def download_zip(files, output_filename): - from zipfile import ZipFile + import zipfile - input_files = [frappe.get_site_path('private', 'files', filename) for filename in files] - output_path = frappe.get_site_path('private', 'files', output_filename) + zip_stream = io.BytesIO() + with zipfile.ZipFile(zip_stream, 'w', zipfile.ZIP_DEFLATED) as zip_file: + for file in files: + file_path = frappe.utils.get_files_path( + file.file_name, is_private=file.is_private) - with ZipFile(output_path, 'w') as output_zip: - for input_file in input_files: - output_zip.write(input_file, arcname=os.path.basename(input_file)) - - with open(output_path, 'rb') as fileobj: - filedata = fileobj.read() + zip_file.write(file_path, arcname=file.file_name) frappe.local.response.filename = output_filename - frappe.local.response.filecontent = filedata + frappe.local.response.filecontent = zip_stream.getvalue() frappe.local.response.type = "download" + zip_stream.close() def get_invoice_summary(items, taxes): summary_data = frappe._dict() @@ -307,23 +310,12 @@ def prepare_and_attach_invoice(doc, replace=False): @frappe.whitelist() def generate_single_invoice(docname): doc = frappe.get_doc("Sales Invoice", docname) - + frappe.has_permission("Sales Invoice", doc=doc, throw=True) e_invoice = prepare_and_attach_invoice(doc, True) + return e_invoice.file_url - return e_invoice.file_name - -@frappe.whitelist() -def download_e_invoice_file(file_name): - content = None - with open(frappe.get_site_path('private', 'files', file_name), "r") as f: - content = f.read() - - frappe.local.response.filename = file_name - frappe.local.response.filecontent = content - frappe.local.response.type = "download" - -#Delete e-invoice attachment on cancel. +# Delete e-invoice attachment on cancel. def sales_invoice_on_cancel(doc, method): if get_company_country(doc.company) not in ['Italy', 'Italia', 'Italian Republic', 'Repubblica Italiana']: @@ -335,16 +327,38 @@ def sales_invoice_on_cancel(doc, method): def get_company_country(company): return frappe.get_cached_value('Company', company, 'country') -def get_e_invoice_attachments(invoice): - if not invoice.company_tax_id: - return [] +def get_e_invoice_attachments(invoices): + if not isinstance(invoices, list): + if not invoices.company_tax_id: + return + + invoices = [invoices] + + tax_id_map = { + invoice.name: ( + invoice.company_tax_id + if invoice.company_tax_id.startswith("IT") + else "IT" + invoice.company_tax_id + ) for invoice in invoices + } + + attachments = frappe.get_all( + "File", + fields=("name", "file_name", "attached_to_name", "is_private"), + filters= { + "attached_to_name": ('in', tax_id_map), + "attached_to_doctype": 'Sales Invoice' + } + ) out = [] - attachments = get_attachments(invoice.doctype, invoice.name) - company_tax_id = invoice.company_tax_id if invoice.company_tax_id.startswith("IT") else "IT" + invoice.company_tax_id - for attachment in attachments: - if attachment.file_name and attachment.file_name.startswith(company_tax_id) and attachment.file_name.endswith(".xml"): + if ( + attachment.file_name + and attachment.file_name.endswith(".xml") + and attachment.file_name.startswith( + tax_id_map.get(attachment.attached_to_name)) + ): out.append(attachment) return out diff --git a/erpnext/regional/report/gstr_1/gstr_1.py b/erpnext/regional/report/gstr_1/gstr_1.py index 09b04ff367..62faa30e3f 100644 --- a/erpnext/regional/report/gstr_1/gstr_1.py +++ b/erpnext/regional/report/gstr_1/gstr_1.py @@ -78,7 +78,7 @@ class Gstr1Report(object): place_of_supply = invoice_details.get("place_of_supply") ecommerce_gstin = invoice_details.get("ecommerce_gstin") - b2cs_output.setdefault((rate, place_of_supply, ecommerce_gstin, inv),{ + b2cs_output.setdefault((rate, place_of_supply, ecommerce_gstin),{ "place_of_supply": "", "ecommerce_gstin": "", "rate": "", @@ -90,7 +90,7 @@ class Gstr1Report(object): "invoice_value": invoice_details.get("base_grand_total"), }) - row = b2cs_output.get((rate, place_of_supply, ecommerce_gstin, inv)) + row = b2cs_output.get((rate, place_of_supply, ecommerce_gstin)) row["place_of_supply"] = place_of_supply row["ecommerce_gstin"] = ecommerce_gstin row["rate"] = rate diff --git a/erpnext/regional/report/gstr_2/gstr_2.py b/erpnext/regional/report/gstr_2/gstr_2.py index f899349ccc..616c2b853d 100644 --- a/erpnext/regional/report/gstr_2/gstr_2.py +++ b/erpnext/regional/report/gstr_2/gstr_2.py @@ -44,7 +44,7 @@ class Gstr2Report(Gstr1Report): for inv, items_based_on_rate in self.items_based_on_tax_rate.items(): invoice_details = self.invoices.get(inv) for rate, items in items_based_on_rate.items(): - if rate: + if rate or invoice_details.get('gst_category') == 'Registered Composition': if inv not in self.igst_invoices: rate = rate / 2 row, taxable_value = self.get_row_data_for_invoice(inv, invoice_details, rate, items) @@ -86,7 +86,7 @@ class Gstr2Report(Gstr1Report): conditions += opts[1] if self.filters.get("type_of_business") == "B2B": - conditions += "and ifnull(gst_category, '') in ('Registered Regular', 'Deemed Export', 'SEZ') and is_return != 1 " + conditions += "and ifnull(gst_category, '') in ('Registered Regular', 'Deemed Export', 'SEZ', 'Registered Composition') and is_return != 1 " elif self.filters.get("type_of_business") == "CDNR": conditions += """ and is_return = 1 """ diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index c452594608..96b3fa4ccd 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -230,13 +230,20 @@ class Customer(TransactionBase): frappe.db.set(self, "customer_name", newdn) def set_loyalty_program(self): - if self.loyalty_program: return + if self.loyalty_program: + return + loyalty_program = get_loyalty_programs(self) - if not loyalty_program: return + if not loyalty_program: + return + if len(loyalty_program) == 1: self.loyalty_program = loyalty_program[0] else: - frappe.msgprint(_("Multiple Loyalty Program found for the Customer. Please select manually.")) + frappe.msgprint( + _("Multiple Loyalty Programs found for Customer {}. Please select manually.") + .format(frappe.bold(self.customer_name)) + ) def create_onboarding_docs(self, args): defaults = frappe.defaults.get_defaults() @@ -340,7 +347,6 @@ def _set_missing_values(source, target): @frappe.whitelist() def get_loyalty_programs(doc): ''' returns applicable loyalty programs for a customer ''' - from frappe.desk.treeview import get_children lp_details = [] loyalty_programs = frappe.get_all("Loyalty Program", @@ -349,15 +355,33 @@ def get_loyalty_programs(doc): "ifnull(to_date, '2500-01-01')": [">=", today()]}) for loyalty_program in loyalty_programs: - customer_groups = [d.value for d in get_children("Customer Group", loyalty_program.customer_group)] + [loyalty_program.customer_group] - customer_territories = [d.value for d in get_children("Territory", loyalty_program.customer_territory)] + [loyalty_program.customer_territory] - - if (not loyalty_program.customer_group or doc.customer_group in customer_groups)\ - and (not loyalty_program.customer_territory or doc.territory in customer_territories): + if ( + (not loyalty_program.customer_group + or doc.customer_group in get_nested_links( + "Customer Group", + loyalty_program.customer_group, + doc.flags.ignore_permissions + )) + and (not loyalty_program.customer_territory + or doc.territory in get_nested_links( + "Territory", + loyalty_program.customer_territory, + doc.flags.ignore_permissions + )) + ): lp_details.append(loyalty_program.name) return lp_details +def get_nested_links(link_doctype, link_name, ignore_permissions=False): + from frappe.desk.treeview import _get_children + + links = [link_name] + for d in _get_children(link_doctype, link_name, ignore_permissions): + links.append(d.value) + + return links + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_customer_list(doctype, txt, searchfield, start, page_len, filters=None): @@ -572,4 +596,4 @@ def get_customer_primary_contact(doctype, txt, searchfield, start, page_len, fil """, { 'customer': customer, 'txt': '%%%s%%' % txt - }) \ No newline at end of file + }) diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index 5da248c1b5..246f9234a4 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -64,6 +64,7 @@ class Quotation(SellingController): opp = frappe.get_doc("Opportunity", opportunity) opp.set_status(status=status, update=True) + @frappe.whitelist() def declare_enquiry_lost(self, lost_reasons_list, detailed_reason=None): if not self.has_sales_order(): get_lost_reasons = frappe.get_list('Quotation Lost Reason', diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index e56129170c..d9e52e1d69 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -150,7 +150,7 @@ class SalesOrder(SellingController): if enq: frappe.db.sql("update `tabOpportunity` set status = %s where name=%s",(flag,enq[0][0])) - def update_prevdoc_status(self, flag): + def update_prevdoc_status(self, flag=None): for quotation in list(set([d.prevdoc_docname for d in self.get("items")])): if quotation: doc = frappe.get_doc("Quotation", quotation) @@ -372,6 +372,7 @@ class SalesOrder(SellingController): self.indicator_color = "green" self.indicator_title = _("Paid") + @frappe.whitelist() def get_work_order_items(self, for_raw_material_request=0): '''Returns items with BOM that already do not have a linked work order''' items = [] @@ -778,6 +779,7 @@ def get_events(start, end, filters=None): @frappe.whitelist() def make_purchase_order_for_default_supplier(source_name, selected_items=None, target_doc=None): + """Creates Purchase Order for each Supplier. Returns a list of doc objects.""" if not selected_items: return if isinstance(selected_items, string_types): @@ -820,15 +822,16 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=None, t target.stock_qty = (flt(source.stock_qty) - flt(source.ordered_qty)) target.project = source_parent.project - suppliers = [item.get('supplier') for item in selected_items if item.get('supplier') and item.get('supplier')] - suppliers = list(set(suppliers)) + suppliers = [item.get('supplier') for item in selected_items if item.get('supplier')] + suppliers = list(dict.fromkeys(suppliers)) # remove duplicates while preserving order - items_to_map = [item.get('item_code') for item in selected_items if item.get('item_code') and item.get('item_code')] + items_to_map = [item.get('item_code') for item in selected_items if item.get('item_code')] items_to_map = list(set(items_to_map)) if not suppliers: frappe.throw(_("Please set a Supplier against the Items to be considered in the Purchase Order.")) + purchase_orders = [] for supplier in suppliers: doc = get_mapped_doc("Sales Order", source_name, { "Sales Order": { @@ -872,7 +875,9 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=None, t doc.insert() frappe.db.commit() - return doc + purchase_orders.append(doc) + + return purchase_orders @frappe.whitelist() def make_purchase_order(source_name, selected_items=None, target_doc=None): diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index ee16f44171..3137621fd7 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -1,11 +1,12 @@ # 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 json -from frappe.utils import flt, add_days, nowdate -import frappe.permissions import unittest +import frappe +import frappe.permissions +from frappe.utils import flt, add_days, nowdate +from frappe.core.doctype.user_permission.test_user_permission import create_user from erpnext.selling.doctype.sales_order.sales_order \ import make_material_request, make_delivery_note, make_sales_invoice, WarehouseRequired from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry @@ -340,6 +341,9 @@ class TestSalesOrder(unittest.TestCase): prev_total = so.get("base_total") prev_total_in_words = so.get("base_in_words") + # get reserved qty before update items + reserved_qty_for_second_item = get_reserved_qty("_Test Item 2") + first_item_of_so = so.get("items")[0] trans_item = json.dumps([ {'item_code' : first_item_of_so.item_code, 'rate' : first_item_of_so.rate, \ @@ -353,6 +357,10 @@ class TestSalesOrder(unittest.TestCase): self.assertEqual(so.get("items")[-1].rate, 200) self.assertEqual(so.get("items")[-1].qty, 7) self.assertEqual(so.get("items")[-1].amount, 1400) + + # reserved qty should increase after adding row + self.assertEqual(get_reserved_qty('_Test Item 2'), reserved_qty_for_second_item + 7) + self.assertEqual(so.status, 'To Deliver and Bill') updated_total = so.get("base_total") @@ -372,6 +380,9 @@ class TestSalesOrder(unittest.TestCase): create_dn_against_so(so.name, 2) make_sales_invoice(so.name) + # get reserved qty before update items + reserved_qty_for_second_item = get_reserved_qty("_Test Item 2") + # add an item so as to try removing items trans_item = json.dumps([ {"item_code": '_Test Item', "qty": 5, "rate":1000, "docname": so.get("items")[0].name}, @@ -381,6 +392,9 @@ class TestSalesOrder(unittest.TestCase): so.reload() self.assertEqual(len(so.get("items")), 2) + # reserved qty should increase after adding row + self.assertEqual(get_reserved_qty('_Test Item 2'), reserved_qty_for_second_item + 2) + # check if delivered items can be removed trans_item = json.dumps([{ "item_code": '_Test Item 2', @@ -401,6 +415,10 @@ class TestSalesOrder(unittest.TestCase): so.reload() self.assertEqual(len(so.get("items")), 1) + + # reserved qty should decrease (back to initial) after deleting row + self.assertEqual(get_reserved_qty('_Test Item 2'), reserved_qty_for_second_item) + self.assertEqual(so.status, 'To Deliver and Bill') @@ -444,10 +462,8 @@ class TestSalesOrder(unittest.TestCase): def test_update_child_perm(self): so = make_sales_order(item_code= "_Test Item", qty=4) - user = 'test@example.com' - test_user = frappe.get_doc('User', user) - test_user.add_roles("Accounts User") - frappe.set_user(user) + test_user = create_user("test_so_child_perms@example.com", "Accounts User") + frappe.set_user(test_user.name) # update qty trans_item = json.dumps([{'item_code' : '_Test Item', 'rate' : 200, 'qty' : 7, 'docname': so.items[0].name}]) @@ -456,18 +472,14 @@ class TestSalesOrder(unittest.TestCase): # add new item trans_item = json.dumps([{'item_code' : '_Test Item', 'rate' : 100, 'qty' : 2}]) self.assertRaises(frappe.ValidationError, update_child_qty_rate,'Sales Order', trans_item, so.name) - test_user.remove_roles("Accounts User") - frappe.set_user("Administrator") def test_update_child_qty_rate_with_workflow(self): from frappe.model.workflow import apply_workflow - frappe.set_user("Administrator") workflow = make_sales_order_workflow() so = make_sales_order(item_code= "_Test Item", qty=1, rate=150, do_not_submit=1) apply_workflow(so, 'Approve') - frappe.set_user("Administrator") user = 'test@example.com' test_user = frappe.get_doc('User', user) test_user.add_roles("Sales User", "Test Junior Approver") @@ -508,12 +520,18 @@ class TestSalesOrder(unittest.TestCase): so = make_sales_order(item_code = "_Test Item", warehouse=None) + # get reserved qty of packed item + existing_reserved_qty = get_reserved_qty("_Packed Item") + added_item = json.dumps([{"item_code" : "_Product Bundle Item", "rate" : 200, 'qty' : 2}]) update_child_qty_rate('Sales Order', added_item, so.name) so.reload() self.assertEqual(so.packed_items[0].qty, 4) + # reserved qty in packed item should increase after adding bundle item + self.assertEqual(get_reserved_qty("_Packed Item"), existing_reserved_qty + 4) + # test uom and conversion factor change update_uom_conv_factor = json.dumps([{ 'item_code': so.get("items")[0].item_code, @@ -528,6 +546,9 @@ class TestSalesOrder(unittest.TestCase): so.reload() self.assertEqual(so.packed_items[0].qty, 8) + # reserved qty in packed item should increase after changing bundle item uom + self.assertEqual(get_reserved_qty("_Packed Item"), existing_reserved_qty + 8) + def test_update_child_with_tax_template(self): """ Test Action: Create a SO with one item having its tax account head already in the SO. @@ -618,33 +639,31 @@ class TestSalesOrder(unittest.TestCase): frappe.db.set_value("Stock Settings", None, "default_warehouse", old_stock_settings_value) def test_warehouse_user(self): - frappe.permissions.add_user_permission("Warehouse", "_Test Warehouse 1 - _TC", "test@example.com") - frappe.permissions.add_user_permission("Warehouse", "_Test Warehouse 2 - _TC1", "test2@example.com") - frappe.permissions.add_user_permission("Company", "_Test Company 1", "test2@example.com") - - test_user = frappe.get_doc("User", "test@example.com") - test_user.add_roles("Sales User", "Stock User") - test_user.remove_roles("Sales Manager") + test_user = create_user("test_so_warehouse_user@example.com", "Sales User", "Stock User") test_user_2 = frappe.get_doc("User", "test2@example.com") test_user_2.add_roles("Sales User", "Stock User") test_user_2.remove_roles("Sales Manager") - frappe.set_user("test@example.com") + frappe.permissions.add_user_permission("Warehouse", "_Test Warehouse 1 - _TC", test_user.name) + frappe.permissions.add_user_permission("Warehouse", "_Test Warehouse 2 - _TC1", test_user_2.name) + frappe.permissions.add_user_permission("Company", "_Test Company 1", test_user_2.name) - so = make_sales_order(company="_Test Company 1", + frappe.set_user(test_user.name) + + so = make_sales_order(company="_Test Company 1", customer="_Test Customer 1", warehouse="_Test Warehouse 2 - _TC1", do_not_save=True) so.conversion_rate = 0.02 so.plc_conversion_rate = 0.02 self.assertRaises(frappe.PermissionError, so.insert) - frappe.set_user("test2@example.com") + frappe.set_user(test_user_2.name) so.insert() frappe.set_user("Administrator") - frappe.permissions.remove_user_permission("Warehouse", "_Test Warehouse 1 - _TC", "test@example.com") - frappe.permissions.remove_user_permission("Warehouse", "_Test Warehouse 2 - _TC1", "test2@example.com") - frappe.permissions.remove_user_permission("Company", "_Test Company 1", "test2@example.com") + frappe.permissions.remove_user_permission("Warehouse", "_Test Warehouse 1 - _TC", test_user.name) + frappe.permissions.remove_user_permission("Warehouse", "_Test Warehouse 2 - _TC1", test_user_2.name) + frappe.permissions.remove_user_permission("Company", "_Test Company 1", test_user_2.name) def test_block_delivery_note_against_cancelled_sales_order(self): so = make_sales_order() @@ -743,7 +762,7 @@ class TestSalesOrder(unittest.TestCase): so = make_sales_order(item_list=so_items, do_not_submit=True) so.submit() - po = make_purchase_order_for_default_supplier(so.name, selected_items=[so_items[0]]) + po = make_purchase_order_for_default_supplier(so.name, selected_items=[so_items[0]])[0] po.submit() dn = create_dn_against_so(so.name, delivered_qty=2) @@ -825,7 +844,7 @@ class TestSalesOrder(unittest.TestCase): so.submit() # create po for only one item - po1 = make_purchase_order_for_default_supplier(so.name, selected_items=[so_items[0]]) + po1 = make_purchase_order_for_default_supplier(so.name, selected_items=[so_items[0]])[0] po1.submit() self.assertEqual(so.customer, po1.customer) @@ -835,7 +854,7 @@ class TestSalesOrder(unittest.TestCase): self.assertEqual(len(po1.items), 1) # create po for remaining item - po2 = make_purchase_order_for_default_supplier(so.name, selected_items=[so_items[1]]) + po2 = make_purchase_order_for_default_supplier(so.name, selected_items=[so_items[1]])[0] po2.submit() # teardown @@ -846,6 +865,45 @@ class TestSalesOrder(unittest.TestCase): so.load_from_db() so.cancel() + def test_drop_shipping_full_for_default_suppliers(self): + """Test if multiple POs are generated in one go against different default suppliers.""" + from erpnext.selling.doctype.sales_order.sales_order import make_purchase_order_for_default_supplier + + if not frappe.db.exists("Item", "_Test Item for Drop Shipping 1"): + make_item("_Test Item for Drop Shipping 1", {"is_stock_item": 1, "delivered_by_supplier": 1}) + + if not frappe.db.exists("Item", "_Test Item for Drop Shipping 2"): + make_item("_Test Item for Drop Shipping 2", {"is_stock_item": 1, "delivered_by_supplier": 1}) + + so_items = [ + { + "item_code": "_Test Item for Drop Shipping 1", + "warehouse": "", + "qty": 2, + "rate": 400, + "delivered_by_supplier": 1, + "supplier": '_Test Supplier' + }, + { + "item_code": "_Test Item for Drop Shipping 2", + "warehouse": "", + "qty": 2, + "rate": 400, + "delivered_by_supplier": 1, + "supplier": '_Test Supplier 1' + } + ] + + # create so and po + so = make_sales_order(item_list=so_items, do_not_submit=True) + so.submit() + + purchase_orders = make_purchase_order_for_default_supplier(so.name, selected_items=so_items) + + self.assertEqual(len(purchase_orders), 2) + self.assertEqual(purchase_orders[0].supplier, '_Test Supplier') + self.assertEqual(purchase_orders[1].supplier, '_Test Supplier 1') + def test_reserved_qty_for_closing_so(self): bin = frappe.get_all("Bin", filters={"item_code": "_Test Item", "warehouse": "_Test Warehouse - _TC"}, fields=["reserved_qty"]) diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js index 278821e392..9e3c9a5656 100644 --- a/erpnext/selling/page/point_of_sale/pos_controller.js +++ b/erpnext/selling/page/point_of_sale/pos_controller.js @@ -397,6 +397,7 @@ erpnext.PointOfSale.Controller = class { this.recent_order_list.toggle_component(false); frappe.run_serially([ () => this.frm.refresh(name), + () => this.frm.call('reset_mode_of_payments'), () => this.cart.load_invoice(), () => this.item_selector.toggle_component(true) ]); diff --git a/erpnext/selling/page/point_of_sale/pos_past_order_summary.js b/erpnext/selling/page/point_of_sale/pos_past_order_summary.js index be2b769a8a..b10a9e33c5 100644 --- a/erpnext/selling/page/point_of_sale/pos_past_order_summary.js +++ b/erpnext/selling/page/point_of_sale/pos_past_order_summary.js @@ -64,10 +64,7 @@ erpnext.PointOfSale.PastOrderSummary = class { {fieldname: 'print', fieldtype: 'Data', label: 'Print Preview'} ], primary_action: () => { - const frm = this.events.get_frm(); - frm.doc = this.doc; - frm.print_preview.lang_code = frm.doc.language; - frm.print_preview.printit(true); + this.print_receipt(); }, primary_action_label: __('Print'), }); @@ -192,13 +189,21 @@ erpnext.PointOfSale.PastOrderSummary = class { }); this.$summary_container.on('click', '.print-btn', () => { - const frm = this.events.get_frm(); - frm.doc = this.doc; - frm.print_preview.lang_code = frm.doc.language; - frm.print_preview.printit(true); + this.print_receipt(); }); } + print_receipt() { + const frm = this.events.get_frm(); + frappe.utils.print( + frm.doctype, + frm.docname, + frm.pos_print_format, + frm.doc.letter_head, + frm.doc.language || frappe.boot.lang + ); + } + attach_shortcuts() { const ctrl_label = frappe.utils.is_mac() ? '⌘' : 'Ctrl'; this.$summary_container.find('.print-btn').attr("title", `${ctrl_label}+P`); diff --git a/erpnext/setup/doctype/company/company.js b/erpnext/setup/doctype/company/company.js index c041d269a7..c2b5e4f9a9 100644 --- a/erpnext/setup/doctype/company/company.js +++ b/erpnext/setup/doctype/company/company.js @@ -259,6 +259,7 @@ erpnext.company.setup_queries = function(frm) { ["default_payroll_payable_account", {"root_type": "Liability"}], ["round_off_account", {"root_type": "Expense"}], ["write_off_account", {"root_type": "Expense"}], + ["default_discount_account", {}], ["discount_allowed_account", {"root_type": "Expense"}], ["discount_received_account", {"root_type": "Income"}], ["exchange_gain_loss_account", {"root_type": "Expense"}], @@ -275,7 +276,7 @@ erpnext.company.setup_queries = function(frm) { ["expenses_included_in_asset_valuation", {"account_type": "Expenses Included In Asset Valuation"}], ["capital_work_in_progress_account", {"account_type": "Capital Work in Progress"}], ["asset_received_but_not_billed", {"account_type": "Asset Received But Not Billed"}], - ["unrealized_profit_loss_account", {"root_type": "Liability"}] + ["unrealized_profit_loss_account", {"root_type": "Liability"},] ], function(i, v) { erpnext.company.set_custom_query(frm, v); }); diff --git a/erpnext/setup/doctype/company/company.json b/erpnext/setup/doctype/company/company.json index 56f60dfcff..83cbf475ab 100644 --- a/erpnext/setup/doctype/company/company.json +++ b/erpnext/setup/doctype/company/company.json @@ -59,6 +59,7 @@ "default_deferred_expense_account", "default_payroll_payable_account", "default_expense_claim_payable_account", + "default_discount_account", "section_break_22", "cost_center", "column_break_26", @@ -733,6 +734,12 @@ "fieldtype": "Link", "label": "Unrealized Profit / Loss Account", "options": "Account" + }, + { + "fieldname": "default_discount_account", + "fieldtype": "Link", + "label": "Default Payment Discount Account", + "options": "Account" } ], "icon": "fa fa-building", diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index 433851cde5..09221714d3 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -66,6 +66,7 @@ class Company(NestedSet): if frappe.db.sql("select abbr from tabCompany where name!=%s and abbr=%s", (self.name, self.abbr)): frappe.throw(_("Abbreviation already used for another company")) + @frappe.whitelist() def create_default_tax_template(self): from erpnext.setup.setup_wizard.operations.taxes_setup import create_sales_tax create_sales_tax({ diff --git a/erpnext/setup/doctype/email_digest/email_digest.py b/erpnext/setup/doctype/email_digest/email_digest.py index cbb4c7c5de..ac55fdfdb8 100644 --- a/erpnext/setup/doctype/email_digest/email_digest.py +++ b/erpnext/setup/doctype/email_digest/email_digest.py @@ -24,6 +24,7 @@ class EmailDigest(Document): self._accounts = {} self.currency = frappe.db.get_value('Company', self.company, "default_currency") + @frappe.whitelist() def get_users(self): """get list of users""" user_list = frappe.db.sql(""" @@ -41,6 +42,7 @@ class EmailDigest(Document): frappe.response['user_list'] = user_list + @frappe.whitelist() def send(self): # send email only to enabled users valid_users = [p[0] for p in frappe.db.sql("""select name from `tabUser` diff --git a/erpnext/setup/doctype/global_defaults/global_defaults.py b/erpnext/setup/doctype/global_defaults/global_defaults.py index fa7bc504b6..76a8450829 100644 --- a/erpnext/setup/doctype/global_defaults/global_defaults.py +++ b/erpnext/setup/doctype/global_defaults/global_defaults.py @@ -50,6 +50,7 @@ class GlobalDefaults(Document): # clear cache frappe.clear_cache() + @frappe.whitelist() def get_defaults(self): return frappe.defaults.get_defaults() diff --git a/erpnext/setup/doctype/naming_series/naming_series.py b/erpnext/setup/doctype/naming_series/naming_series.py index abff97364c..c4f1de14e4 100644 --- a/erpnext/setup/doctype/naming_series/naming_series.py +++ b/erpnext/setup/doctype/naming_series/naming_series.py @@ -10,10 +10,12 @@ from frappe import msgprint, throw, _ from frappe.model.document import Document from frappe.model.naming import parse_naming_series from frappe.permissions import get_doctypes_with_read +from frappe.core.doctype.doctype.doctype import validate_series class NamingSeriesNotSetError(frappe.ValidationError): pass class NamingSeries(Document): + @frappe.whitelist() def get_transactions(self, arg=None): doctypes = list(set(frappe.db.sql_list("""select parent from `tabDocField` df where fieldname='naming_series'""") @@ -52,6 +54,7 @@ class NamingSeries(Document): options = list(filter(lambda x: x, [cstr(n).strip() for n in ol])) return options + @frappe.whitelist() def update_series(self, arg=None): """update series list""" self.validate_series_set() @@ -126,7 +129,7 @@ class NamingSeries(Document): dt = frappe.get_doc("DocType", self.select_doc_for_series) options = self.scrub_options_list(self.set_options.split("\n")) for series in options: - dt.validate_series(series) + validate_series(dt, series) for i in sr: if i[0]: existing_series = [d.split('.')[0] for d in i[0].split("\n")] @@ -138,10 +141,12 @@ class NamingSeries(Document): if not re.match("^[\w\- /.#{}]*$", n, re.UNICODE): throw(_('Special Characters except "-", "#", ".", "/", "{" and "}" not allowed in naming series')) + @frappe.whitelist() def get_options(self, arg=None): if frappe.get_meta(arg or self.select_doc_for_series).get_field("naming_series"): return frappe.get_meta(arg or self.select_doc_for_series).get_field("naming_series").options + @frappe.whitelist() def get_current(self, arg=None): """get series current""" if self.prefix: diff --git a/erpnext/setup/install.py b/erpnext/setup/install.py index 1e424dd4b3..82f191d0b7 100644 --- a/erpnext/setup/install.py +++ b/erpnext/setup/install.py @@ -142,13 +142,15 @@ def add_standard_navbar_items(): } ] - current_nabvar_items = navbar_settings.help_dropdown + current_navbar_items = navbar_settings.help_dropdown navbar_settings.set('help_dropdown', []) for item in erpnext_navbar_items: - navbar_settings.append('help_dropdown', item) + current_labels = [item.get('item_label') for item in current_navbar_items] + if not item.get('item_label') in current_labels: + navbar_settings.append('help_dropdown', item) - for item in current_nabvar_items: + for item in current_navbar_items: navbar_settings.append('help_dropdown', { 'item_label': item.item_label, 'item_type': item.item_type, diff --git a/erpnext/setup/workspace/home/home.json b/erpnext/setup/workspace/home/home.json index 69ca7cf9ad..305456b266 100644 --- a/erpnext/setup/workspace/home/home.json +++ b/erpnext/setup/workspace/home/home.json @@ -10,13 +10,14 @@ "hide_custom": 0, "icon": "getting-started", "idx": 0, + "is_default": 0, "is_standard": 1, "label": "Home", "links": [ { "hidden": 0, "is_query_report": 0, - "label": "Healthcare", + "label": "Accounting", "onboard": 0, "type": "Card Break" }, @@ -24,8 +25,8 @@ "dependencies": "", "hidden": 0, "is_query_report": 0, - "label": "Patient", - "link_to": "Patient", + "label": "Chart of Accounts", + "link_to": "Account", "link_type": "DocType", "onboard": 1, "type": "Link" @@ -34,25 +35,8 @@ "dependencies": "", "hidden": 0, "is_query_report": 0, - "label": "Diagnosis", - "link_to": "Diagnosis", - "link_type": "DocType", - "onboard": 1, - "type": "Link" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Agriculture", - "onboard": 0, - "type": "Card Break" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Crop", - "link_to": "Crop", + "label": "Company", + "link_to": "Company", "link_type": "DocType", "onboard": 1, "type": "Link" @@ -61,8 +45,8 @@ "dependencies": "", "hidden": 0, "is_query_report": 0, - "label": "Crop Cycle", - "link_to": "Crop Cycle", + "label": "Customer", + "link_to": "Customer", "link_type": "DocType", "onboard": 1, "type": "Link" @@ -71,112 +55,8 @@ "dependencies": "", "hidden": 0, "is_query_report": 0, - "label": "Location", - "link_to": "Location", - "link_type": "DocType", - "onboard": 1, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Fertilizer", - "link_to": "Fertilizer", - "link_type": "DocType", - "onboard": 1, - "type": "Link" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Education", - "onboard": 0, - "type": "Card Break" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Student", - "link_to": "Student", - "link_type": "DocType", - "onboard": 1, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Course", - "link_to": "Course", - "link_type": "DocType", - "onboard": 1, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Instructor", - "link_to": "Instructor", - "link_type": "DocType", - "onboard": 1, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Room", - "link_to": "Room", - "link_type": "DocType", - "onboard": 1, - "type": "Link" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Non Profit", - "onboard": 0, - "type": "Card Break" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Member", - "link_to": "Member", - "link_type": "DocType", - "onboard": 1, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Volunteer", - "link_to": "Volunteer", - "link_type": "DocType", - "onboard": 1, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Chapter", - "link_to": "Chapter", - "link_type": "DocType", - "onboard": 1, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Donor", - "link_to": "Donor", + "label": "Supplier", + "link_to": "Supplier", "link_type": "DocType", "onboard": 1, "type": "Link" @@ -188,6 +68,16 @@ "onboard": 0, "type": "Card Break" }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Item", + "link_to": "Item", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, { "dependencies": "", "hidden": 0, @@ -302,73 +192,6 @@ "onboard": 1, "type": "Link" }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Accounting", - "onboard": 0, - "type": "Card Break" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Item", - "link_to": "Item", - "link_type": "DocType", - "onboard": 1, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Customer", - "link_to": "Customer", - "link_type": "DocType", - "onboard": 1, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Supplier", - "link_to": "Supplier", - "link_type": "DocType", - "onboard": 1, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Company", - "link_to": "Company", - "link_type": "DocType", - "onboard": 1, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Chart of Accounts", - "link_to": "Account", - "link_type": "DocType", - "onboard": 1, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Opening Invoice Creation Tool", - "link_to": "Opening Invoice Creation Tool", - "link_type": "DocType", - "onboard": 1, - "type": "Link" - }, { "hidden": 0, "is_query_report": 0, @@ -386,6 +209,16 @@ "onboard": 1, "type": "Link" }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Opening Invoice Creation Tool", + "link_to": "Opening Invoice Creation Tool", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, { "dependencies": "", "hidden": 0, @@ -415,9 +248,177 @@ "link_type": "DocType", "onboard": 1, "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Healthcare", + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Patient", + "link_to": "Patient", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Diagnosis", + "link_to": "Diagnosis", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Education", + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Student", + "link_to": "Student", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Instructor", + "link_to": "Instructor", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Course", + "link_to": "Course", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Room", + "link_to": "Room", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Non Profit", + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Donor", + "link_to": "Donor", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Member", + "link_to": "Member", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Volunteer", + "link_to": "Volunteer", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Chapter", + "link_to": "Chapter", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Agriculture", + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Location", + "link_to": "Location", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Crop", + "link_to": "Crop", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Crop Cycle", + "link_to": "Crop Cycle", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Fertilizer", + "link_to": "Fertilizer", + "link_type": "DocType", + "onboard": 1, + "type": "Link" } ], - "modified": "2021-01-01 12:13:16.055668", + "modified": "2021-03-16 15:59:58.416154", "modified_by": "Administrator", "module": "Setup", "name": "Home", diff --git a/erpnext/stock/doctype/bin/bin.json b/erpnext/stock/doctype/bin/bin.json index 04d624ec0b..8e79f0e555 100644 --- a/erpnext/stock/doctype/bin/bin.json +++ b/erpnext/stock/doctype/bin/bin.json @@ -1,4 +1,5 @@ { + "actions": [], "autoname": "MAT-BIN-.YYYY.-.#####", "creation": "2013-01-10 16:34:25", "doctype": "DocType", @@ -112,7 +113,8 @@ { "fieldname": "reserved_qty_for_sub_contract", "fieldtype": "Float", - "label": "Reserved Qty for sub contract" + "label": "Reserved Qty for sub contract", + "read_only": 1 }, { "fieldname": "ma_rate", @@ -166,7 +168,8 @@ "hide_toolbar": 1, "idx": 1, "in_create": 1, - "modified": "2019-11-18 18:34:59.456882", + "links": [], + "modified": "2021-03-30 23:09:39.572776", "modified_by": "Administrator", "module": "Stock", "name": "Bin", @@ -196,5 +199,6 @@ ], "quick_entry": 1, "search_fields": "item_code,warehouse", + "sort_field": "modified", "sort_order": "ASC" } \ No newline at end of file diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 35443906c8..d326a04173 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -101,7 +101,7 @@ class DeliveryNote(SellingController): for f in fieldname: toggle_print_hide(self.meta if key == "parent" else item_meta, f) - super(DeliveryNote, self).before_print() + super(DeliveryNote, self).before_print(settings) def set_actual_qty(self): for d in self.get('items'): diff --git a/erpnext/stock/doctype/delivery_trip/delivery_trip.py b/erpnext/stock/doctype/delivery_trip/delivery_trip.py index 28e9533186..de85bc3922 100644 --- a/erpnext/stock/doctype/delivery_trip/delivery_trip.py +++ b/erpnext/stock/doctype/delivery_trip/delivery_trip.py @@ -90,6 +90,7 @@ class DeliveryTrip(Document): delivery_notes = [get_link_to_form("Delivery Note", note) for note in delivery_notes] frappe.msgprint(_("Delivery Notes {0} updated").format(", ".join(delivery_notes))) + @frappe.whitelist() def process_route(self, optimize): """ Estimate the arrival times for each stop in the Delivery Trip. diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json index 33a8fe7c8d..6fed9efa63 100644 --- a/erpnext/stock/doctype/item/item.json +++ b/erpnext/stock/doctype/item/item.json @@ -1054,6 +1054,7 @@ "read_only": 1 }, { + "depends_on": "eval: doc.show_in_website || doc.show_variant_in_website", "fieldname": "website_image_alt", "fieldtype": "Data", "label": "Image Description" @@ -1066,7 +1067,7 @@ "index_web_pages_for_search": 1, "links": [], "max_attachments": 1, - "modified": "2021-03-15 13:41:04.108932", + "modified": "2021-03-18 14:04:38.575519", "modified_by": "Administrator", "module": "Stock", "name": "Item", @@ -1137,4 +1138,4 @@ "sort_order": "DESC", "title_field": "item_name", "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index 7b7d2da969..7cb84a69f0 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -50,6 +50,7 @@ class Item(WebsiteGenerator): self.set_onload('stock_exists', self.stock_ledger_created()) self.set_asset_naming_series() + @frappe.whitelist() def set_asset_naming_series(self): if not hasattr(self, '_asset_naming_series'): from erpnext.assets.doctype.asset.asset import get_asset_naming_series @@ -706,6 +707,7 @@ class Item(WebsiteGenerator): frappe.db.set_value("Stock Settings", None, "allow_negative_stock", existing_allow_negative_stock) frappe.db.auto_commit_on_many_writes = 0 + @frappe.whitelist() def copy_specification_from_item_group(self): self.set("website_specifications", []) if self.item_group: diff --git a/erpnext/stock/doctype/item/test_records.json b/erpnext/stock/doctype/item/test_records.json index 909c4eeb90..c1f20a47b7 100644 --- a/erpnext/stock/doctype/item/test_records.json +++ b/erpnext/stock/doctype/item/test_records.json @@ -12,6 +12,7 @@ "item_name": "_Test Item", "apply_warehouse_wise_reorder_level": 1, "gst_hsn_code": "999800", + "opening_stock": 10, "valuation_rate": 100, "item_defaults": [{ "company": "_Test Company", diff --git a/erpnext/stock/doctype/item_attribute/test_records.json b/erpnext/stock/doctype/item_attribute/test_records.json index d346979496..6aa6ffd6c9 100644 --- a/erpnext/stock/doctype/item_attribute/test_records.json +++ b/erpnext/stock/doctype/item_attribute/test_records.json @@ -4,10 +4,12 @@ "attribute_name": "Test Size", "priority": 1, "item_attribute_values": [ + {"attribute_value": "Extra Small", "abbr": "XSL"}, {"attribute_value": "Small", "abbr": "S"}, {"attribute_value": "Medium", "abbr": "M"}, {"attribute_value": "Large", "abbr": "L"}, - {"attribute_value": "Extra Small", "abbr": "XSL"} + {"attribute_value": "Extra Large", "abbr": "XL"}, + {"attribute_value": "2XL", "abbr": "2XL"} ] }, { diff --git a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py index 69a8bf19d3..83109469fc 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py @@ -12,6 +12,7 @@ from erpnext.accounts.doctype.account.account import get_account_currency from erpnext.controllers.taxes_and_totals import init_landed_taxes_and_totals class LandedCostVoucher(Document): + @frappe.whitelist() def get_items_from_purchase_receipts(self): self.set("items", []) for pr in self.get("purchase_receipts"): diff --git a/erpnext/stock/doctype/material_request/material_request.js b/erpnext/stock/doctype/material_request/material_request.js index 527b0d3ea9..7dfc5da50d 100644 --- a/erpnext/stock/doctype/material_request/material_request.js +++ b/erpnext/stock/doctype/material_request/material_request.js @@ -354,6 +354,10 @@ frappe.ui.form.on('Material Request', { }, material_request_type: function(frm) { frm.toggle_reqd('customer', frm.doc.material_request_type=="Customer Provided"); + + if (frm.doc.material_request_type !== 'Material Transfer' && frm.doc.set_from_warehouse) { + frm.set_value('set_from_warehouse', ''); + } }, }); diff --git a/erpnext/stock/doctype/material_request/material_request.json b/erpnext/stock/doctype/material_request/material_request.json index d73349dd39..8d7b238c17 100644 --- a/erpnext/stock/doctype/material_request/material_request.json +++ b/erpnext/stock/doctype/material_request/material_request.json @@ -20,9 +20,9 @@ "company", "amended_from", "warehouse_section", - "set_warehouse", - "column_break5", "set_from_warehouse", + "column_break5", + "set_warehouse", "items_section", "scan_barcode", "items", @@ -314,7 +314,7 @@ "idx": 70, "is_submittable": 1, "links": [], - "modified": "2020-09-19 01:04:09.285862", + "modified": "2021-03-31 23:52:55.392512", "modified_by": "Administrator", "module": "Stock", "name": "Material Request", diff --git a/erpnext/stock/doctype/packing_slip/packing_slip.py b/erpnext/stock/doctype/packing_slip/packing_slip.py index a7a29cca7f..2008bffcd3 100644 --- a/erpnext/stock/doctype/packing_slip/packing_slip.py +++ b/erpnext/stock/doctype/packing_slip/packing_slip.py @@ -152,6 +152,7 @@ class PackingSlip(Document): return cint(recommended_case_no[0][0]) + 1 + @frappe.whitelist() def get_items(self): self.set("items", []) diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 0da57b734b..61b72092e7 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -25,14 +25,15 @@ class PickList(Document): if not frappe.get_cached_value('Item', item.item_code, 'has_serial_no'): continue if not item.serial_no: - frappe.throw(_("Row #{0}: {1} does not have any available serial numbers in {2}".format( - frappe.bold(item.idx), frappe.bold(item.item_code), frappe.bold(item.warehouse))), + frappe.throw(_("Row #{0}: {1} does not have any available serial numbers in {2}").format( + frappe.bold(item.idx), frappe.bold(item.item_code), frappe.bold(item.warehouse)), title=_("Serial Nos Required")) if len(item.serial_no.split('\n')) == item.picked_qty: continue frappe.throw(_('For item {0} at row {1}, count of serial numbers does not match with the picked quantity') .format(frappe.bold(item.item_code), frappe.bold(item.idx)), title=_("Quantity Mismatch")) + @frappe.whitelist() def set_item_locations(self, save=False): items = self.aggregate_item_qty() self.item_location_map = frappe._dict() @@ -380,7 +381,7 @@ def create_stock_entry(pick_list): stock_entry.set_incoming_rate() stock_entry.set_actual_qty() - stock_entry.calculate_rate_and_amount(update_finished_item_rate=False) + stock_entry.calculate_rate_and_amount() return stock_entry.as_dict() diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.py b/erpnext/stock/doctype/quality_inspection/quality_inspection.py index 58b1eca2d3..469511af60 100644 --- a/erpnext/stock/doctype/quality_inspection/quality_inspection.py +++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.py @@ -18,6 +18,7 @@ class QualityInspection(Document): if self.readings: self.inspect_and_set_status() + @frappe.whitelist() def get_item_specification_details(self): if not self.quality_inspection_template: self.quality_inspection_template = frappe.db.get_value('Item', @@ -32,6 +33,7 @@ class QualityInspection(Document): child.update(d) child.status = "Accepted" + @frappe.whitelist() def get_quality_inspection_template(self): template = '' if self.bom_no: @@ -62,17 +64,21 @@ class QualityInspection(Document): (quality_inspection, self.modified, self.reference_name, self.item_code)) else: + args = [quality_inspection, self.modified, self.reference_name, self.item_code] doctype = self.reference_type + ' Item' + if self.reference_type == 'Stock Entry': doctype = 'Stock Entry Detail' if self.reference_type and self.reference_name: conditions = "" if self.batch_no and self.docstatus == 1: - conditions += " and t1.batch_no = '%s'"%(self.batch_no) + conditions += " and t1.batch_no = %s" + args.append(self.batch_no) if self.docstatus == 2: # if cancel, then remove qi link wherever same name - conditions += " and t1.quality_inspection = '%s'"%(self.name) + conditions += " and t1.quality_inspection = %s" + args.append(self.name) frappe.db.sql(""" UPDATE @@ -85,7 +91,7 @@ class QualityInspection(Document): and t1.parent = t2.name {conditions} """.format(parent_doc=self.reference_type, child_doc=doctype, conditions=conditions), - (quality_inspection, self.modified, self.reference_name, self.item_code)) + args) def inspect_and_set_status(self): for reading in self.readings: diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py index 8436acbed2..f8cfdf83e7 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import frappe, erpnext from frappe.model.document import Document -from frappe.utils import cint, get_link_to_form +from frappe.utils import cint, get_link_to_form, add_to_date, today from erpnext.stock.stock_ledger import repost_future_sle from erpnext.accounts.utils import update_gl_entries_after, check_if_stock_and_account_balance_synced from frappe.utils.user import get_users_with_role @@ -29,7 +29,7 @@ class RepostItemValuation(Document): self.company = frappe.get_cached_value(self.voucher_type, self.voucher_no, "company") elif self.warehouse: self.company = frappe.get_cached_value("Warehouse", self.warehouse, "company") - + def set_status(self, status=None): if not status: status = 'Queued' @@ -39,6 +39,7 @@ class RepostItemValuation(Document): frappe.enqueue(repost, timeout=1800, queue='long', job_name='repost_sle', now=frappe.flags.in_test, doc=self) + @frappe.whitelist() def restart_reposting(self): self.set_status('Queued') frappe.enqueue(repost, timeout=1800, queue='long', @@ -54,7 +55,6 @@ def repost(doc): repost_sl_entries(doc) repost_gl_entries(doc) - check_if_stock_and_account_balance_synced(doc.posting_date, doc.company) doc.set_status('Completed') except Exception: @@ -103,7 +103,7 @@ def notify_error_to_stock_managers(doc, traceback): recipients = get_users_with_role("Stock Manager") if not recipients: get_users_with_role("System Manager") - + subject = _("Error while reposting item valuation") message = (_("Hi,") + "
" + _("An error has been appeared while reposting item valuation via {0}") @@ -112,4 +112,24 @@ def notify_error_to_stock_managers(doc, traceback): ) frappe.sendmail(recipients=recipients, subject=subject, message=message) +def repost_entries(): + riv_entries = get_repost_item_valuation_entries() + for row in riv_entries: + doc = frappe.get_cached_doc('Repost Item Valuation', row.name) + repost(doc) + + riv_entries = get_repost_item_valuation_entries() + if riv_entries: + return + + for d in frappe.get_all('Company', filters= {'enable_perpetual_inventory': 1}): + check_if_stock_and_account_balance_synced(today(), d.company) + +def get_repost_item_valuation_entries(): + date = add_to_date(today(), hours=-12) + + return frappe.db.sql(""" SELECT name from `tabRepost Item Valuation` + WHERE status != 'Completed' and creation <= %s and docstatus = 1 + ORDER BY timestamp(posting_date, posting_time) asc, creation asc + """, date, as_dict=1) \ No newline at end of file diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index ea1b3873ea..f8ac400a8e 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -458,7 +458,7 @@ class StockEntry(StockController): Set rate for outgoing, scrapped and finished items """ # Set rate for outgoing items - outgoing_items_cost = self.set_rate_for_outgoing_items(reset_outgoing_rate) + outgoing_items_cost = self.set_rate_for_outgoing_items(reset_outgoing_rate, raise_error_if_no_rate) finished_item_qty = sum([d.transfer_qty for d in self.items if d.is_finished_item]) # Set basic rate for incoming items @@ -482,13 +482,13 @@ class StockEntry(StockController): d.basic_rate = flt(d.basic_rate, d.precision("basic_rate")) d.basic_amount = flt(flt(d.transfer_qty) * flt(d.basic_rate), d.precision("basic_amount")) - def set_rate_for_outgoing_items(self, reset_outgoing_rate=True): + def set_rate_for_outgoing_items(self, reset_outgoing_rate=True, raise_error_if_no_rate=True): outgoing_items_cost = 0.0 for d in self.get('items'): if d.s_warehouse: if reset_outgoing_rate: args = self.get_args_for_incoming_rate(d) - rate = get_incoming_rate(args) + rate = get_incoming_rate(args, raise_error_if_no_rate) if rate > 0: d.basic_rate = rate @@ -839,6 +839,7 @@ class StockEntry(StockController): if not pro_doc.operations: pro_doc.set_actual_dates() + @frappe.whitelist() def get_item_details(self, args=None, for_update=False): item = frappe.db.sql("""select i.name, i.stock_uom, i.description, i.image, i.item_name, i.item_group, i.has_batch_no, i.sample_quantity, i.has_serial_no, i.allow_alternative_item, @@ -913,6 +914,7 @@ class StockEntry(StockController): return ret + @frappe.whitelist() def set_items_for_stock_in(self): self.items = [] @@ -937,6 +939,7 @@ class StockEntry(StockController): 'batch_no': d.batch_no }) + @frappe.whitelist() def get_items(self): self.set('items', []) self.validate_work_order() @@ -1010,7 +1013,8 @@ class StockEntry(StockController): self.set_scrap_items() self.set_actual_qty() - self.calculate_rate_and_amount(raise_error_if_no_rate=False) + self.validate_customer_provided_item() + self.calculate_rate_and_amount() def set_scrap_items(self): if self.purpose != "Send to Subcontractor" and self.purpose in ["Manufacture", "Repack"]: diff --git a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py index 59f1f3961b..7ebd4e6cb2 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py @@ -125,7 +125,7 @@ class TestStockLedgerEntry(unittest.TestCase): pr = make_purchase_receipt(company="_Test Company", posting_date='2020-04-10', warehouse="Stores - _TC", item_code="_Test Item for Reposting", qty=5, rate=100) - return_pr = make_purchase_receipt(company="_Test Company", posting_date='2020-04-15', + return_pr = make_purchase_receipt(company="_Test Company", posting_date='2020-04-15', warehouse="Stores - _TC", item_code="_Test Item for Reposting", is_return=1, return_against=pr.name, qty=-2) # check sle @@ -278,7 +278,7 @@ class TestStockLedgerEntry(unittest.TestCase): frappe.db.set_value("Buying Settings", None, "backflush_raw_materials_of_subcontract_based_on", "BOM") make_bom(item = subcontracted_item, raw_materials =[rm_item_code], currency="INR") - + # Purchase raw materials on supplier warehouse: Qty = 50, Rate = 100 pr = make_purchase_receipt(company=company, posting_date='2020-04-10', warehouse="Stores - _TC", item_code=rm_item_code, qty=10, rate=100) @@ -292,7 +292,7 @@ class TestStockLedgerEntry(unittest.TestCase): # Update raw material's valuation via LCV, Additional cost = 50 lcv = create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company) - + pr1.reload() self.assertEqual(pr1.items[0].valuation_rate, 125) @@ -310,31 +310,33 @@ class TestStockLedgerEntry(unittest.TestCase): # Back dated stock transactions are only allowed to stock managers frappe.db.set_value("Stock Settings", None, "role_allowed_to_create_edit_back_dated_transactions", "Stock Manager") - + # Set User with Stock User role but not Stock Manager - frappe.set_user("test@example.com") - user = frappe.get_doc("User", "test@example.com") - user.add_roles("Stock User") - user.remove_roles("Stock Manager") + try: + frappe.set_user("test@example.com") + user = frappe.get_doc("User", "test@example.com") + user.add_roles("Stock User") + user.remove_roles("Stock Manager") - stock_entry_on_today = make_stock_entry(target="_Test Warehouse - _TC", qty=10, basic_rate=100) - back_dated_se_1 = make_stock_entry(target="_Test Warehouse - _TC", qty=10, basic_rate=100, - posting_date=add_days(today(), -1), do_not_submit=True) + stock_entry_on_today = make_stock_entry(target="_Test Warehouse - _TC", qty=10, basic_rate=100) + back_dated_se_1 = make_stock_entry(target="_Test Warehouse - _TC", qty=10, basic_rate=100, + posting_date=add_days(today(), -1), do_not_submit=True) - # Block back-dated entry - self.assertRaises(BackDatedStockTransaction, back_dated_se_1.submit) + # Block back-dated entry + self.assertRaises(BackDatedStockTransaction, back_dated_se_1.submit) - user.add_roles("Stock Manager") + user.add_roles("Stock Manager") - # Back dated entry allowed to Stock Manager - back_dated_se_2 = make_stock_entry(target="_Test Warehouse - _TC", qty=10, basic_rate=100, - posting_date=add_days(today(), -1)) + # Back dated entry allowed to Stock Manager + back_dated_se_2 = make_stock_entry(target="_Test Warehouse - _TC", qty=10, basic_rate=100, + posting_date=add_days(today(), -1)) - back_dated_se_2.cancel() - stock_entry_on_today.cancel() + back_dated_se_2.cancel() + stock_entry_on_today.cancel() - frappe.db.set_value("Stock Settings", None, "role_allowed_to_create_edit_back_dated_transactions", None) - frappe.set_user("Administrator") + finally: + frappe.db.set_value("Stock Settings", None, "role_allowed_to_create_edit_back_dated_transactions", None) + frappe.set_user("Administrator") def create_repack_entry(**args): diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index f0a90f9754..b452e96c5e 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -29,6 +29,8 @@ class StockReconciliation(StockController): self.remove_items_with_no_change() self.validate_data() self.validate_expense_account() + self.validate_customer_provided_item() + self.set_zero_value_for_customer_provided_items() self.set_total_qty_and_amount() self.validate_putaway_capacity() @@ -217,7 +219,7 @@ class StockReconciliation(StockController): if row.valuation_rate in ("", None): row.valuation_rate = previous_sle.get("valuation_rate", 0) - if row.qty and not row.valuation_rate: + if row.qty and not row.valuation_rate and not row.allow_zero_valuation_rate: frappe.throw(_("Valuation Rate required for Item {0} at row {1}").format(row.item_code, row.idx)) if ((previous_sle and row.qty == previous_sle.get("qty_after_transaction") @@ -436,6 +438,20 @@ class StockReconciliation(StockController): if frappe.db.get_value("Account", self.expense_account, "report_type") == "Profit and Loss": frappe.throw(_("Difference Account must be a Asset/Liability type account, since this Stock Reconciliation is an Opening Entry"), OpeningEntryAccountError) + def set_zero_value_for_customer_provided_items(self): + changed_any_values = False + + for d in self.get('items'): + is_customer_item = frappe.db.get_value('Item', d.item_code, 'is_customer_provided_item') + if is_customer_item and d.valuation_rate: + d.valuation_rate = 0.0 + changed_any_values = True + + if changed_any_values: + msgprint(_("Valuation rate for customer provided items has been set to zero."), + title=_("Note"), indicator="blue") + + def set_total_qty_and_amount(self): for d in self.get("items"): d.amount = flt(d.qty, d.precision("qty")) * flt(d.valuation_rate, d.precision("valuation_rate")) @@ -531,4 +547,4 @@ def get_difference_account(purpose, company): account = frappe.db.get_value('Account', {'is_group': 0, 'company': company, 'account_type': 'Temporary'}, 'name') - return account \ No newline at end of file + return account diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index 088456f865..6690c6a606 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -193,6 +193,16 @@ class TestStockReconciliation(unittest.TestCase): stock_doc = frappe.get_doc("Stock Reconciliation", d) stock_doc.cancel() + def test_customer_provided_items(self): + item_code = 'Stock-Reco-customer-Item-100' + create_item(item_code, is_customer_provided_item = 1, + customer = '_Test Customer', is_purchase_item = 0) + + sr = create_stock_reconciliation(item_code = item_code, qty = 10, rate = 420) + + self.assertEqual(sr.get("items")[0].allow_zero_valuation_rate, 1) + self.assertEqual(sr.get("items")[0].valuation_rate, 0) + self.assertEqual(sr.get("items")[0].amount, 0) def insert_existing_sle(warehouse): from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry diff --git a/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json b/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json index e53db0772b..85c7ebe263 100644 --- a/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json +++ b/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json @@ -13,6 +13,7 @@ "qty", "valuation_rate", "amount", + "allow_zero_valuation_rate", "serial_no_and_batch_section", "serial_no", "column_break_11", @@ -166,10 +167,19 @@ "fieldtype": "Link", "label": "Batch No", "options": "Batch" + }, + { + "default": "0", + "fieldname": "allow_zero_valuation_rate", + "fieldtype": "Check", + "label": "Allow Zero Valuation Rate", + "print_hide": 1, + "read_only": 1 } ], "istable": 1, - "modified": "2019-06-14 17:10:53.188305", + "links": [], + "modified": "2021-03-23 11:09:44.407157", "modified_by": "Administrator", "module": "Stock", "name": "Stock Reconciliation Item", @@ -179,4 +189,4 @@ "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 873cfec85e..e23f7d43d9 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -110,7 +110,7 @@ def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=Tru get_gross_profit(out) if args.doctype == 'Material Request': out.rate = args.rate or out.price_list_rate - out.amount = flt(args.qty * out.rate) + out.amount = flt(args.qty) * flt(out.rate) return out @@ -314,7 +314,9 @@ def get_basic_details(args, item, overwrite_warehouse=True): "last_purchase_rate": item.last_purchase_rate if args.get("doctype") in ["Purchase Order"] else 0, "transaction_date": args.get("transaction_date"), "against_blanket_order": args.get("against_blanket_order"), - "bom_no": item.get("default_bom") + "bom_no": item.get("default_bom"), + "weight_per_unit": args.get("weight_per_unit") or item.get("weight_per_unit"), + "weight_uom": args.get("weight_uom") or item.get("weight_uom") }) if item.get("enable_deferred_revenue") or item.get("enable_deferred_expense"): @@ -369,6 +371,9 @@ def get_basic_details(args, item, overwrite_warehouse=True): if meta.get_field("barcode"): update_barcode_value(out) + if out.get("weight_per_unit"): + out['total_weight'] = out.weight_per_unit * out.stock_qty + return out def get_item_warehouse(item, args, overwrite_warehouse, defaults={}): diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py index e5d4d626c4..6dfede4590 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.py +++ b/erpnext/stock/report/stock_balance/stock_balance.py @@ -198,7 +198,7 @@ def get_item_warehouse_map(filters, sle): else: qty_diff = flt(d.actual_qty) - value_diff = flt(d.stock_value) - flt(qty_dict.bal_val) + value_diff = flt(d.stock_value_difference) if d.posting_date < from_date: qty_dict.opening_qty += qty_diff diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index f54b3c1bb2..121c51cf6a 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -207,11 +207,11 @@ class update_entries_after(object): def build(self): - from erpnext.controllers.stock_controller import check_if_future_sle_exists + from erpnext.controllers.stock_controller import future_sle_exists if self.args.get("sle_id"): self.process_sle_against_current_timestamp() - if not check_if_future_sle_exists(self.args): + if not future_sle_exists(self.args): self.update_bin() else: entries_to_fix = self.get_future_entries_to_fix() @@ -856,4 +856,4 @@ def get_future_sle_with_negative_qty(args): and qty_after_transaction < 0 order by timestamp(posting_date, posting_time) asc limit 1 - """, args, as_dict=1) \ No newline at end of file + """, args, as_dict=1) diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py index bbbbc4a527..767a8a6a29 100644 --- a/erpnext/support/doctype/issue/issue.py +++ b/erpnext/support/doctype/issue/issue.py @@ -165,6 +165,7 @@ class Issue(Document): communication.ignore_mandatory = True communication.save() + @frappe.whitelist() def split_issue(self, subject, communication_id): # Bug: Pressing enter doesn't send subject from copy import deepcopy @@ -259,6 +260,7 @@ class Issue(Document): self.set_response_and_resolution_time(priority=self.priority, service_level_agreement=self.service_level_agreement) frappe.msgprint(_("Service Level Agreement has been changed to {0}.").format(self.service_level_agreement)) + @frappe.whitelist() def reset_service_level_agreement(self, reason, user): if not frappe.db.get_single_value("Support Settings", "allow_resetting_service_level_agreement"): frappe.throw(_("Allow Resetting Service Level Agreement from Support Settings.")) diff --git a/erpnext/support/report/issue_summary/issue_summary.py b/erpnext/support/report/issue_summary/issue_summary.py index 3d735314f4..7861e30d25 100644 --- a/erpnext/support/report/issue_summary/issue_summary.py +++ b/erpnext/support/report/issue_summary/issue_summary.py @@ -260,8 +260,7 @@ class IssueSummary(object): self.issue_summary_data[value]['avg_user_resolution_time'] = entry.get('avg_user_resolution_time') or 0.0 def get_chart_data(self): - if not self.data: - return None + self.chart = [] labels = [] open_issues = [] @@ -310,8 +309,7 @@ class IssueSummary(object): } def get_report_summary(self): - if not self.data: - return None + self.report_summary = [] open_issues = 0 replied = 0