diff --git a/erpnext/__init__.py b/erpnext/__init__.py index bef6661254..dcfad1f100 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -131,11 +131,3 @@ def allow_regional(fn): return frappe.get_attr(overrides[function_path][-1])(*args, **kwargs) return caller - -def get_last_membership(member): - '''Returns last membership if exists''' - last_membership = frappe.get_all('Membership', 'name,to_date,membership_type', - dict(member=member, paid=1), order_by='to_date desc', limit=1) - - if last_membership: - return last_membership[0] diff --git a/erpnext/accounts/deferred_revenue.py b/erpnext/accounts/deferred_revenue.py index 9e2cdfffd9..ab1061beeb 100644 --- a/erpnext/accounts/deferred_revenue.py +++ b/erpnext/accounts/deferred_revenue.py @@ -120,6 +120,7 @@ def get_booking_dates(doc, item, posting_date=None): prev_gl_entry = frappe.db.sql(''' select name, posting_date from `tabGL Entry` where company=%s and account=%s and voucher_type=%s and voucher_no=%s and voucher_detail_no=%s + and is_cancelled = 0 order by posting_date desc limit 1 ''', (doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name), as_dict=True) @@ -227,6 +228,7 @@ def get_already_booked_amount(doc, item): gl_entries_details = frappe.db.sql(''' select sum({0}) as total_credit, sum({1}) as total_credit_in_account_currency, voucher_detail_no from `tabGL Entry` where company=%s and account=%s and voucher_type=%s and voucher_no=%s and voucher_detail_no=%s + and is_cancelled = 0 group by voucher_detail_no '''.format(total_credit_debit, total_credit_debit_currency), (doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name), as_dict=True) @@ -282,7 +284,7 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None): return # check if books nor frozen till endate: - if getdate(end_date) >= getdate(accounts_frozen_upto): + if accounts_frozen_upto and (end_date) <= getdate(accounts_frozen_upto): end_date = get_last_day(add_days(accounts_frozen_upto, 1)) if via_journal_entry: diff --git a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py index 72b6893faf..d84b8e07d3 100644 --- a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py +++ b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py @@ -109,7 +109,7 @@ def create_bank_account(bank_name="Citi Bank", account_name="_Test Bank - _TC"): frappe.get_doc({ "doctype": "Bank", "bank_name":bank_name, - }).insert() + }).insert(ignore_if_duplicate=True) except frappe.DuplicateEntryError: pass @@ -119,7 +119,7 @@ def create_bank_account(bank_name="Citi Bank", account_name="_Test Bank - _TC"): "account_name":"Checking Account", "bank": bank_name, "account": account_name - }).insert() + }).insert(ignore_if_duplicate=True) except frappe.DuplicateEntryError: pass @@ -184,7 +184,7 @@ def add_vouchers(): "supplier_group":"All Supplier Groups", "supplier_type": "Company", "supplier_name": "Conrad Electronic" - }).insert() + }).insert(ignore_if_duplicate=True) except frappe.DuplicateEntryError: pass @@ -203,7 +203,7 @@ def add_vouchers(): "supplier_group":"All Supplier Groups", "supplier_type": "Company", "supplier_name": "Mr G" - }).insert() + }).insert(ignore_if_duplicate=True) except frappe.DuplicateEntryError: pass @@ -227,7 +227,7 @@ def add_vouchers(): "supplier_group":"All Supplier Groups", "supplier_type": "Company", "supplier_name": "Poore Simon's" - }).insert() + }).insert(ignore_if_duplicate=True) except frappe.DuplicateEntryError: pass @@ -237,7 +237,7 @@ def add_vouchers(): "customer_group":"All Customer Groups", "customer_type": "Company", "customer_name": "Poore Simon's" - }).insert() + }).insert(ignore_if_duplicate=True) except frappe.DuplicateEntryError: pass @@ -266,7 +266,7 @@ def add_vouchers(): "customer_group":"All Customer Groups", "customer_type": "Company", "customer_name": "Fayva" - }).insert() + }).insert(ignore_if_duplicate=True) except frappe.DuplicateEntryError: pass 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 ade7f8146b..6e7b80e731 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 @@ -166,7 +166,7 @@ class OpeningInvoiceCreationTool(Document): frappe.scrub(row.party_type): row.party, "is_pos": 0, "doctype": "Sales Invoice" if self.invoice_type == "Sales" else "Purchase Invoice", - "update_stock": 0, + "update_stock": 0, # important: https://github.com/frappe/erpnext/pull/23559 "invoice_number": row.invoice_number, "disable_rounded_total": 1 }) 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 6700e9b975..77d54a605e 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 @@ -1,11 +1,8 @@ # Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt -import unittest - import frappe -from frappe.cache_manager import clear_doctype_cache -from frappe.custom.doctype.property_setter.property_setter import make_property_setter +from frappe.tests.utils import FrappeTestCase from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import ( create_dimension, @@ -17,11 +14,13 @@ from erpnext.accounts.doctype.opening_invoice_creation_tool.opening_invoice_crea test_dependencies = ["Customer", "Supplier", "Accounting Dimension"] -class TestOpeningInvoiceCreationTool(unittest.TestCase): - def setUp(self): +class TestOpeningInvoiceCreationTool(FrappeTestCase): + @classmethod + def setUpClass(self): if not frappe.db.exists("Company", "_Test Opening Invoice Company"): make_company() create_dimension() + return super().setUpClass() def make_invoices(self, invoice_type="Sales", company=None, party_1=None, party_2=None, invoice_number=None, department=None): doc = frappe.get_single("Opening Invoice Creation Tool") @@ -31,26 +30,20 @@ class TestOpeningInvoiceCreationTool(unittest.TestCase): return doc.make_invoices() def test_opening_sales_invoice_creation(self): - property_setter = make_property_setter("Sales Invoice", "update_stock", "default", 1, "Check") - try: - invoices = self.make_invoices(company="_Test Opening Invoice Company") + 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) - - finally: - property_setter.delete() - clear_doctype_cache("Sales Invoice") + # Check if update stock is not enabled + self.assertEqual(si.update_stock, 0) def check_expected_values(self, invoices, expected_value, invoice_type="Sales"): doctype = "Sales Invoice" if invoice_type == "Sales" else "Purchase Invoice" diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index 3be3925b5a..b2b818a214 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -114,8 +114,6 @@ frappe.ui.form.on('Payment Entry', { var doctypes = ["Expense Claim", "Journal Entry"]; } else if (frm.doc.party_type == "Student") { var doctypes = ["Fees"]; - } else if (frm.doc.party_type == "Donor") { - var doctypes = ["Donation"]; } else { var doctypes = ["Journal Entry"]; } @@ -144,7 +142,7 @@ frappe.ui.form.on('Payment Entry', { const child = locals[cdt][cdn]; const filters = {"docstatus": 1, "company": doc.company}; const party_type_doctypes = ['Sales Invoice', 'Sales Order', 'Purchase Invoice', - 'Purchase Order', 'Expense Claim', 'Fees', 'Dunning', 'Donation']; + 'Purchase Order', 'Expense Claim', 'Fees', 'Dunning']; if (in_list(party_type_doctypes, child.reference_doctype)) { filters[doc.party_type.toLowerCase()] = doc.party; @@ -196,8 +194,14 @@ frappe.ui.form.on('Payment Entry', { frm.doc.paid_from_account_currency != frm.doc.paid_to_account_currency)); frm.toggle_display("base_paid_amount", frm.doc.paid_from_account_currency != company_currency); - frm.toggle_display("base_total_taxes_and_charges", frm.doc.total_taxes_and_charges && - (frm.doc.paid_from_account_currency != company_currency)); + + if (frm.doc.payment_type == "Pay") { + frm.toggle_display("base_total_taxes_and_charges", frm.doc.total_taxes_and_charges && + (frm.doc.paid_to_account_currency != company_currency)); + } else { + frm.toggle_display("base_total_taxes_and_charges", frm.doc.total_taxes_and_charges && + (frm.doc.paid_from_account_currency != company_currency)); + } frm.toggle_display("base_received_amount", ( frm.doc.paid_to_account_currency != company_currency @@ -232,7 +236,8 @@ frappe.ui.form.on('Payment Entry', { var company_currency = frm.doc.company? frappe.get_doc(":Company", frm.doc.company).default_currency: ""; frm.set_currency_labels(["base_paid_amount", "base_received_amount", "base_total_allocated_amount", - "difference_amount", "base_paid_amount_after_tax", "base_received_amount_after_tax"], company_currency); + "difference_amount", "base_paid_amount_after_tax", "base_received_amount_after_tax", + "base_total_taxes_and_charges"], company_currency); frm.set_currency_labels(["paid_amount"], frm.doc.paid_from_account_currency); frm.set_currency_labels(["received_amount"], frm.doc.paid_to_account_currency); @@ -341,6 +346,8 @@ frappe.ui.form.on('Payment Entry', { } frm.set_party_account_based_on_party = true; + let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency; + return frappe.call({ method: "erpnext.accounts.doctype.payment_entry.payment_entry.get_party_details", args: { @@ -374,7 +381,11 @@ frappe.ui.form.on('Payment Entry', { if (r.message.bank_account) { frm.set_value("bank_account", r.message.bank_account); } - } + }, + () => frm.events.set_current_exchange_rate(frm, "source_exchange_rate", + frm.doc.paid_from_account_currency, company_currency), + () => frm.events.set_current_exchange_rate(frm, "target_exchange_rate", + frm.doc.paid_to_account_currency, company_currency) ]); } } @@ -478,14 +489,14 @@ frappe.ui.form.on('Payment Entry', { }, paid_from_account_currency: function(frm) { - if(!frm.doc.paid_from_account_currency) return; - var company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency; + if(!frm.doc.paid_from_account_currency || !frm.doc.company) return; + let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency; if (frm.doc.paid_from_account_currency == company_currency) { frm.set_value("source_exchange_rate", 1); } else if (frm.doc.paid_from){ if (in_list(["Internal Transfer", "Pay"], frm.doc.payment_type)) { - var company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency; + let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency; frappe.call({ method: "erpnext.setup.utils.get_exchange_rate", args: { @@ -505,8 +516,8 @@ frappe.ui.form.on('Payment Entry', { }, paid_to_account_currency: function(frm) { - if(!frm.doc.paid_to_account_currency) return; - var company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency; + if(!frm.doc.paid_to_account_currency || !frm.doc.company) return; + let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency; frm.events.set_current_exchange_rate(frm, "target_exchange_rate", frm.doc.paid_to_account_currency, company_currency); @@ -747,8 +758,7 @@ frappe.ui.form.on('Payment Entry', { (frm.doc.payment_type=="Receive" && frm.doc.party_type=="Customer") || (frm.doc.payment_type=="Pay" && frm.doc.party_type=="Supplier") || (frm.doc.payment_type=="Pay" && frm.doc.party_type=="Employee") || - (frm.doc.payment_type=="Receive" && frm.doc.party_type=="Student") || - (frm.doc.payment_type=="Receive" && frm.doc.party_type=="Donor") + (frm.doc.payment_type=="Receive" && frm.doc.party_type=="Student") ) { if(total_positive_outstanding > total_negative_outstanding) if (!frm.doc.paid_amount) @@ -791,8 +801,7 @@ frappe.ui.form.on('Payment Entry', { (frm.doc.payment_type=="Receive" && frm.doc.party_type=="Customer") || (frm.doc.payment_type=="Pay" && frm.doc.party_type=="Supplier") || (frm.doc.payment_type=="Pay" && frm.doc.party_type=="Employee") || - (frm.doc.payment_type=="Receive" && frm.doc.party_type=="Student") || - (frm.doc.payment_type=="Receive" && frm.doc.party_type=="Donor") + (frm.doc.payment_type=="Receive" && frm.doc.party_type=="Student") ) { if(total_positive_outstanding_including_order > paid_amount) { var remaining_outstanding = total_positive_outstanding_including_order - paid_amount; @@ -951,12 +960,6 @@ frappe.ui.form.on('Payment Entry', { frappe.msgprint(__("Row #{0}: Reference Document Type must be one of Expense Claim or Journal Entry", [row.idx])); return false; } - - if (frm.doc.party_type == "Donor" && row.reference_doctype != "Donation") { - frappe.model.set_value(row.doctype, row.name, "reference_doctype", null); - frappe.msgprint(__("Row #{0}: Reference Document Type must be Donation", [row.idx])); - return false; - } } if (row) { diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.json b/erpnext/accounts/doctype/payment_entry/payment_entry.json index c8d1db91f5..3fc1adff2d 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.json +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.json @@ -66,7 +66,9 @@ "tax_withholding_category", "section_break_56", "taxes", + "section_break_60", "base_total_taxes_and_charges", + "column_break_61", "total_taxes_and_charges", "deductions_or_loss_section", "deductions", @@ -715,12 +717,21 @@ "fieldtype": "Data", "hidden": 1, "label": "Paid To Account Type" + }, + { + "fieldname": "column_break_61", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_60", + "fieldtype": "Section Break", + "hide_border": 1 } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-11-24 18:58:24.919764", + "modified": "2022-02-23 20:08:39.559814", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Entry", @@ -763,6 +774,7 @@ "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "title_field": "title", "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 02a144d3e7..f9f33502d3 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -91,7 +91,6 @@ class PaymentEntry(AccountsController): self.update_expense_claim() self.update_outstanding_amounts() self.update_advance_paid() - self.update_donation() self.update_payment_schedule() self.set_status() @@ -101,7 +100,6 @@ class PaymentEntry(AccountsController): self.update_expense_claim() self.update_outstanding_amounts() self.update_advance_paid() - self.update_donation(cancel=1) self.delink_advance_entry_references() self.update_payment_schedule(cancel=1) self.set_payment_req_status() @@ -284,8 +282,6 @@ class PaymentEntry(AccountsController): valid_reference_doctypes = ("Expense Claim", "Journal Entry", "Employee Advance", "Gratuity") elif self.party_type == "Shareholder": valid_reference_doctypes = ("Journal Entry") - elif self.party_type == "Donor": - valid_reference_doctypes = ("Donation") for d in self.get("references"): if not d.allocated_amount: @@ -843,13 +839,6 @@ class PaymentEntry(AccountsController): else: update_reimbursed_amount(doc, d.allocated_amount) - def update_donation(self, cancel=0): - if self.payment_type == "Receive" and self.party_type == "Donor" and self.party: - for d in self.get("references"): - if d.reference_doctype=="Donation" and d.reference_name: - is_paid = 0 if cancel else 1 - frappe.db.set_value("Donation", d.reference_name, "paid", is_paid) - def on_recurring(self, reference_doc, auto_repeat_doc): self.reference_no = reference_doc.name self.reference_date = nowdate() @@ -945,8 +934,12 @@ class PaymentEntry(AccountsController): tax.base_total = tax.total * self.source_exchange_rate - self.total_taxes_and_charges += current_tax_amount - self.base_total_taxes_and_charges += current_tax_amount * self.source_exchange_rate + if self.payment_type == 'Pay': + self.base_total_taxes_and_charges += flt(current_tax_amount / self.source_exchange_rate) + self.total_taxes_and_charges += flt(current_tax_amount / self.target_exchange_rate) + else: + self.base_total_taxes_and_charges += flt(current_tax_amount / self.target_exchange_rate) + self.total_taxes_and_charges += flt(current_tax_amount / self.source_exchange_rate) if self.get('taxes'): self.paid_amount_after_tax = self.get('taxes')[-1].base_total @@ -1077,7 +1070,7 @@ def get_outstanding_reference_documents(args): if d.voucher_type in ("Purchase Invoice"): d["bill_no"] = frappe.db.get_value(d.voucher_type, d.voucher_no, "bill_no") - # Get all SO / PO which are not fully billed or aginst which full advance not paid + # Get all SO / PO which are not fully billed or against which full advance not paid orders_to_be_billed = [] if (args.get("party_type") != "Student"): orders_to_be_billed = get_orders_to_be_billed(args.get("posting_date"),args.get("party_type"), @@ -1337,10 +1330,6 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre total_amount = ref_doc.get("grand_total") exchange_rate = 1 outstanding_amount = ref_doc.get("outstanding_amount") - elif reference_doctype == "Donation": - total_amount = ref_doc.get("amount") - outstanding_amount = total_amount - exchange_rate = 1 elif reference_doctype == "Dunning": total_amount = ref_doc.get("dunning_amount") exchange_rate = 1 @@ -1611,8 +1600,6 @@ def set_party_type(dt): party_type = "Employee" elif dt == "Fees": party_type = "Student" - elif dt == "Donation": - party_type = "Donor" return party_type def set_party_account(dt, dn, doc, party_type): @@ -1640,7 +1627,7 @@ def set_party_account_currency(dt, party_account, doc): return party_account_currency def set_payment_type(dt, doc): - if (dt in ("Sales Order", "Donation") or (dt in ("Sales Invoice", "Fees", "Dunning") and doc.outstanding_amount > 0)) \ + if (dt == "Sales Order" or (dt in ("Sales Invoice", "Fees", "Dunning") and doc.outstanding_amount > 0)) \ or (dt=="Purchase Invoice" and doc.outstanding_amount < 0): payment_type = "Receive" else: @@ -1673,9 +1660,6 @@ def set_grand_total_and_outstanding_amount(party_amount, dt, party_account_curre elif dt == "Dunning": grand_total = doc.grand_total outstanding_amount = doc.grand_total - elif dt == "Donation": - grand_total = doc.amount - outstanding_amount = doc.amount elif dt == "Gratuity": grand_total = doc.amount outstanding_amount = flt(doc.amount) - flt(doc.paid_amount) diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py index cc3528e9aa..349b8bb5b1 100644 --- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py @@ -633,6 +633,45 @@ class TestPaymentEntry(unittest.TestCase): self.assertEqual(flt(expected_party_balance), party_balance) self.assertEqual(flt(expected_party_account_balance), party_account_balance) + def test_multi_currency_payment_entry_with_taxes(self): + payment_entry = create_payment_entry(party='_Test Supplier USD', paid_to = '_Test Payable USD - _TC', + save=True) + payment_entry.append('taxes', { + 'account_head': '_Test Account Service Tax - _TC', + 'charge_type': 'Actual', + 'tax_amount': 10, + 'add_deduct_tax': 'Add', + 'description': 'Test' + }) + + payment_entry.save() + self.assertEqual(payment_entry.base_total_taxes_and_charges, 10) + self.assertEqual(flt(payment_entry.total_taxes_and_charges, 2), flt(10 / payment_entry.target_exchange_rate, 2)) + +def create_payment_entry(**args): + payment_entry = frappe.new_doc('Payment Entry') + payment_entry.company = args.get('company') or '_Test Company' + payment_entry.payment_type = args.get('payment_type') or 'Pay' + payment_entry.party_type = args.get('party_type') or 'Supplier' + payment_entry.party = args.get('party') or '_Test Supplier' + payment_entry.paid_from = args.get('paid_from') or '_Test Bank - _TC' + payment_entry.paid_to = args.get('paid_to') or 'Creditors - _TC' + payment_entry.paid_amount = args.get('paid_amount') or 1000 + + payment_entry.setup_party_account_field() + payment_entry.set_missing_values() + payment_entry.set_exchange_rate() + payment_entry.received_amount = payment_entry.paid_amount / payment_entry.target_exchange_rate + payment_entry.reference_no = 'Test001' + payment_entry.reference_date = nowdate() + + if args.get('save'): + payment_entry.save() + if args.get('submit'): + payment_entry.submit() + + return payment_entry + def create_payment_terms_template(): create_payment_term('Basic Amount Receivable') diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index 5229d87017..9b3b3aa414 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -439,7 +439,6 @@ class POSInvoice(SalesInvoice): 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") diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py index ddca68a57b..d4513c6a68 100644 --- a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py +++ b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py @@ -84,20 +84,12 @@ class POSInvoiceMergeLog(Document): sales_invoice.set_posting_time = 1 sales_invoice.posting_date = getdate(self.posting_date) sales_invoice.save() - self.write_off_fractional_amount(sales_invoice, data) sales_invoice.submit() self.consolidated_invoice = sales_invoice.name return sales_invoice.name - def write_off_fractional_amount(self, invoice, data): - pos_invoice_grand_total = sum(d.grand_total for d in data) - - if abs(pos_invoice_grand_total - invoice.grand_total) < 1: - invoice.write_off_amount += -1 * (pos_invoice_grand_total - invoice.grand_total) - invoice.save() - def process_merging_into_credit_note(self, data): credit_note = self.get_new_sales_invoice() credit_note.is_return = 1 @@ -110,7 +102,6 @@ class POSInvoiceMergeLog(Document): # TODO: return could be against multiple sales invoice which could also have been consolidated? # credit_note.return_against = self.consolidated_invoice credit_note.save() - self.write_off_fractional_amount(credit_note, data) credit_note.submit() self.consolidated_credit_note = credit_note.name 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 5930aa097f..89f7f18b42 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 @@ -5,6 +5,7 @@ import json import unittest import frappe +from frappe.tests.utils import change_settings from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import init_user_and_profile from erpnext.accounts.doctype.pos_invoice.pos_invoice import make_sales_return @@ -280,3 +281,100 @@ class TestPOSInvoiceMergeLog(unittest.TestCase): frappe.set_user("Administrator") frappe.db.sql("delete from `tabPOS Profile`") frappe.db.sql("delete from `tabPOS Invoice`") + + @change_settings("System Settings", {"number_format": "#,###.###", "currency_precision": 3, "float_precision": 3}) + def test_consolidation_round_off_error_3(self): + frappe.db.sql("delete from `tabPOS Invoice`") + + try: + make_stock_entry( + to_warehouse="_Test Warehouse - _TC", + item_code="_Test Item", + rate=8000, + qty=10, + ) + init_user_and_profile() + + item_rates = [69, 59, 29] + for i in [1, 2]: + inv = create_pos_invoice(is_return=1, do_not_save=1) + inv.items = [] + for rate in item_rates: + inv.append("items", { + "item_code": "_Test Item", + "warehouse": "_Test Warehouse - _TC", + "qty": -1, + "rate": rate, + "income_account": "Sales - _TC", + "expense_account": "Cost of Goods Sold - _TC", + "cost_center": "_Test Cost Center - _TC", + }) + inv.append("taxes", { + "account_head": "_Test Account VAT - _TC", + "charge_type": "On Net Total", + "cost_center": "_Test Cost Center - _TC", + "description": "VAT", + "doctype": "Sales Taxes and Charges", + "rate": 15, + "included_in_print_rate": 1 + }) + inv.payments = [] + inv.append('payments', { + 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': -157 + }) + inv.paid_amount = -157 + inv.save() + inv.submit() + + consolidate_pos_invoices() + + inv.load_from_db() + consolidated_invoice = frappe.get_doc('Sales Invoice', inv.consolidated_invoice) + self.assertEqual(consolidated_invoice.status, 'Return') + self.assertEqual(consolidated_invoice.rounding_adjustment, -0.001) + + finally: + frappe.set_user("Administrator") + frappe.db.sql("delete from `tabPOS Profile`") + frappe.db.sql("delete from `tabPOS Invoice`") + + def test_consolidation_rounding_adjustment(self): + ''' + Test if the rounding adjustment is calculated correctly + ''' + frappe.db.sql("delete from `tabPOS Invoice`") + + try: + make_stock_entry( + to_warehouse="_Test Warehouse - _TC", + item_code="_Test Item", + rate=8000, + qty=10, + ) + + init_user_and_profile() + + inv = create_pos_invoice(qty=1, rate=69.5, do_not_save=True) + inv.append('payments', { + 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 70 + }) + inv.insert() + inv.submit() + + inv2 = create_pos_invoice(qty=1, rate=59.5, do_not_save=True) + inv2.append('payments', { + 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 60 + }) + inv2.insert() + inv2.submit() + + consolidate_pos_invoices() + + inv.load_from_db() + consolidated_invoice = frappe.get_doc('Sales Invoice', inv.consolidated_invoice) + self.assertEqual(consolidated_invoice.rounding_adjustment, 1) + + finally: + frappe.set_user("Administrator") + frappe.db.sql("delete from `tabPOS Profile`") + frappe.db.sql("delete from `tabPOS Invoice`") \ No newline at end of file diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py index 09aa72352e..1b34d6d1f2 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py @@ -73,7 +73,7 @@ def get_report_pdf(doc, consolidated=True): 'to_date': doc.to_date, 'company': doc.company, 'finance_book': doc.finance_book if doc.finance_book else None, - 'account': doc.account if doc.account else None, + 'account': [doc.account] if doc.account else None, 'party_type': 'Customer', 'party': [entry.customer], 'presentation_currency': presentation_currency, diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index b894f90c7e..573da276a2 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -263,6 +263,9 @@ class SalesInvoice(SellingController): self.process_common_party_accounting() def validate_pos_return(self): + if self.is_consolidated: + # pos return is already validated in pos invoice + return if self.is_pos and self.is_return: total_amount_in_payments = 0 diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 941061f2a2..6d929e4386 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -1595,6 +1595,56 @@ class TestSalesInvoice(unittest.TestCase): self.assertEqual(expected_values[gle.account][1], gle.debit) self.assertEqual(expected_values[gle.account][2], gle.credit) + def test_rounding_adjustment_3(self): + si = create_sales_invoice(do_not_save=True) + si.items = [] + for d in [(1122, 2), (1122.01, 1), (1122.01, 1)]: + si.append("items", { + "item_code": "_Test Item", + "gst_hsn_code": "999800", + "warehouse": "_Test Warehouse - _TC", + "qty": d[1], + "rate": d[0], + "income_account": "Sales - _TC", + "cost_center": "_Test Cost Center - _TC" + }) + for tax_account in ["_Test Account VAT - _TC", "_Test Account Service Tax - _TC"]: + si.append("taxes", { + "charge_type": "On Net Total", + "account_head": tax_account, + "description": tax_account, + "rate": 6, + "cost_center": "_Test Cost Center - _TC", + "included_in_print_rate": 1 + }) + si.save() + si.submit() + self.assertEqual(si.net_total, 4007.16) + self.assertEqual(si.grand_total, 4488.02) + self.assertEqual(si.total_taxes_and_charges, 480.86) + self.assertEqual(si.rounding_adjustment, -0.02) + + expected_values = dict((d[0], d) for d in [ + [si.debit_to, 4488.0, 0.0], + ["_Test Account Service Tax - _TC", 0.0, 240.43], + ["_Test Account VAT - _TC", 0.0, 240.43], + ["Sales - _TC", 0.0, 4007.15], + ["Round Off - _TC", 0.01, 0] + ]) + + gl_entries = frappe.db.sql("""select account, debit, credit + from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s + order by account asc""", si.name, as_dict=1) + + debit_credit_diff = 0 + for gle in gl_entries: + self.assertEqual(expected_values[gle.account][0], gle.account) + self.assertEqual(expected_values[gle.account][1], gle.debit) + self.assertEqual(expected_values[gle.account][2], gle.credit) + debit_credit_diff += (gle.debit - gle.credit) + + self.assertEqual(debit_credit_diff, 0) + def test_sales_invoice_with_shipping_rule(self): from erpnext.accounts.doctype.shipping_rule.test_shipping_rule import create_shipping_rule @@ -2429,14 +2479,22 @@ class TestSalesInvoice(unittest.TestCase): def test_sales_commission(self): - si = frappe.copy_doc(test_records[0]) + si = frappe.copy_doc(test_records[2]) + + frappe.db.set_value('Item', si.get('items')[0].item_code, 'grant_commission', 1) + frappe.db.set_value('Item', si.get('items')[1].item_code, 'grant_commission', 0) + item = copy.deepcopy(si.get('items')[0]) item.update({ "qty": 1, "rate": 500, - "grant_commission": 1 }) - si.append("items", item) + + item = copy.deepcopy(si.get('items')[1]) + item.update({ + "qty": 1, + "rate": 500, + }) # Test valid values for commission_rate, total_commission in ((0, 0), (10, 50), (100, 500)): diff --git a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json index ae9ac35729..2901cf0888 100644 --- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json +++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json @@ -832,6 +832,7 @@ }, { "default": "0", + "fetch_from": "item_code.grant_commission", "fieldname": "grant_commission", "fieldtype": "Check", "label": "Grant Commission", @@ -841,7 +842,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2021-10-05 12:24:54.968907", + "modified": "2022-02-24 14:41:36.392560", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice Item", @@ -849,5 +850,6 @@ "owner": "Administrator", "permissions": [], "sort_field": "modified", - "sort_order": "DESC" + "sort_order": "DESC", + "states": [] } \ 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 b5909447dc..8043a1b66f 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,7 +46,7 @@ def valdiate_taxes_and_charges_template(doc): for tax in doc.get("taxes"): validate_taxes_and_charges(tax) - validate_account_head(tax, doc) + validate_account_head(tax.idx, tax.account_head, doc.company) validate_cost_center(tax, doc) validate_inclusive_tax(tax, doc) @@ -55,5 +55,8 @@ def validate_disabled(doc): frappe.throw(_("Disabled template must not be default template")) def validate_for_tax_category(doc): + if not doc.tax_category: + return + 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/general_ledger.py b/erpnext/accounts/general_ledger.py index d24d56b4bb..0cd5e86a8c 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -274,7 +274,7 @@ def make_round_off_gle(gl_map, debit_credit_diff, precision): debit_credit_diff += flt(d.credit) round_off_account_exists = True - if round_off_account_exists and abs(debit_credit_diff) <= (1.0 / (10**precision)): + if round_off_account_exists and abs(debit_credit_diff) < (1.0 / (10**precision)): gl_map.remove(round_off_gle) return diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index c13bc23c15..d6f6c5bcb6 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -307,7 +307,7 @@ def validate_party_gle_currency(party_type, party, company, party_account_curren .format(frappe.bold(party_type), frappe.bold(party), frappe.bold(existing_gle_currency), frappe.bold(company)), InvalidAccountCurrency) def validate_party_accounts(doc): - + from erpnext.controllers.accounts_controller import validate_account_head companies = [] for account in doc.get("accounts"): @@ -330,6 +330,9 @@ def validate_party_accounts(doc): if doc.default_currency != party_account_currency and doc.default_currency != company_default_currency: frappe.throw(_("Billing currency must be equal to either default company's currency or party account currency")) + # validate if account is mapped for same company + validate_account_head(account.idx, account.account, account.company) + @frappe.whitelist() def get_due_date(posting_date, party_type, party, company=None, bill_date=None): diff --git a/erpnext/accounts/report/tax_detail/test_tax_detail.py b/erpnext/accounts/report/tax_detail/test_tax_detail.py index bf668ab779..621de825ea 100644 --- a/erpnext/accounts/report/tax_detail/test_tax_detail.py +++ b/erpnext/accounts/report/tax_detail/test_tax_detail.py @@ -61,7 +61,7 @@ class TestTaxDetail(unittest.TestCase): # Create GL Entries: db_doc.submit() else: - db_doc.insert() + db_doc.insert(ignore_if_duplicate=True) except frappe.exceptions.DuplicateEntryError: pass diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 39e84e3cef..b17b90ba6e 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -847,7 +847,7 @@ def create_payment_gateway_account(gateway, payment_channel="Email"): "payment_account": bank_account.name, "currency": bank_account.account_currency, "payment_channel": payment_channel - }).insert(ignore_permissions=True) + }).insert(ignore_permissions=True, ignore_if_duplicate=True) except frappe.DuplicateEntryError: # already exists, due to a reinstall? diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index ddbff89fc7..ffd1065efc 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -1280,7 +1280,7 @@ def create_asset(**args): if not args.do_not_save: try: - asset.save() + asset.insert(ignore_if_duplicate=True) except frappe.DuplicateEntryError: pass @@ -1321,7 +1321,7 @@ def create_fixed_asset_item(item_code=None, auto_create_assets=1, is_grouped_ass "is_grouped_asset": is_grouped_asset, "asset_naming_series": naming_series }) - item.insert() + item.insert(ignore_if_duplicate=True) except frappe.DuplicateEntryError: pass return item diff --git a/erpnext/assets/doctype/asset_category/test_asset_category.py b/erpnext/assets/doctype/asset_category/test_asset_category.py index 3d19fa39d1..2f52248edb 100644 --- a/erpnext/assets/doctype/asset_category/test_asset_category.py +++ b/erpnext/assets/doctype/asset_category/test_asset_category.py @@ -23,7 +23,7 @@ class TestAssetCategory(unittest.TestCase): }) try: - asset_category.insert() + asset_category.insert(ignore_if_duplicate=True) except frappe.DuplicateEntryError: pass diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 1b5f35efbb..2e7d3063cc 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -316,6 +316,16 @@ class PurchaseOrder(BuyingController): 'target_ref_field': 'stock_qty', 'source_field': 'stock_qty' }) + self.status_updater.append({ + 'source_dt': 'Purchase Order Item', + 'target_dt': 'Packed Item', + 'target_field': 'ordered_qty', + 'target_parent_dt': 'Sales Order', + 'target_parent_field': '', + 'join_field': 'sales_order_packed_item', + 'target_ref_field': 'qty', + 'source_field': 'stock_qty' + }) def update_delivered_qty_in_sales_order(self): """Update delivered qty in Sales Order for drop ship""" diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index 645e97ee7c..efa2ab1268 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -3,9 +3,9 @@ import json -import unittest import frappe +from frappe.tests.utils import FrappeTestCase from frappe.utils import add_days, flt, getdate, nowdate from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry @@ -27,7 +27,7 @@ from erpnext.stock.doctype.purchase_receipt.purchase_receipt import ( from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry -class TestPurchaseOrder(unittest.TestCase): +class TestPurchaseOrder(FrappeTestCase): def test_make_purchase_receipt(self): po = create_purchase_order(do_not_submit=True) self.assertRaises(frappe.ValidationError, make_purchase_receipt, po.name) diff --git a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json index 87cd57517e..a18c527644 100644 --- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json +++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json @@ -63,6 +63,7 @@ "material_request_item", "sales_order", "sales_order_item", + "sales_order_packed_item", "supplier_quotation", "supplier_quotation_item", "col_break5", @@ -837,21 +838,30 @@ "label": "Product Bundle", "options": "Product Bundle", "read_only": 1 + }, + { + "fieldname": "sales_order_packed_item", + "fieldtype": "Data", + "label": "Sales Order Packed Item", + "no_copy": 1, + "print_hide": 1 } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-08-30 20:06:26.712097", + "modified": "2022-02-02 13:10:18.398976", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order Item", + "naming_rule": "Random", "owner": "Administrator", "permissions": [], "quick_entry": 1, "search_fields": "item_name", "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py index 51901991b5..5b2112424c 100644 --- a/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py +++ b/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py @@ -1,9 +1,9 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt -import unittest import frappe +from frappe.tests.utils import FrappeTestCase from frappe.utils import nowdate from erpnext.buying.doctype.request_for_quotation.request_for_quotation import ( @@ -16,7 +16,7 @@ from erpnext.stock.doctype.item.test_item import make_item from erpnext.templates.pages.rfq import check_supplier_has_docname_access -class TestRequestforQuotation(unittest.TestCase): +class TestRequestforQuotation(FrappeTestCase): def test_quote_status(self): rfq = make_request_for_quotation() diff --git a/erpnext/buying/doctype/supplier/test_supplier.py b/erpnext/buying/doctype/supplier/test_supplier.py index 13fe9df13e..7358e2af22 100644 --- a/erpnext/buying/doctype/supplier/test_supplier.py +++ b/erpnext/buying/doctype/supplier/test_supplier.py @@ -1,7 +1,6 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt -import unittest import frappe from frappe.test_runner import make_test_records @@ -12,153 +11,154 @@ from erpnext.exceptions import PartyDisabled test_dependencies = ['Payment Term', 'Payment Terms Template'] test_records = frappe.get_test_records('Supplier') +from frappe.tests.utils import FrappeTestCase -class TestSupplier(unittest.TestCase): - def test_get_supplier_group_details(self): - doc = frappe.new_doc("Supplier Group") - doc.supplier_group_name = "_Testing Supplier Group" - doc.payment_terms = "_Test Payment Term Template 3" - doc.accounts = [] - test_account_details = { - "company": "_Test Company", - "account": "Creditors - _TC", - } - doc.append("accounts", test_account_details) - doc.save() - s_doc = frappe.new_doc("Supplier") - s_doc.supplier_name = "Testing Supplier" - s_doc.supplier_group = "_Testing Supplier Group" - s_doc.payment_terms = "" - s_doc.accounts = [] - s_doc.insert() - s_doc.get_supplier_group_details() - self.assertEqual(s_doc.payment_terms, "_Test Payment Term Template 3") - self.assertEqual(s_doc.accounts[0].company, "_Test Company") - self.assertEqual(s_doc.accounts[0].account, "Creditors - _TC") - s_doc.delete() - doc.delete() - def test_supplier_default_payment_terms(self): - # Payment Term based on Days after invoice date - frappe.db.set_value( - "Supplier", "_Test Supplier With Template 1", "payment_terms", "_Test Payment Term Template 3") +class TestSupplier(FrappeTestCase): + def test_get_supplier_group_details(self): + doc = frappe.new_doc("Supplier Group") + doc.supplier_group_name = "_Testing Supplier Group" + doc.payment_terms = "_Test Payment Term Template 3" + doc.accounts = [] + test_account_details = { + "company": "_Test Company", + "account": "Creditors - _TC", + } + doc.append("accounts", test_account_details) + doc.save() + s_doc = frappe.new_doc("Supplier") + s_doc.supplier_name = "Testing Supplier" + s_doc.supplier_group = "_Testing Supplier Group" + s_doc.payment_terms = "" + s_doc.accounts = [] + s_doc.insert() + s_doc.get_supplier_group_details() + self.assertEqual(s_doc.payment_terms, "_Test Payment Term Template 3") + self.assertEqual(s_doc.accounts[0].company, "_Test Company") + self.assertEqual(s_doc.accounts[0].account, "Creditors - _TC") + s_doc.delete() + doc.delete() - due_date = get_due_date("2016-01-22", "Supplier", "_Test Supplier With Template 1") - self.assertEqual(due_date, "2016-02-21") + def test_supplier_default_payment_terms(self): + # Payment Term based on Days after invoice date + frappe.db.set_value( + "Supplier", "_Test Supplier With Template 1", "payment_terms", "_Test Payment Term Template 3") - due_date = get_due_date("2017-01-22", "Supplier", "_Test Supplier With Template 1") - self.assertEqual(due_date, "2017-02-21") + due_date = get_due_date("2016-01-22", "Supplier", "_Test Supplier With Template 1") + self.assertEqual(due_date, "2016-02-21") - # Payment Term based on last day of month - frappe.db.set_value( - "Supplier", "_Test Supplier With Template 1", "payment_terms", "_Test Payment Term Template 1") + due_date = get_due_date("2017-01-22", "Supplier", "_Test Supplier With Template 1") + self.assertEqual(due_date, "2017-02-21") - due_date = get_due_date("2016-01-22", "Supplier", "_Test Supplier With Template 1") - self.assertEqual(due_date, "2016-02-29") + # Payment Term based on last day of month + frappe.db.set_value( + "Supplier", "_Test Supplier With Template 1", "payment_terms", "_Test Payment Term Template 1") - due_date = get_due_date("2017-01-22", "Supplier", "_Test Supplier With Template 1") - self.assertEqual(due_date, "2017-02-28") + due_date = get_due_date("2016-01-22", "Supplier", "_Test Supplier With Template 1") + self.assertEqual(due_date, "2016-02-29") - frappe.db.set_value("Supplier", "_Test Supplier With Template 1", "payment_terms", "") + due_date = get_due_date("2017-01-22", "Supplier", "_Test Supplier With Template 1") + self.assertEqual(due_date, "2017-02-28") - # Set credit limit for the supplier group instead of supplier and evaluate the due date - frappe.db.set_value("Supplier Group", "_Test Supplier Group", "payment_terms", "_Test Payment Term Template 3") + frappe.db.set_value("Supplier", "_Test Supplier With Template 1", "payment_terms", "") - due_date = get_due_date("2016-01-22", "Supplier", "_Test Supplier With Template 1") - self.assertEqual(due_date, "2016-02-21") + # Set credit limit for the supplier group instead of supplier and evaluate the due date + frappe.db.set_value("Supplier Group", "_Test Supplier Group", "payment_terms", "_Test Payment Term Template 3") - # Payment terms for Supplier Group instead of supplier and evaluate the due date - frappe.db.set_value("Supplier Group", "_Test Supplier Group", "payment_terms", "_Test Payment Term Template 1") + due_date = get_due_date("2016-01-22", "Supplier", "_Test Supplier With Template 1") + self.assertEqual(due_date, "2016-02-21") - # Leap year - due_date = get_due_date("2016-01-22", "Supplier", "_Test Supplier With Template 1") - self.assertEqual(due_date, "2016-02-29") - # # Non Leap year - due_date = get_due_date("2017-01-22", "Supplier", "_Test Supplier With Template 1") - self.assertEqual(due_date, "2017-02-28") + # Payment terms for Supplier Group instead of supplier and evaluate the due date + frappe.db.set_value("Supplier Group", "_Test Supplier Group", "payment_terms", "_Test Payment Term Template 1") - # Supplier with no default Payment Terms Template - frappe.db.set_value("Supplier Group", "_Test Supplier Group", "payment_terms", "") - frappe.db.set_value("Supplier", "_Test Supplier", "payment_terms", "") + # Leap year + due_date = get_due_date("2016-01-22", "Supplier", "_Test Supplier With Template 1") + self.assertEqual(due_date, "2016-02-29") + # # Non Leap year + due_date = get_due_date("2017-01-22", "Supplier", "_Test Supplier With Template 1") + self.assertEqual(due_date, "2017-02-28") - due_date = get_due_date("2016-01-22", "Supplier", "_Test Supplier") - self.assertEqual(due_date, "2016-01-22") - # # Non Leap year - due_date = get_due_date("2017-01-22", "Supplier", "_Test Supplier") - self.assertEqual(due_date, "2017-01-22") + # Supplier with no default Payment Terms Template + frappe.db.set_value("Supplier Group", "_Test Supplier Group", "payment_terms", "") + frappe.db.set_value("Supplier", "_Test Supplier", "payment_terms", "") - def test_supplier_disabled(self): - make_test_records("Item") + due_date = get_due_date("2016-01-22", "Supplier", "_Test Supplier") + self.assertEqual(due_date, "2016-01-22") + # # Non Leap year + due_date = get_due_date("2017-01-22", "Supplier", "_Test Supplier") + self.assertEqual(due_date, "2017-01-22") - frappe.db.set_value("Supplier", "_Test Supplier", "disabled", 1) + def test_supplier_disabled(self): + make_test_records("Item") - from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order + frappe.db.set_value("Supplier", "_Test Supplier", "disabled", 1) - po = create_purchase_order(do_not_save=True) + from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order - self.assertRaises(PartyDisabled, po.save) + po = create_purchase_order(do_not_save=True) - frappe.db.set_value("Supplier", "_Test Supplier", "disabled", 0) + self.assertRaises(PartyDisabled, po.save) - po.save() + frappe.db.set_value("Supplier", "_Test Supplier", "disabled", 0) - def test_supplier_country(self): - # Test that country field exists in Supplier DocType - supplier = frappe.get_doc('Supplier', '_Test Supplier with Country') - self.assertTrue('country' in supplier.as_dict()) + po.save() - # Test if test supplier field record is 'Greece' - self.assertEqual(supplier.country, "Greece") + def test_supplier_country(self): + # Test that country field exists in Supplier DocType + supplier = frappe.get_doc('Supplier', '_Test Supplier with Country') + self.assertTrue('country' in supplier.as_dict()) - # Test update Supplier instance country value - supplier = frappe.get_doc('Supplier', '_Test Supplier') - supplier.country = 'Greece' - supplier.save() - self.assertEqual(supplier.country, "Greece") + # Test if test supplier field record is 'Greece' + self.assertEqual(supplier.country, "Greece") - def test_party_details_tax_category(self): - from erpnext.accounts.party import get_party_details + # Test update Supplier instance country value + supplier = frappe.get_doc('Supplier', '_Test Supplier') + supplier.country = 'Greece' + supplier.save() + self.assertEqual(supplier.country, "Greece") - frappe.delete_doc_if_exists("Address", "_Test Address With Tax Category-Billing") + def test_party_details_tax_category(self): + from erpnext.accounts.party import get_party_details - # Tax Category without Address - details = get_party_details("_Test Supplier With Tax Category", party_type="Supplier") - self.assertEqual(details.tax_category, "_Test Tax Category 1") + frappe.delete_doc_if_exists("Address", "_Test Address With Tax Category-Billing") - address = frappe.get_doc(dict( - doctype='Address', - address_title='_Test Address With Tax Category', - tax_category='_Test Tax Category 2', - address_type='Billing', - address_line1='Station Road', - city='_Test City', - country='India', - links=[dict( - link_doctype='Supplier', - link_name='_Test Supplier With Tax Category' - )] - )).insert() + # Tax Category without Address + details = get_party_details("_Test Supplier With Tax Category", party_type="Supplier") + self.assertEqual(details.tax_category, "_Test Tax Category 1") - # Tax Category with Address - details = get_party_details("_Test Supplier With Tax Category", party_type="Supplier") - self.assertEqual(details.tax_category, "_Test Tax Category 2") + address = frappe.get_doc(dict( + doctype='Address', + address_title='_Test Address With Tax Category', + tax_category='_Test Tax Category 2', + address_type='Billing', + address_line1='Station Road', + city='_Test City', + country='India', + links=[dict( + link_doctype='Supplier', + link_name='_Test Supplier With Tax Category' + )] + )).insert() - # Rollback - address.delete() + # Tax Category with Address + details = get_party_details("_Test Supplier With Tax Category", party_type="Supplier") + self.assertEqual(details.tax_category, "_Test Tax Category 2") + + # Rollback + address.delete() def create_supplier(**args): - args = frappe._dict(args) + args = frappe._dict(args) - try: - doc = frappe.get_doc({ - "doctype": "Supplier", - "supplier_name": args.supplier_name, - "supplier_group": args.supplier_group or "Services", - "supplier_type": args.supplier_type or "Company", - "tax_withholding_category": args.tax_withholding_category - }).insert() + if frappe.db.exists("Supplier", args.supplier_name): + return frappe.get_doc("Supplier", args.supplier_name) - return doc + doc = frappe.get_doc({ + "doctype": "Supplier", + "supplier_name": args.supplier_name, + "supplier_group": args.supplier_group or "Services", + "supplier_type": args.supplier_type or "Company", + "tax_withholding_category": args.tax_withholding_category + }).insert() - except frappe.DuplicateEntryError: - return frappe.get_doc("Supplier", args.supplier_name) + return doc diff --git a/erpnext/buying/doctype/supplier_quotation/test_supplier_quotation.py b/erpnext/buying/doctype/supplier_quotation/test_supplier_quotation.py index d48ac7eb3b..a4d45975c3 100644 --- a/erpnext/buying/doctype/supplier_quotation/test_supplier_quotation.py +++ b/erpnext/buying/doctype/supplier_quotation/test_supplier_quotation.py @@ -3,12 +3,12 @@ -import unittest import frappe +from frappe.tests.utils import FrappeTestCase -class TestPurchaseOrder(unittest.TestCase): +class TestPurchaseOrder(FrappeTestCase): def test_make_purchase_order(self): from erpnext.buying.doctype.supplier_quotation.supplier_quotation import make_purchase_order diff --git a/erpnext/buying/doctype/supplier_scorecard/test_supplier_scorecard.py b/erpnext/buying/doctype/supplier_scorecard/test_supplier_scorecard.py index 7908c35cbb..8ecc2cd466 100644 --- a/erpnext/buying/doctype/supplier_scorecard/test_supplier_scorecard.py +++ b/erpnext/buying/doctype/supplier_scorecard/test_supplier_scorecard.py @@ -1,12 +1,12 @@ # Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt -import unittest import frappe +from frappe.tests.utils import FrappeTestCase -class TestSupplierScorecard(unittest.TestCase): +class TestSupplierScorecard(FrappeTestCase): def test_create_scorecard(self): doc = make_supplier_scorecard().insert() diff --git a/erpnext/buying/doctype/supplier_scorecard_criteria/test_supplier_scorecard_criteria.py b/erpnext/buying/doctype/supplier_scorecard_criteria/test_supplier_scorecard_criteria.py index dacc982420..7ff84c15e5 100644 --- a/erpnext/buying/doctype/supplier_scorecard_criteria/test_supplier_scorecard_criteria.py +++ b/erpnext/buying/doctype/supplier_scorecard_criteria/test_supplier_scorecard_criteria.py @@ -1,12 +1,12 @@ # Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt -import unittest import frappe +from frappe.tests.utils import FrappeTestCase -class TestSupplierScorecardCriteria(unittest.TestCase): +class TestSupplierScorecardCriteria(FrappeTestCase): def test_variables_exist(self): delete_test_scorecards() for d in test_good_criteria: diff --git a/erpnext/buying/doctype/supplier_scorecard_variable/test_supplier_scorecard_variable.py b/erpnext/buying/doctype/supplier_scorecard_variable/test_supplier_scorecard_variable.py index 4d75981125..32005a37dc 100644 --- a/erpnext/buying/doctype/supplier_scorecard_variable/test_supplier_scorecard_variable.py +++ b/erpnext/buying/doctype/supplier_scorecard_variable/test_supplier_scorecard_variable.py @@ -1,16 +1,16 @@ # Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt -import unittest import frappe +from frappe.tests.utils import FrappeTestCase from erpnext.buying.doctype.supplier_scorecard_variable.supplier_scorecard_variable import ( VariablePathNotFound, ) -class TestSupplierScorecardVariable(unittest.TestCase): +class TestSupplierScorecardVariable(FrappeTestCase): def test_variable_exist(self): for d in test_existing_variables: my_doc = frappe.get_doc("Supplier Scorecard Variable", d.get("name")) diff --git a/erpnext/buying/report/procurement_tracker/test_procurement_tracker.py b/erpnext/buying/report/procurement_tracker/test_procurement_tracker.py index 84de8c6743..44524527e3 100644 --- a/erpnext/buying/report/procurement_tracker/test_procurement_tracker.py +++ b/erpnext/buying/report/procurement_tracker/test_procurement_tracker.py @@ -2,10 +2,10 @@ # For license information, please see license.txt -import unittest from datetime import datetime import frappe +from frappe.tests.utils import FrappeTestCase from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt from erpnext.buying.report.procurement_tracker.procurement_tracker import execute @@ -14,7 +14,7 @@ from erpnext.stock.doctype.material_request.test_material_request import make_ma from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse -class TestProcurementTracker(unittest.TestCase): +class TestProcurementTracker(FrappeTestCase): def test_result_for_procurement_tracker(self): filters = { 'company': '_Test Procurement Company', diff --git a/erpnext/buying/report/requested_items_to_order_and_receive/requested_items_to_order_and_receive.py b/erpnext/buying/report/requested_items_to_order_and_receive/requested_items_to_order_and_receive.py index f98e5f12c2..60a8f92cc3 100644 --- a/erpnext/buying/report/requested_items_to_order_and_receive/requested_items_to_order_and_receive.py +++ b/erpnext/buying/report/requested_items_to_order_and_receive/requested_items_to_order_and_receive.py @@ -6,6 +6,7 @@ import copy import frappe from frappe import _ +from frappe.query_builder.functions import Coalesce, Sum from frappe.utils import date_diff, flt, getdate @@ -16,12 +17,9 @@ def execute(filters=None): validate_filters(filters) columns = get_columns(filters) - conditions = get_conditions(filters) + data = get_data(filters) - #get queried data - data = get_data(filters, conditions) - - #prepare data for report and chart views + # prepare data for report and chart views data, chart_data = prepare_data(data, filters) return columns, data, None, chart_data @@ -34,53 +32,70 @@ def validate_filters(filters): elif date_diff(to_date, from_date) < 0: frappe.throw(_("To Date cannot be before From Date.")) -def get_conditions(filters): - conditions = '' +def get_data(filters): + mr = frappe.qb.DocType("Material Request") + mr_item = frappe.qb.DocType("Material Request Item") + query = ( + frappe.qb.from_(mr) + .join(mr_item).on(mr_item.parent == mr.name) + .select( + mr.name.as_("material_request"), + mr.transaction_date.as_("date"), + mr_item.schedule_date.as_("required_date"), + mr_item.item_code.as_("item_code"), + Sum(Coalesce(mr_item.stock_qty, 0)).as_("qty"), + Coalesce(mr_item.stock_uom, '').as_("uom"), + Sum(Coalesce(mr_item.ordered_qty, 0)).as_("ordered_qty"), + Sum(Coalesce(mr_item.received_qty, 0)).as_("received_qty"), + ( + Sum(Coalesce(mr_item.stock_qty, 0)) - Sum(Coalesce(mr_item.received_qty, 0)) + ).as_("qty_to_receive"), + Sum(Coalesce(mr_item.received_qty, 0)).as_("received_qty"), + ( + Sum(Coalesce(mr_item.stock_qty, 0)) - Sum(Coalesce(mr_item.ordered_qty, 0)) + ).as_("qty_to_order"), + mr_item.item_name, + mr_item.description, + mr.company + ).where( + (mr.material_request_type == "Purchase") + & (mr.docstatus == 1) + & (mr.status != "Stopped") + & (mr.per_received < 100) + ) + ) + + query = get_conditions(filters, query, mr, mr_item) # add conditional conditions + + query = ( + query.groupby( + mr.name, mr_item.item_code + ).orderby( + mr.transaction_date, mr.schedule_date + ) + ) + data = query.run(as_dict=True) + return data + +def get_conditions(filters, query, mr, mr_item): if filters.get("from_date") and filters.get("to_date"): - conditions += " and mr.transaction_date between '{0}' and '{1}'".format(filters.get("from_date"),filters.get("to_date")) - + query = ( + query.where( + (mr.transaction_date >= filters.get("from_date")) + & (mr.transaction_date <= filters.get("to_date")) + ) + ) if filters.get("company"): - conditions += " and mr.company = '{0}'".format(filters.get("company")) + query = query.where(mr.company == filters.get("company")) if filters.get("material_request"): - conditions += " and mr.name = '{0}'".format(filters.get("material_request")) + query = query.where(mr.name == filters.get("material_request")) if filters.get("item_code"): - conditions += " and mr_item.item_code = '{0}'".format(filters.get("item_code")) + query = query.where(mr_item.item_code == filters.get("item_code")) - return conditions - -def get_data(filters, conditions): - data = frappe.db.sql(""" - select - mr.name as material_request, - mr.transaction_date as date, - mr_item.schedule_date as required_date, - mr_item.item_code as item_code, - sum(ifnull(mr_item.stock_qty, 0)) as qty, - ifnull(mr_item.stock_uom, '') as uom, - sum(ifnull(mr_item.ordered_qty, 0)) as ordered_qty, - sum(ifnull(mr_item.received_qty, 0)) as received_qty, - (sum(ifnull(mr_item.stock_qty, 0)) - sum(ifnull(mr_item.received_qty, 0))) as qty_to_receive, - (sum(ifnull(mr_item.stock_qty, 0)) - sum(ifnull(mr_item.ordered_qty, 0))) as qty_to_order, - mr_item.item_name as item_name, - mr_item.description as "description", - mr.company as company - from - `tabMaterial Request` mr, `tabMaterial Request Item` mr_item - where - mr_item.parent = mr.name - and mr.material_request_type = "Purchase" - and mr.docstatus = 1 - and mr.status != "Stopped" - {conditions} - group by mr.name, mr_item.item_code - having - sum(ifnull(mr_item.ordered_qty, 0)) < sum(ifnull(mr_item.stock_qty, 0)) - order by mr.transaction_date, mr.schedule_date""".format(conditions=conditions), as_dict=1) - - return data + return query def update_qty_columns(row_to_update, data_row): fields = ["qty", "ordered_qty", "received_qty", "qty_to_receive", "qty_to_order"] diff --git a/erpnext/buying/report/requested_items_to_order_and_receive/test_requested_items_to_order_and_receive.py b/erpnext/buying/report/requested_items_to_order_and_receive/test_requested_items_to_order_and_receive.py new file mode 100644 index 0000000000..f3c751c5c3 --- /dev/null +++ b/erpnext/buying/report/requested_items_to_order_and_receive/test_requested_items_to_order_and_receive.py @@ -0,0 +1,69 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe +from frappe.tests.utils import FrappeTestCase +from frappe.utils import add_days, today + +from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt +from erpnext.buying.report.requested_items_to_order_and_receive.requested_items_to_order_and_receive import ( + get_data, +) +from erpnext.stock.doctype.item.test_item import create_item +from erpnext.stock.doctype.material_request.material_request import make_purchase_order + + +class TestRequestedItemsToOrderAndReceive(FrappeTestCase): + def setUp(self) -> None: + create_item("Test MR Report Item") + self.setup_material_request() # to order and receive + self.setup_material_request(order=True) # to receive (ordered) + self.setup_material_request(order=True, receive=True) # complete (ordered & received) + + self.filters = frappe._dict( + company="_Test Company", from_date=today(), to_date=add_days(today(), 30), + item_code="Test MR Report Item" + ) + + def tearDown(self) -> None: + frappe.db.rollback() + + def test_date_range(self): + data = get_data(self.filters) + self.assertEqual(len(data), 2) # MRs today should be fetched + + self.filters.from_date = add_days(today(), 1) + data = get_data(self.filters) + self.assertEqual(len(data), 0) # MRs today should not be fetched as from date is tomorrow + + def test_ordered_received_material_requests(self): + data = get_data(self.filters) + + # from the 3 MRs made, only 2 (to receive) should be fetched + self.assertEqual(len(data), 2) + self.assertEqual(data[0].ordered_qty, 0.0) + self.assertEqual(data[1].ordered_qty, 57.0) + + def setup_material_request(self, order=False, receive=False): + po = None + test_records = frappe.get_test_records('Material Request') + + mr = frappe.copy_doc(test_records[0]) + mr.transaction_date = today() + mr.schedule_date = add_days(today(), 1) + for row in mr.items: + row.item_code = "Test MR Report Item" + row.item_name = "Test MR Report Item" + row.description = "Test MR Report Item" + row.uom = "Nos" + row.schedule_date = add_days(today(), 1) + mr.submit() + + if order or receive: + po = make_purchase_order(mr.name) + po.supplier = "_Test Supplier" + po.submit() + if receive: + pr = make_purchase_receipt(po.name) + pr.submit() + diff --git a/erpnext/buying/report/subcontracted_item_to_be_received/test_subcontracted_item_to_be_received.py b/erpnext/buying/report/subcontracted_item_to_be_received/test_subcontracted_item_to_be_received.py index 144523ad52..c2b38d38e1 100644 --- a/erpnext/buying/report/subcontracted_item_to_be_received/test_subcontracted_item_to_be_received.py +++ b/erpnext/buying/report/subcontracted_item_to_be_received/test_subcontracted_item_to_be_received.py @@ -3,9 +3,9 @@ # Compiled at: 2019-05-06 09:51:46 # Decompiled by https://python-decompiler.com -import unittest import frappe +from frappe.tests.utils import FrappeTestCase from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order @@ -15,7 +15,7 @@ from erpnext.buying.report.subcontracted_item_to_be_received.subcontracted_item_ from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry -class TestSubcontractedItemToBeReceived(unittest.TestCase): +class TestSubcontractedItemToBeReceived(FrappeTestCase): def test_pending_and_received_qty(self): po = create_purchase_order(item_code='_Test FG Item', is_subcontracted='Yes') diff --git a/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/test_subcontracted_raw_materials_to_be_transferred.py b/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/test_subcontracted_raw_materials_to_be_transferred.py index 3c203ac23f..fc9acabc81 100644 --- a/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/test_subcontracted_raw_materials_to_be_transferred.py +++ b/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/test_subcontracted_raw_materials_to_be_transferred.py @@ -4,9 +4,9 @@ # Decompiled by https://python-decompiler.com import json -import unittest import frappe +from frappe.tests.utils import FrappeTestCase from erpnext.buying.doctype.purchase_order.purchase_order import make_rm_stock_entry from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order @@ -16,7 +16,7 @@ from erpnext.buying.report.subcontracted_raw_materials_to_be_transferred.subcont from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry -class TestSubcontractedItemToBeTransferred(unittest.TestCase): +class TestSubcontractedItemToBeTransferred(FrappeTestCase): def test_pending_and_transferred_qty(self): po = create_purchase_order(item_code='_Test FG Item', is_subcontracted='Yes', supplier_warehouse="_Test Warehouse 1 - _TC") diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index d05787fdfb..a94af10cde 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1566,13 +1566,12 @@ def validate_taxes_and_charges(tax): tax.rate = None -def validate_account_head(tax, doc): - company = frappe.get_cached_value('Account', - tax.account_head, 'company') +def validate_account_head(idx, account, company): + account_company = frappe.get_cached_value('Account', account, 'company') - if company != doc.company: + if account_company != company: frappe.throw(_('Row {0}: Account {1} does not belong to Company {2}') - .format(tax.idx, frappe.bold(tax.account_head), frappe.bold(doc.company)), title=_('Invalid Account')) + .format(idx, frappe.bold(account), frappe.bold(company)), title=_('Invalid Account')) def validate_cost_center(tax, doc): diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index c8e5eddfea..8972c32879 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -507,13 +507,41 @@ class StockController(AccountsController): "voucher_no": self.name, "company": self.company }) - if future_sle_exists(args): + + if future_sle_exists(args) or repost_required_for_queue(self): item_based_reposting = cint(frappe.db.get_single_value("Stock Reposting Settings", "item_based_reposting")) if item_based_reposting: create_item_wise_repost_entries(voucher_type=self.doctype, voucher_no=self.name) else: create_repost_item_valuation_entry(args) +def repost_required_for_queue(doc: StockController) -> bool: + """check if stock document contains repeated item-warehouse with queue based valuation. + + if queue exists for repeated items then SLEs need to reprocessed in background again. + """ + + consuming_sles = frappe.db.get_all("Stock Ledger Entry", + filters={ + "voucher_type": doc.doctype, + "voucher_no": doc.name, + "actual_qty": ("<", 0), + "is_cancelled": 0 + }, + fields=["item_code", "warehouse", "stock_queue"] + ) + item_warehouses = [(sle.item_code, sle.warehouse) for sle in consuming_sles] + + unique_item_warehouses = set(item_warehouses) + + if len(unique_item_warehouses) == len(item_warehouses): + return False + + for sle in consuming_sles: + if sle.stock_queue != "[]": # using FIFO/LIFO valuation + return True + return False + @frappe.whitelist() def make_quality_inspections(doctype, docname, items): diff --git a/erpnext/controllers/subcontracting.py b/erpnext/controllers/subcontracting.py index 3addb91aaa..c52c688b73 100644 --- a/erpnext/controllers/subcontracting.py +++ b/erpnext/controllers/subcontracting.py @@ -363,8 +363,6 @@ class Subcontracting(): return for row in self.get(self.raw_material_table): - self.__validate_consumed_qty(row) - key = (row.rm_item_code, row.main_item_code, row.purchase_order) if not self.__transferred_items or not self.__transferred_items.get(key): return @@ -372,12 +370,6 @@ class Subcontracting(): self.__validate_batch_no(row, key) self.__validate_serial_no(row, key) - def __validate_consumed_qty(self, row): - if self.backflush_based_on != 'BOM' and flt(row.consumed_qty) == 0.0: - msg = f'Row {row.idx}: the consumed qty cannot be zero for the item {frappe.bold(row.rm_item_code)}' - - frappe.throw(_(msg),title=_('Consumed Items Qty Check')) - def __validate_batch_no(self, row, key): if row.get('batch_no') and row.get('batch_no') not in self.__transferred_items.get(key).get('batch_no'): link = get_link_to_form('Purchase Order', row.purchase_order) diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 2776628227..a1bb6670c4 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -113,17 +113,24 @@ class calculate_taxes_and_totals(object): for item in self.doc.get("items"): self.doc.round_floats_in(item) + if not item.rate: + item.rate = item.price_list_rate + if item.discount_percentage == 100: item.rate = 0.0 elif item.price_list_rate: - if not item.rate or (item.pricing_rules and item.discount_percentage > 0): + if item.pricing_rules or abs(item.discount_percentage) > 0: item.rate = flt(item.price_list_rate * (1.0 - (item.discount_percentage / 100.0)), item.precision("rate")) - item.discount_amount = item.price_list_rate * (item.discount_percentage / 100.0) - elif item.discount_amount and item.pricing_rules: + + if abs(item.discount_percentage) > 0: + item.discount_amount = item.price_list_rate * (item.discount_percentage / 100.0) + + elif item.discount_amount or item.pricing_rules: item.rate = item.price_list_rate - item.discount_amount - if item.doctype in ['Quotation Item', 'Sales Order Item', 'Delivery Note Item', 'Sales Invoice Item', 'POS Invoice Item', 'Purchase Invoice Item', 'Purchase Order Item', 'Purchase Receipt Item']: + if item.doctype in ['Quotation Item', 'Sales Order Item', 'Delivery Note Item', 'Sales Invoice Item', + 'POS Invoice Item', 'Purchase Invoice Item', 'Purchase Order Item', 'Purchase Receipt Item']: 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")) @@ -270,7 +277,8 @@ class calculate_taxes_and_totals(object): shipping_rule.apply(self.doc) def calculate_taxes(self): - if not self.doc.get('is_consolidated'): + rounding_adjustment_computed = self.doc.get('is_consolidated') and self.doc.get('rounding_adjustment') + if not rounding_adjustment_computed: self.doc.rounding_adjustment = 0 # maintain actual tax rate based on idx @@ -326,7 +334,7 @@ class calculate_taxes_and_totals(object): if i == (len(self.doc.get("taxes")) - 1) and self.discount_amount_applied \ and self.doc.discount_amount \ and self.doc.apply_discount_on == "Grand Total" \ - and not self.doc.get('is_consolidated'): + and not rounding_adjustment_computed: self.doc.rounding_adjustment = flt(self.doc.grand_total - flt(self.doc.discount_amount) - tax.total, self.doc.precision("rounding_adjustment")) @@ -465,20 +473,22 @@ class calculate_taxes_and_totals(object): self.doc.total_net_weight += d.total_weight def set_rounded_total(self): - if not self.doc.get('is_consolidated'): - if self.doc.meta.get_field("rounded_total"): - if self.doc.is_rounded_total_disabled(): - self.doc.rounded_total = self.doc.base_rounded_total = 0 - return + if self.doc.get('is_consolidated') and self.doc.get('rounding_adjustment'): + return - self.doc.rounded_total = round_based_on_smallest_currency_fraction(self.doc.grand_total, - self.doc.currency, self.doc.precision("rounded_total")) + if self.doc.meta.get_field("rounded_total"): + if self.doc.is_rounded_total_disabled(): + self.doc.rounded_total = self.doc.base_rounded_total = 0 + return - #if print_in_rate is set, we would have already calculated rounding adjustment - self.doc.rounding_adjustment += flt(self.doc.rounded_total - self.doc.grand_total, - self.doc.precision("rounding_adjustment")) + self.doc.rounded_total = round_based_on_smallest_currency_fraction(self.doc.grand_total, + self.doc.currency, self.doc.precision("rounded_total")) - self._set_in_company_currency(self.doc, ["rounding_adjustment", "rounded_total"]) + #if print_in_rate is set, we would have already calculated rounding adjustment + self.doc.rounding_adjustment += flt(self.doc.rounded_total - self.doc.grand_total, + self.doc.precision("rounding_adjustment")) + + self._set_in_company_currency(self.doc, ["rounding_adjustment", "rounded_total"]) def _cleanup(self): if not self.doc.get('is_consolidated'): diff --git a/erpnext/domains/non_profit.py b/erpnext/domains/non_profit.py deleted file mode 100644 index d9fc5e5df0..0000000000 --- a/erpnext/domains/non_profit.py +++ /dev/null @@ -1,22 +0,0 @@ -data = { - 'desktop_icons': [ - 'Non Profit', - 'Member', - 'Donor', - 'Volunteer', - 'Grant Application', - 'Accounts', - 'Buying', - 'HR', - 'ToDo' - ], - 'restricted_roles': [ - 'Non Profit Manager', - 'Non Profit Member', - 'Non Profit Portal User' - ], - 'modules': [ - 'Non Profit' - ], - 'default_portal_role': 'Non Profit Manager' -} diff --git a/erpnext/e_commerce/product_data_engine/query.py b/erpnext/e_commerce/product_data_engine/query.py index 007bf8b348..cfc3c7b357 100644 --- a/erpnext/e_commerce/product_data_engine/query.py +++ b/erpnext/e_commerce/product_data_engine/query.py @@ -264,7 +264,7 @@ class ProductQuery: customer = get_customer(silent=True) if customer: quotation = frappe.get_all("Quotation", fields=["name"], filters= - {"party_name": customer, "order_type": "Shopping Cart", "docstatus": 0}, + {"party_name": customer, "contact_email": frappe.session.user, "order_type": "Shopping Cart", "docstatus": 0}, order_by="modified desc", limit_page_length=1) if quotation: items = frappe.get_all( @@ -298,4 +298,4 @@ class ProductQuery: # slice results manually result[:self.page_length] - return result \ No newline at end of file + return result diff --git a/erpnext/e_commerce/shopping_cart/cart.py b/erpnext/e_commerce/shopping_cart/cart.py index 458cf69af7..372aed0b95 100644 --- a/erpnext/e_commerce/shopping_cart/cart.py +++ b/erpnext/e_commerce/shopping_cart/cart.py @@ -310,7 +310,7 @@ def _get_cart_quotation(party=None): party = get_party() quotation = frappe.get_all("Quotation", fields=["name"], filters= - {"party_name": party.name, "order_type": "Shopping Cart", "docstatus": 0}, + {"party_name": party.name, "contact_email": frappe.session.user, "order_type": "Shopping Cart", "docstatus": 0}, order_by="modified desc", limit_page_length=1) if quotation: diff --git a/erpnext/e_commerce/shopping_cart/test_shopping_cart.py b/erpnext/e_commerce/shopping_cart/test_shopping_cart.py index 8519e68d09..9c389d0d0b 100644 --- a/erpnext/e_commerce/shopping_cart/test_shopping_cart.py +++ b/erpnext/e_commerce/shopping_cart/test_shopping_cart.py @@ -5,6 +5,7 @@ import unittest import frappe +from frappe.tests.utils import change_settings from frappe.utils import add_months, nowdate from erpnext.accounts.doctype.tax_rule.tax_rule import ConflictingTaxRule @@ -15,7 +16,7 @@ from erpnext.e_commerce.shopping_cart.cart import ( get_party, update_cart, ) -from erpnext.tests.utils import change_settings, create_test_contact_and_address +from erpnext.tests.utils import create_test_contact_and_address # test_dependencies = ['Payment Terms Template'] @@ -57,13 +58,19 @@ class TestShoppingCart(unittest.TestCase): return quotation def test_get_cart_customer(self): - self.login_as_customer() + def validate_quotation(): + # test if quotation with customer is fetched + quotation = _get_cart_quotation() + self.assertEqual(quotation.quotation_to, "Customer") + self.assertEqual(quotation.party_name, "_Test Customer") + self.assertEqual(quotation.contact_email, frappe.session.user) + return quotation - # test if quotation with customer is fetched - quotation = _get_cart_quotation() - self.assertEqual(quotation.quotation_to, "Customer") - self.assertEqual(quotation.party_name, "_Test Customer") - self.assertEqual(quotation.contact_email, frappe.session.user) + self.login_as_customer("test_contact_two_customer@example.com", "_Test Contact 2 For _Test Customer") + validate_quotation() + + self.login_as_customer() + quotation = validate_quotation() return quotation @@ -175,7 +182,7 @@ class TestShoppingCart(unittest.TestCase): def create_tax_rule(self): tax_rule = frappe.get_test_records("Tax Rule")[0] try: - frappe.get_doc(tax_rule).insert() + frappe.get_doc(tax_rule).insert(ignore_if_duplicate=True) except (frappe.DuplicateEntryError, ConflictingTaxRule): pass @@ -254,10 +261,9 @@ class TestShoppingCart(unittest.TestCase): self.create_user_if_not_exists("test_cart_user@example.com") frappe.set_user("test_cart_user@example.com") - def login_as_customer(self): - self.create_user_if_not_exists("test_contact_customer@example.com", - "_Test Contact For _Test Customer") - frappe.set_user("test_contact_customer@example.com") + def login_as_customer(self, email="test_contact_customer@example.com", name="_Test Contact For _Test Customer"): + self.create_user_if_not_exists(email, name) + frappe.set_user(email) def clear_existing_quotations(self): quotations = frappe.get_all("Quotation", filters={ diff --git a/erpnext/e_commerce/variant_selector/test_variant_selector.py b/erpnext/e_commerce/variant_selector/test_variant_selector.py index 4d907c6221..ee098e16e7 100644 --- a/erpnext/e_commerce/variant_selector/test_variant_selector.py +++ b/erpnext/e_commerce/variant_selector/test_variant_selector.py @@ -1,4 +1,5 @@ import frappe +from frappe.tests.utils import FrappeTestCase from erpnext.controllers.item_variant import create_variant from erpnext.e_commerce.doctype.e_commerce_settings.test_e_commerce_settings import ( @@ -7,11 +8,10 @@ from erpnext.e_commerce.doctype.e_commerce_settings.test_e_commerce_settings imp from erpnext.e_commerce.doctype.website_item.website_item import make_website_item from erpnext.e_commerce.variant_selector.utils import get_next_attribute_and_values from erpnext.stock.doctype.item.test_item import make_item -from erpnext.tests.utils import ERPNextTestCase test_dependencies = ["Item"] -class TestVariantSelector(ERPNextTestCase): +class TestVariantSelector(FrappeTestCase): @classmethod def setUpClass(cls): @@ -116,4 +116,4 @@ class TestVariantSelector(ERPNextTestCase): self.assertEqual(next_values["exact_match"][0],"Test-Tshirt-Temp-S-R") self.assertEqual(next_values["exact_match"][0],"Test-Tshirt-Temp-S-R") self.assertEqual(price_info["price_list_rate"], 100.0) - self.assertEqual(price_info["formatted_price_sales_uom"], "₹ 100.00") \ No newline at end of file + self.assertEqual(price_info["formatted_price_sales_uom"], "₹ 100.00") diff --git a/erpnext/education/doctype/student_attendance_tool/student_attendance_tool.js b/erpnext/education/doctype/student_attendance_tool/student_attendance_tool.js index 68e7780039..4526585175 100644 --- a/erpnext/education/doctype/student_attendance_tool/student_attendance_tool.js +++ b/erpnext/education/doctype/student_attendance_tool/student_attendance_tool.js @@ -3,6 +3,10 @@ frappe.provide("education"); frappe.ui.form.on('Student Attendance Tool', { + setup: (frm) => { + frm.students_area = $('
{{ introduction }}
-{% if meetup_embed_html %} - {{ meetup_embed_html }} -{% endif %} -
-
-
-
-
-
-
-
-
- {{ index|length }}. {{ frappe.db.get_value('User', user.user, 'full_name') }}
-
- {% if user.website_url %}
- {{ user.website_url | truncate (50) or '' }}
- {% endif %}
-
-
- -
- {% if user.introduction %}
- {{ user.introduction }}
- {% endif %}
-
- |
-
No member yet.
-{% endif %} - -Name | -{{ doc.member_name }} | -
{{ frappe.db.get_value('User', doc.email, 'email') or '' }} | -|
Phone | -{{ frappe.db.get_value('User', doc.email, 'phone') or '' }} | -
{{ address or ''}}
-Please Review this grant application
Organization/Indvidual | -{{ applicant_type }} | -
Grant Applicant Name | -{{ applicant_name}} | -
Date | -{{ frappe.format_date(creation) }} | -
Status | -{{ status }} | -
{{ email }} | -
{{ grant_description }}
-{{ amount }}
-{{ has_any_past_grant_record }}
-Contact Person | -{{ contact_person }} | -
{{ email }} | -
You must register and login to view contact details
- {% endif %} -Assessment Review done
- {% endif %} - {% else %} - - {% endif %} -{% endblock %} -{% block style %} - - -{% endblock %} diff --git a/erpnext/non_profit/doctype/grant_application/templates/grant_application_row.html b/erpnext/non_profit/doctype/grant_application/templates/grant_application_row.html deleted file mode 100644 index e375b16154..0000000000 --- a/erpnext/non_profit/doctype/grant_application/templates/grant_application_row.html +++ /dev/null @@ -1,11 +0,0 @@ -{% if doc.published %} - -{% endif %} diff --git a/erpnext/non_profit/doctype/grant_application/test_grant_application.py b/erpnext/non_profit/doctype/grant_application/test_grant_application.py deleted file mode 100644 index ef267d7af8..0000000000 --- a/erpnext/non_profit/doctype/grant_application/test_grant_application.py +++ /dev/null @@ -1,8 +0,0 @@ -# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt - -import unittest - - -class TestGrantApplication(unittest.TestCase): - pass diff --git a/erpnext/non_profit/doctype/member/__init__.py b/erpnext/non_profit/doctype/member/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/non_profit/doctype/member/member.js b/erpnext/non_profit/doctype/member/member.js deleted file mode 100644 index e58ec0f5ee..0000000000 --- a/erpnext/non_profit/doctype/member/member.js +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -frappe.ui.form.on('Member', { - setup: function(frm) { - frappe.db.get_single_value('Non Profit Settings', 'enable_razorpay_for_memberships').then(val => { - if (val && (frm.doc.subscription_id || frm.doc.customer_id)) { - frm.set_df_property('razorpay_details_section', 'hidden', false); - } - }) - }, - - refresh: function(frm) { - - frappe.dynamic_link = {doc: frm.doc, fieldname: 'name', doctype: 'Member'}; - - frm.toggle_display(['address_html','contact_html'], !frm.doc.__islocal); - - if(!frm.doc.__islocal) { - frappe.contacts.render_address_and_contact(frm); - - // custom buttons - frm.add_custom_button(__('Accounting Ledger'), function() { - frappe.set_route('query-report', 'General Ledger', - {party_type:'Member', party:frm.doc.name}); - }); - - frm.add_custom_button(__('Accounts Receivable'), function() { - frappe.set_route('query-report', 'Accounts Receivable', {member:frm.doc.name}); - }); - - if (!frm.doc.customer) { - frm.add_custom_button(__('Create Customer'), () => { - frm.call('make_customer_and_link').then(() => { - frm.reload_doc(); - }); - }); - } - - // indicator - erpnext.utils.set_party_dashboard_indicators(frm); - - } else { - frappe.contacts.clear_address_and_contact(frm); - } - - frappe.call({ - method:"frappe.client.get_value", - args:{ - 'doctype':"Membership", - 'filters':{'member': frm.doc.name}, - 'fieldname':[ - 'to_date' - ] - }, - callback: function (data) { - if(data.message) { - frappe.model.set_value(frm.doctype,frm.docname, - "membership_expiry_date", data.message.to_date); - } - } - }); - } -}); diff --git a/erpnext/non_profit/doctype/member/member.json b/erpnext/non_profit/doctype/member/member.json deleted file mode 100644 index 7c1baf1a8d..0000000000 --- a/erpnext/non_profit/doctype/member/member.json +++ /dev/null @@ -1,210 +0,0 @@ -{ - "actions": [], - "allow_rename": 1, - "autoname": "naming_series:", - "creation": "2017-09-11 09:24:52.898356", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "naming_series", - "member_name", - "membership_expiry_date", - "column_break_5", - "membership_type", - "email_id", - "image", - "customer_section", - "customer", - "customer_name", - "supplier_section", - "supplier", - "address_contacts", - "address_html", - "column_break_9", - "contact_html", - "razorpay_details_section", - "subscription_id", - "customer_id", - "subscription_status", - "column_break_21", - "subscription_start", - "subscription_end" - ], - "fields": [ - { - "fieldname": "naming_series", - "fieldtype": "Select", - "label": "Series", - "options": "NPO-MEM-.YYYY.-", - "reqd": 1 - }, - { - "fieldname": "member_name", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Member Name", - "reqd": 1 - }, - { - "fieldname": "membership_expiry_date", - "fieldtype": "Date", - "label": "Membership Expiry Date" - }, - { - "fieldname": "column_break_5", - "fieldtype": "Column Break" - }, - { - "fieldname": "membership_type", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Membership Type", - "options": "Membership Type", - "reqd": 1 - }, - { - "fieldname": "image", - "fieldtype": "Attach Image", - "hidden": 1, - "label": "Image", - "no_copy": 1, - "print_hide": 1 - }, - { - "collapsible": 1, - "fieldname": "customer_section", - "fieldtype": "Section Break", - "label": "Customer" - }, - { - "fieldname": "customer", - "fieldtype": "Link", - "label": "Customer", - "options": "Customer" - }, - { - "fetch_from": "customer.customer_name", - "fieldname": "customer_name", - "fieldtype": "Data", - "label": "Customer Name", - "read_only": 1 - }, - { - "collapsible": 1, - "fieldname": "supplier_section", - "fieldtype": "Section Break", - "label": "Supplier" - }, - { - "fieldname": "supplier", - "fieldtype": "Link", - "label": "Supplier", - "options": "Supplier" - }, - { - "depends_on": "eval:!doc.__islocal;", - "fieldname": "address_contacts", - "fieldtype": "Section Break", - "label": "Address and Contact", - "options": "fa fa-map-marker" - }, - { - "fieldname": "address_html", - "fieldtype": "HTML", - "label": "Address HTML" - }, - { - "fieldname": "column_break_9", - "fieldtype": "Column Break" - }, - { - "fieldname": "contact_html", - "fieldtype": "HTML", - "label": "Contact HTML" - }, - { - "fieldname": "email_id", - "fieldtype": "Data", - "label": "Email Address", - "options": "Email" - }, - { - "fieldname": "subscription_id", - "fieldtype": "Data", - "label": "Subscription ID", - "read_only": 1 - }, - { - "fieldname": "customer_id", - "fieldtype": "Data", - "label": "Customer ID", - "read_only": 1 - }, - { - "fieldname": "razorpay_details_section", - "fieldtype": "Section Break", - "hidden": 1, - "label": "Razorpay Details" - }, - { - "fieldname": "column_break_21", - "fieldtype": "Column Break" - }, - { - "fieldname": "subscription_start", - "fieldtype": "Date", - "label": "Subscription Start " - }, - { - "fieldname": "subscription_end", - "fieldtype": "Date", - "label": "Subscription End" - }, - { - "fieldname": "subscription_status", - "fieldtype": "Select", - "label": "Subscription Status", - "options": "\nActive\nHalted" - } - ], - "image_field": "image", - "links": [], - "modified": "2021-07-11 14:27:26.368039", - "modified_by": "Administrator", - "module": "Non Profit", - "name": "Member", - "owner": "Administrator", - "permissions": [ - { - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "Non Profit Manager", - "share": 1, - "write": 1 - }, - { - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "Non Profit Member", - "share": 1, - "write": 1 - } - ], - "quick_entry": 1, - "restrict_to_domain": "Non Profit", - "sort_field": "modified", - "sort_order": "DESC", - "title_field": "member_name", - "track_changes": 1 -} \ No newline at end of file diff --git a/erpnext/non_profit/doctype/member/member.py b/erpnext/non_profit/doctype/member/member.py deleted file mode 100644 index 4d80e57ecc..0000000000 --- a/erpnext/non_profit/doctype/member/member.py +++ /dev/null @@ -1,185 +0,0 @@ -# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - - -import frappe -from frappe import _ -from frappe.contacts.address_and_contact import load_address_and_contact -from frappe.integrations.utils import get_payment_gateway_controller -from frappe.model.document import Document -from frappe.utils import cint, get_link_to_form - -from erpnext.non_profit.doctype.membership_type.membership_type import get_membership_type - - -class Member(Document): - def onload(self): - """Load address and contacts in `__onload`""" - load_address_and_contact(self) - - - def validate(self): - if self.email_id: - self.validate_email_type(self.email_id) - - def validate_email_type(self, email): - from frappe.utils import validate_email_address - validate_email_address(email.strip(), True) - - def setup_subscription(self): - non_profit_settings = frappe.get_doc('Non Profit Settings') - if not non_profit_settings.enable_razorpay_for_memberships: - frappe.throw(_('Please check Enable Razorpay for Memberships in {0} to setup subscription')).format( - get_link_to_form('Non Profit Settings', 'Non Profit Settings')) - - controller = get_payment_gateway_controller("Razorpay") - settings = controller.get_settings({}) - - plan_id = frappe.get_value("Membership Type", self.membership_type, "razorpay_plan_id") - - if not plan_id: - frappe.throw(_("Please setup Razorpay Plan ID")) - - subscription_details = { - "plan_id": plan_id, - "billing_frequency": cint(non_profit_settings.billing_frequency), - "customer_notify": 1 - } - - args = { - 'subscription_details': subscription_details - } - - subscription = controller.setup_subscription(settings, **args) - - return subscription - - @frappe.whitelist() - def make_customer_and_link(self): - if self.customer: - frappe.msgprint(_("A customer is already linked to this Member")) - - customer = create_customer(frappe._dict({ - 'fullname': self.member_name, - 'email': self.email_id, - 'phone': None - })) - - self.customer = customer - self.save() - frappe.msgprint(_("Customer {0} has been created succesfully.").format(self.customer)) - - -def get_or_create_member(user_details): - member_list = frappe.get_all("Member", filters={'email': user_details.email, 'membership_type': user_details.plan_id}) - if member_list and member_list[0]: - return member_list[0]['name'] - else: - return create_member(user_details) - -def create_member(user_details): - user_details = frappe._dict(user_details) - member = frappe.new_doc("Member") - member.update({ - "member_name": user_details.fullname, - "email_id": user_details.email, - "pan_number": user_details.pan or None, - "membership_type": user_details.plan_id, - "customer_id": user_details.customer_id or None, - "subscription_id": user_details.subscription_id or None, - "subscription_status": user_details.subscription_status or "" - }) - - member.insert(ignore_permissions=True) - member.customer = create_customer(user_details, member.name) - member.save(ignore_permissions=True) - - return member - -def create_customer(user_details, member=None): - customer = frappe.new_doc("Customer") - customer.customer_name = user_details.fullname - customer.customer_type = "Individual" - customer.flags.ignore_mandatory = True - customer.insert(ignore_permissions=True) - - try: - contact = frappe.new_doc("Contact") - contact.first_name = user_details.fullname - if user_details.mobile: - contact.add_phone(user_details.mobile, is_primary_phone=1, is_primary_mobile_no=1) - if user_details.email: - contact.add_email(user_details.email, is_primary=1) - contact.insert(ignore_permissions=True) - - contact.append("links", { - "link_doctype": "Customer", - "link_name": customer.name - }) - - if member: - contact.append("links", { - "link_doctype": "Member", - "link_name": member - }) - - contact.save(ignore_permissions=True) - - except frappe.DuplicateEntryError: - return customer.name - - except Exception as e: - frappe.log_error(frappe.get_traceback(), _("Contact Creation Failed")) - pass - - return customer.name - -@frappe.whitelist(allow_guest=True) -def create_member_subscription_order(user_details): - """Create Member subscription and order for payment - - Args: - user_details (TYPE): Description - - Returns: - Dictionary: Dictionary with subscription details - { - 'subscription_details': { - 'plan_id': 'plan_EXwyxDYDCj3X4v', - 'billing_frequency': 24, - 'customer_notify': 1 - }, - 'subscription_id': 'sub_EZycCvXFvqnC6p' - } - """ - - user_details = frappe._dict(user_details) - member = get_or_create_member(user_details) - - subscription = member.setup_subscription() - - member.subscription_id = subscription.get('subscription_id') - member.save(ignore_permissions=True) - - return subscription - -@frappe.whitelist() -def register_member(fullname, email, rzpay_plan_id, subscription_id, pan=None, mobile=None): - plan = get_membership_type(rzpay_plan_id) - if not plan: - raise frappe.DoesNotExistError - - member = frappe.db.exists("Member", {'email': email, 'subscription_id': subscription_id }) - if member: - return member - else: - member = create_member(dict( - fullname=fullname, - email=email, - plan_id=plan, - subscription_id=subscription_id, - pan=pan, - mobile=mobile - )) - - return member.name diff --git a/erpnext/non_profit/doctype/member/member_dashboard.py b/erpnext/non_profit/doctype/member/member_dashboard.py deleted file mode 100644 index 0e31e3ceb8..0000000000 --- a/erpnext/non_profit/doctype/member/member_dashboard.py +++ /dev/null @@ -1,22 +0,0 @@ -from frappe import _ - - -def get_data(): - return { - 'heatmap': True, - 'heatmap_message': _('Member Activity'), - 'fieldname': 'member', - 'non_standard_fieldnames': { - 'Bank Account': 'party' - }, - 'transactions': [ - { - 'label': _('Membership Details'), - 'items': ['Membership'] - }, - { - 'label': _('Fee'), - 'items': ['Bank Account'] - } - ] - } diff --git a/erpnext/non_profit/doctype/member/member_list.js b/erpnext/non_profit/doctype/member/member_list.js deleted file mode 100644 index 8e41e7fdde..0000000000 --- a/erpnext/non_profit/doctype/member/member_list.js +++ /dev/null @@ -1,3 +0,0 @@ -frappe.listview_settings['Member'] = { - add_fields: ["member_name", "membership_type", "image"], -}; diff --git a/erpnext/non_profit/doctype/member/test_member.py b/erpnext/non_profit/doctype/member/test_member.py deleted file mode 100644 index 46f14ed131..0000000000 --- a/erpnext/non_profit/doctype/member/test_member.py +++ /dev/null @@ -1,8 +0,0 @@ -# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt - -import unittest - - -class TestMember(unittest.TestCase): - pass diff --git a/erpnext/non_profit/doctype/membership/__init__.py b/erpnext/non_profit/doctype/membership/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/non_profit/doctype/membership/membership.js b/erpnext/non_profit/doctype/membership/membership.js deleted file mode 100644 index 31872048a0..0000000000 --- a/erpnext/non_profit/doctype/membership/membership.js +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -frappe.ui.form.on('Membership', { - setup: function(frm) { - frappe.db.get_single_value("Non Profit Settings", "enable_razorpay_for_memberships").then(val => { - if (val) frm.set_df_property("razorpay_details_section", "hidden", false); - }) - }, - - refresh: function(frm) { - if (frm.doc.__islocal) - return; - - !frm.doc.invoice && frm.add_custom_button("Generate Invoice", () => { - frm.call({ - doc: frm.doc, - method: "generate_invoice", - args: {save: true}, - freeze: true, - freeze_message: __("Creating Membership Invoice"), - callback: function(r) { - if (r.invoice) - frm.reload_doc(); - } - }); - }); - - frappe.db.get_single_value("Non Profit Settings", "send_email").then(val => { - if (val) frm.add_custom_button("Send Acknowledgement", () => { - frm.call("send_acknowlement").then(() => { - frm.reload_doc(); - }); - }); - }) - }, - - onload: function(frm) { - frm.add_fetch("membership_type", "amount", "amount"); - } -}); diff --git a/erpnext/non_profit/doctype/membership/membership.json b/erpnext/non_profit/doctype/membership/membership.json deleted file mode 100644 index 11d32f9c2b..0000000000 --- a/erpnext/non_profit/doctype/membership/membership.json +++ /dev/null @@ -1,184 +0,0 @@ -{ - "actions": [], - "autoname": "NPO-MSH-.YYYY.-.#####", - "creation": "2017-09-11 11:39:18.492184", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "member", - "member_name", - "membership_type", - "column_break_3", - "company", - "membership_status", - "membership_validity_section", - "from_date", - "to_date", - "column_break_8", - "member_since_date", - "payment_details", - "paid", - "currency", - "amount", - "invoice", - "razorpay_details_section", - "subscription_id", - "payment_id" - ], - "fields": [ - { - "fieldname": "member", - "fieldtype": "Link", - "label": "Member", - "options": "Member" - }, - { - "fieldname": "membership_type", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Membership Type", - "options": "Membership Type", - "reqd": 1 - }, - { - "fieldname": "column_break_3", - "fieldtype": "Column Break" - }, - { - "fieldname": "membership_status", - "fieldtype": "Select", - "in_list_view": 1, - "in_standard_filter": 1, - "label": "Membership Status", - "options": "New\nCurrent\nExpired\nPending\nCancelled" - }, - { - "fieldname": "membership_validity_section", - "fieldtype": "Section Break", - "label": "Validity" - }, - { - "fieldname": "from_date", - "fieldtype": "Date", - "in_list_view": 1, - "label": "From", - "reqd": 1 - }, - { - "fieldname": "to_date", - "fieldtype": "Date", - "in_list_view": 1, - "label": "To", - "reqd": 1 - }, - { - "fieldname": "column_break_8", - "fieldtype": "Column Break" - }, - { - "fieldname": "member_since_date", - "fieldtype": "Date", - "label": "Member Since" - }, - { - "fieldname": "payment_details", - "fieldtype": "Section Break", - "label": "Payment Details" - }, - { - "default": "0", - "fieldname": "paid", - "fieldtype": "Check", - "label": "Paid" - }, - { - "fieldname": "currency", - "fieldtype": "Link", - "label": "Currency", - "options": "Currency" - }, - { - "fieldname": "amount", - "fieldtype": "Float", - "label": "Amount" - }, - { - "fieldname": "razorpay_details_section", - "fieldtype": "Section Break", - "hidden": 1, - "label": "Razorpay Details" - }, - { - "fieldname": "subscription_id", - "fieldtype": "Data", - "label": "Subscription ID", - "read_only": 1 - }, - { - "fieldname": "payment_id", - "fieldtype": "Data", - "label": "Payment ID", - "read_only": 1 - }, - { - "fieldname": "invoice", - "fieldtype": "Link", - "label": "Invoice", - "options": "Sales Invoice" - }, - { - "fetch_from": "member.member_name", - "fieldname": "member_name", - "fieldtype": "Data", - "label": "Member Name", - "read_only": 1 - }, - { - "fieldname": "company", - "fieldtype": "Link", - "label": "Company", - "options": "Company", - "reqd": 1 - } - ], - "index_web_pages_for_search": 1, - "links": [], - "modified": "2021-02-19 14:33:44.925122", - "modified_by": "Administrator", - "module": "Non Profit", - "name": "Membership", - "owner": "Administrator", - "permissions": [ - { - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "Non Profit Manager", - "share": 1, - "write": 1 - }, - { - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "Non Profit Member", - "share": 1, - "write": 1 - } - ], - "restrict_to_domain": "Non Profit", - "search_fields": "member, member_name", - "sort_field": "modified", - "sort_order": "DESC", - "title_field": "member_name", - "track_changes": 1 -} \ No newline at end of file diff --git a/erpnext/non_profit/doctype/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py deleted file mode 100644 index f9b295a223..0000000000 --- a/erpnext/non_profit/doctype/membership/membership.py +++ /dev/null @@ -1,415 +0,0 @@ -# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - - -import json -from datetime import datetime - -import frappe -from frappe import _ -from frappe.email import sendmail_to_system_managers -from frappe.model.document import Document -from frappe.utils import add_days, add_months, add_years, get_link_to_form, getdate, nowdate - -import erpnext -from erpnext.non_profit.doctype.member.member import create_member - - -class Membership(Document): - def validate(self): - if not self.member or not frappe.db.exists("Member", self.member): - # for web forms - user_type = frappe.db.get_value("User", frappe.session.user, "user_type") - if user_type == "Website User": - self.create_member_from_website_user() - else: - frappe.throw(_("Please select a Member")) - - self.validate_membership_period() - - def create_member_from_website_user(self): - member_name = frappe.get_value("Member", dict(email_id=frappe.session.user)) - - if not member_name: - user = frappe.get_doc("User", frappe.session.user) - member = frappe.get_doc(dict( - doctype="Member", - email_id=frappe.session.user, - membership_type=self.membership_type, - member_name=user.get_fullname() - )).insert(ignore_permissions=True) - member_name = member.name - - if self.get("__islocal"): - self.member = member_name - - def validate_membership_period(self): - # get last membership (if active) - last_membership = erpnext.get_last_membership(self.member) - - # if person applied for offline membership - 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")) - - self.from_date = add_days(last_membership.to_date, 1) - elif frappe.session.user == "Administrator": - self.from_date = self.from_date - else: - self.from_date = nowdate() - - if frappe.db.get_single_value("Non Profit Settings", "billing_cycle") == "Yearly": - self.to_date = add_years(self.from_date, 1) - else: - self.to_date = add_months(self.from_date, 1) - - def on_payment_authorized(self, status_changed_to=None): - if status_changed_to not in ("Completed", "Authorized"): - return - self.load_from_db() - self.db_set("paid", 1) - settings = frappe.get_doc("Non Profit Settings") - if settings.allow_invoicing and settings.automate_membership_invoicing: - 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")) - - if self.invoice: - frappe.throw(_("An invoice is already linked to this document")) - - member = frappe.get_doc("Member", self.member) - if not member.customer: - frappe.throw(_("No customer linked to member {0}").format(frappe.bold(self.member))) - - plan = frappe.get_doc("Membership Type", self.membership_type) - settings = frappe.get_doc("Non Profit Settings") - 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: - self.make_payment_entry(settings, invoice) - - if save: - self.save() - - return invoice - - def validate_membership_type_and_settings(self, plan, settings): - settings_link = get_link_to_form("Membership Type", self.membership_type) - - if not settings.membership_debit_account: - frappe.throw(_("You need to set Debit Account in {0}").format(settings_link)) - - if not settings.company: - frappe.throw(_("You need to set Default Company for invoicing in {0}").format(settings_link)) - - if not plan.linked_item: - frappe.throw(_("Please set a Linked Item for the Membership Type {0}").format( - get_link_to_form("Membership Type", self.membership_type))) - - def make_payment_entry(self, settings, invoice): - if not settings.membership_payment_account: - frappe.throw(_("You need to set Payment Account for Membership in {0}").format( - get_link_to_form("Non Profit Settings", "Non Profit Settings"))) - - from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry - frappe.flags.ignore_account_permission = True - pe = get_payment_entry(dt="Sales Invoice", dn=invoice.name, bank_amount=invoice.grand_total) - frappe.flags.ignore_account_permission=False - pe.paid_to = settings.membership_payment_account - pe.reference_no = self.name - pe.reference_date = getdate() - pe.flags.ignore_mandatory = True - pe.save() - pe.submit() - - @frappe.whitelist() - def send_acknowlement(self): - settings = frappe.get_doc("Non Profit Settings") - if not settings.send_email: - frappe.throw(_("You need to enable Send Acknowledge Email in {0}").format( - get_link_to_form("Non Profit Settings", "Non Profit Settings"))) - - member = frappe.get_doc("Member", self.member) - if not member.email_id: - frappe.throw(_("Email address of member {0} is missing").format(frappe.utils.get_link_to_form("Member", self.member))) - - plan = frappe.get_doc("Membership Type", self.membership_type) - email = member.email_id - attachments = [frappe.attach_print("Membership", self.name, print_format=settings.membership_print_format)] - - if self.invoice and settings.send_invoice: - attachments.append(frappe.attach_print("Sales Invoice", self.invoice, print_format=settings.inv_print_format)) - - email_template = frappe.get_doc("Email Template", settings.email_template) - context = { "doc": self, "member": member} - - email_args = { - "recipients": [email], - "message": frappe.render_template(email_template.get("response"), context), - "subject": frappe.render_template(email_template.get("subject"), context), - "attachments": attachments, - "reference_doctype": self.doctype, - "reference_name": self.name - } - - if not frappe.flags.in_test: - frappe.enqueue(method=frappe.sendmail, queue="short", timeout=300, is_async=True, **email_args) - else: - frappe.sendmail(**email_args) - - def generate_and_send_invoice(self): - self.generate_invoice(save=False) - self.send_acknowlement() - - -def make_invoice(membership, member, plan, settings): - invoice = frappe.get_doc({ - "doctype": "Sales Invoice", - "customer": member.customer, - "debit_to": settings.membership_debit_account, - "currency": membership.currency, - "company": settings.company, - "is_pos": 0, - "items": [ - { - "item_code": plan.linked_item, - "rate": membership.amount, - "qty": 1 - } - ] - }) - invoice.set_missing_values() - invoice.insert() - invoice.submit() - - frappe.msgprint(_("Sales Invoice created successfully")) - - return invoice - - -def get_member_based_on_subscription(subscription_id, email=None, customer_id=None): - filters = {"subscription_id": subscription_id} - if email: - filters.update({"email_id": email}) - if customer_id: - filters.update({"customer_id": customer_id}) - - members = frappe.get_all("Member", filters=filters, order_by="creation desc") - - try: - return frappe.get_doc("Member", members[0]["name"]) - except Exception: - return None - - -def verify_signature(data, endpoint="Membership"): - signature = frappe.request.headers.get("X-Razorpay-Signature") - - settings = frappe.get_doc("Non Profit Settings") - key = settings.get_webhook_secret(endpoint) - - controller = frappe.get_doc("Razorpay Settings") - - controller.verify_signature(data, signature, key) - frappe.set_user(settings.creation_user) - - -@frappe.whitelist(allow_guest=True) -def trigger_razorpay_subscription(*args, **kwargs): - data = frappe.request.get_data(as_text=True) - data = process_request_data(data) - - subscription = data.payload.get("subscription", {}).get("entity", {}) - subscription = frappe._dict(subscription) - - payment = data.payload.get("payment", {}).get("entity", {}) - payment = frappe._dict(payment) - - try: - if not data.event == "subscription.charged": - return - - member = get_member_based_on_subscription(subscription.id, payment.email) - if not member: - member = create_member(frappe._dict({ - "fullname": payment.email, - "email": payment.email, - "plan_id": get_plan_from_razorpay_id(subscription.plan_id) - })) - - member.subscription_id = subscription.id - member.customer_id = payment.customer_id - - if subscription.get("notes"): - member = get_additional_notes(member, subscription) - - company = get_company_for_memberships() - # Update Membership - membership = frappe.new_doc("Membership") - membership.update({ - "company": company, - "member": member.name, - "membership_status": "Current", - "membership_type": member.membership_type, - "currency": "INR", - "paid": 1, - "payment_id": payment.id, - "from_date": datetime.fromtimestamp(subscription.current_start), - "to_date": datetime.fromtimestamp(subscription.current_end), - "amount": payment.amount / 100 # Convert to rupees from paise - }) - membership.flags.ignore_mandatory = True - membership.insert() - - # Update membership values - member.subscription_start = datetime.fromtimestamp(subscription.start_at) - member.subscription_end = datetime.fromtimestamp(subscription.end_at) - member.subscription_status = "Active" - member.flags.ignore_mandatory = True - member.save() - - 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: - message = "{0}\n\n{1}\n\n{2}: {3}".format(e, frappe.get_traceback(), _("Payment ID"), payment.id) - log = frappe.log_error(message, _("Error creating membership entry for {0}").format(member.name)) - notify_failure(log) - return {"status": "Failed", "reason": e} - - return {"status": "Success"} - - -@frappe.whitelist(allow_guest=True) -def update_halted_razorpay_subscription(*args, **kwargs): - """ - When all retries have been exhausted, Razorpay moves the subscription to the halted state. - The customer has to manually retry the charge or change the card linked to the subscription, - for the subscription to move back to the active state. - """ - if frappe.request: - data = frappe.request.get_data(as_text=True) - data = process_request_data(data) - elif frappe.flags.in_test: - data = kwargs.get("data") - data = frappe._dict(data) - else: - return - - if not data.event == "subscription.halted": - return - - subscription = data.payload.get("subscription", {}).get("entity", {}) - subscription = frappe._dict(subscription) - - try: - member = get_member_based_on_subscription(subscription.id, customer_id=subscription.customer_id) - if not member: - frappe.throw(_("Member with Razorpay Subscription ID {0} not found").format(subscription.id)) - - member.subscription_status = "Halted" - member.flags.ignore_mandatory = True - member.save() - - if subscription.get("notes"): - member = get_additional_notes(member, subscription) - - except Exception as e: - message = "{0}\n\n{1}".format(e, frappe.get_traceback()) - log = frappe.log_error(message, _("Error updating halted status for member {0}").format(member.name)) - notify_failure(log) - return {"status": "Failed", "reason": e} - - return {"status": "Success"} - - -def process_request_data(data): - try: - verify_signature(data) - except Exception as e: - log = frappe.log_error(e, "Membership Webhook Verification Error") - notify_failure(log) - return {"status": "Failed", "reason": e} - - if isinstance(data, str): - data = json.loads(data) - data = frappe._dict(data) - - return data - - -def get_company_for_memberships(): - company = frappe.db.get_single_value("Non Profit Settings", "company") - if not company: - from erpnext.non_profit.utils import get_company - company = get_company() - return company - - -def get_additional_notes(member, subscription): - if type(subscription.notes) == dict: - for k, v in subscription.notes.items(): - notes = "\n".join("{}: {}".format(k, v)) - - # extract member name from notes - if "name" in k.lower(): - member.update({ - "member_name": subscription.notes.get(k) - }) - - # extract pan number from notes - if "pan" in k.lower(): - member.update({ - "pan_number": subscription.notes.get(k) - }) - - member.add_comment("Comment", notes) - - elif type(subscription.notes) == str: - member.add_comment("Comment", subscription.notes) - - return member - - -def notify_failure(log): - try: - content = """ - Dear System Manager, - Razorpay webhook for creating renewing membership subscription failed due to some reason. - Please check the following error log linked below - Error Log: {0} - Regards, Administrator - """.format(get_link_to_form("Error Log", log.name)) - - sendmail_to_system_managers("[Important] [ERPNext] Razorpay membership webhook failed , please check.", content) - except Exception: - pass - - -def get_plan_from_razorpay_id(plan_id): - plan = frappe.get_all("Membership Type", filters={"razorpay_plan_id": plan_id}, order_by="creation desc") - - try: - return plan[0]["name"] - except Exception: - return None - - -def set_expired_status(): - frappe.db.sql(""" - UPDATE - `tabMembership` SET `membership_status` = 'Expired' - WHERE - `membership_status` not in ('Cancelled') AND `to_date` < %s - """, (nowdate())) diff --git a/erpnext/non_profit/doctype/membership/membership_list.js b/erpnext/non_profit/doctype/membership/membership_list.js deleted file mode 100644 index a959159899..0000000000 --- a/erpnext/non_profit/doctype/membership/membership_list.js +++ /dev/null @@ -1,15 +0,0 @@ -frappe.listview_settings['Membership'] = { - get_indicator: function(doc) { - if (doc.membership_status == 'New') { - return [__('New'), 'blue', 'membership_status,=,New']; - } else if (doc.membership_status === 'Current') { - return [__('Current'), 'green', 'membership_status,=,Current']; - } else if (doc.membership_status === 'Pending') { - return [__('Pending'), 'yellow', 'membership_status,=,Pending']; - } else if (doc.membership_status === 'Expired') { - return [__('Expired'), 'grey', 'membership_status,=,Expired']; - } else { - return [__('Cancelled'), 'red', 'membership_status,=,Cancelled']; - } - } -}; diff --git a/erpnext/non_profit/doctype/membership/test_membership.py b/erpnext/non_profit/doctype/membership/test_membership.py deleted file mode 100644 index fbe344c6a1..0000000000 --- a/erpnext/non_profit/doctype/membership/test_membership.py +++ /dev/null @@ -1,164 +0,0 @@ -# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt - -import unittest - -import frappe -from frappe.utils import add_months, nowdate - -import erpnext -from erpnext.non_profit.doctype.member.member import create_member -from erpnext.non_profit.doctype.membership.membership import update_halted_razorpay_subscription - - -class TestMembership(unittest.TestCase): - def setUp(self): - plan = setup_membership() - - # make test member - self.member_doc = create_member( - frappe._dict({ - "fullname": "_Test_Member", - "email": "_test_member_erpnext@example.com", - "plan_id": plan.name, - "subscription_id": "sub_DEX6xcJ1HSW4CR", - "customer_id": "cust_C0WlbKhp3aLA7W", - "subscription_status": "Active" - }) - ) - self.member_doc.make_customer_and_link() - self.member = self.member_doc.name - - def test_auto_generate_invoice_and_payment_entry(self): - entry = make_membership(self.member) - - # Naive test to see if at all invoice was generated and attached to member - # In any case if details were missing, the invoicing would throw an error - invoice = entry.generate_invoice(save=True) - self.assertEqual(invoice.name, entry.invoice) - - def test_renew_within_30_days(self): - # create a membership for two months - # Should work fine - make_membership(self.member, { "from_date": nowdate() }) - make_membership(self.member, { "from_date": add_months(nowdate(), 1) }) - - from frappe.utils.user import add_role - add_role("test@example.com", "Non Profit Manager") - frappe.set_user("test@example.com") - - # create next membership with expiry not within 30 days - self.assertRaises(frappe.ValidationError, make_membership, self.member, { - "from_date": add_months(nowdate(), 2), - }) - - frappe.set_user("Administrator") - # create the same membership but as administrator - make_membership(self.member, { - "from_date": add_months(nowdate(), 2), - "to_date": add_months(nowdate(), 3), - }) - - def test_halted_memberships(self): - make_membership(self.member, { - "from_date": add_months(nowdate(), 2), - "to_date": add_months(nowdate(), 3) - }) - - self.assertEqual(frappe.db.get_value("Member", self.member, "subscription_status"), "Active") - payload = get_subscription_payload() - update_halted_razorpay_subscription(data=payload) - self.assertEqual(frappe.db.get_value("Member", self.member, "subscription_status"), "Halted") - - def tearDown(self): - frappe.db.rollback() - -def set_config(key, value): - frappe.db.set_value("Non Profit Settings", None, key, value) - -def make_membership(member, payload={}): - data = { - "doctype": "Membership", - "member": member, - "membership_status": "Current", - "membership_type": "_rzpy_test_milythm", - "currency": "INR", - "paid": 1, - "from_date": nowdate(), - "amount": 100 - } - data.update(payload) - membership = frappe.get_doc(data) - membership.insert(ignore_permissions=True, ignore_if_duplicate=True) - return membership - -def create_item(item_code): - if not frappe.db.exists("Item", item_code): - item = frappe.new_doc("Item") - item.item_code = item_code - item.item_name = item_code - item.stock_uom = "Nos" - item.description = item_code - item.item_group = "All Item Groups" - item.is_stock_item = 0 - item.save() - else: - item = frappe.get_doc("Item", item_code) - return item - -def setup_membership(): - # Get default company - company = frappe.get_doc("Company", erpnext.get_default_company()) - - # update non profit settings - settings = frappe.get_doc("Non Profit Settings") - # Enable razorpay - settings.enable_razorpay_for_memberships = 1 - settings.billing_cycle = "Monthly" - settings.billing_frequency = 24 - # Enable invoicing - settings.allow_invoicing = 1 - settings.automate_membership_payment_entries = 1 - settings.company = company.name - settings.donation_company = company.name - settings.membership_payment_account = company.default_cash_account - settings.membership_debit_account = company.default_receivable_account - settings.flags.ignore_mandatory = True - settings.save() - - # make test plan - if not frappe.db.exists("Membership Type", "_rzpy_test_milythm"): - plan = frappe.new_doc("Membership Type") - plan.membership_type = "_rzpy_test_milythm" - plan.amount = 100 - plan.razorpay_plan_id = "_rzpy_test_milythm" - plan.linked_item = create_item("_Test Item for Non Profit Membership").name - plan.insert() - else: - plan = frappe.get_doc("Membership Type", "_rzpy_test_milythm") - - return plan - -def get_subscription_payload(): - return { - "entity": "event", - "account_id": "acc_BFQ7uQEaa7j2z7", - "event": "subscription.halted", - "contains": [ - "subscription" - ], - "payload": { - "subscription": { - "entity": { - "id": "sub_DEX6xcJ1HSW4CR", - "entity": "subscription", - "plan_id": "_rzpy_test_milythm", - "customer_id": "cust_C0WlbKhp3aLA7W", - "status": "halted", - "notes": { - "Important": "Notes for Internal Reference" - }, - } - } - } - } diff --git a/erpnext/non_profit/doctype/membership_type/__init__.py b/erpnext/non_profit/doctype/membership_type/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/non_profit/doctype/membership_type/membership_type.js b/erpnext/non_profit/doctype/membership_type/membership_type.js deleted file mode 100644 index 2f2427629c..0000000000 --- a/erpnext/non_profit/doctype/membership_type/membership_type.js +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -frappe.ui.form.on('Membership Type', { - refresh: function (frm) { - frappe.db.get_single_value('Non Profit Settings', 'enable_razorpay_for_memberships').then(val => { - if (val) frm.set_df_property('razorpay_plan_id', 'hidden', false); - }); - - frappe.db.get_single_value('Non Profit Settings', 'allow_invoicing').then(val => { - if (val) frm.set_df_property('linked_item', 'hidden', false); - }); - - frm.set_query('linked_item', () => { - return { - filters: { - is_stock_item: 0 - } - }; - }); - } -}); diff --git a/erpnext/non_profit/doctype/membership_type/membership_type.json b/erpnext/non_profit/doctype/membership_type/membership_type.json deleted file mode 100644 index 6ce1ecde12..0000000000 --- a/erpnext/non_profit/doctype/membership_type/membership_type.json +++ /dev/null @@ -1,71 +0,0 @@ -{ - "actions": [], - "autoname": "field:membership_type", - "creation": "2017-09-18 12:56:56.343999", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "membership_type", - "amount", - "razorpay_plan_id", - "linked_item" - ], - "fields": [ - { - "fieldname": "membership_type", - "fieldtype": "Data", - "in_list_view": 1, - "in_standard_filter": 1, - "label": "Membership Type", - "reqd": 1, - "unique": 1 - }, - { - "fieldname": "amount", - "fieldtype": "Float", - "in_list_view": 1, - "label": "Amount", - "reqd": 1 - }, - { - "fieldname": "razorpay_plan_id", - "fieldtype": "Data", - "hidden": 1, - "label": "Razorpay Plan ID", - "unique": 1 - }, - { - "fieldname": "linked_item", - "fieldtype": "Link", - "label": "Linked Item", - "options": "Item", - "unique": 1 - } - ], - "links": [], - "modified": "2020-08-05 15:21:43.595745", - "modified_by": "Administrator", - "module": "Non Profit", - "name": "Membership Type", - "owner": "Administrator", - "permissions": [ - { - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "Non Profit Manager", - "share": 1, - "write": 1 - } - ], - "quick_entry": 1, - "restrict_to_domain": "Non Profit", - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1 -} \ No newline at end of file diff --git a/erpnext/non_profit/doctype/membership_type/membership_type.py b/erpnext/non_profit/doctype/membership_type/membership_type.py deleted file mode 100644 index b446421571..0000000000 --- a/erpnext/non_profit/doctype/membership_type/membership_type.py +++ /dev/null @@ -1,18 +0,0 @@ -# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - - -import frappe -from frappe import _ -from frappe.model.document import Document - - -class MembershipType(Document): - def validate(self): - if self.linked_item: - is_stock_item = frappe.db.get_value("Item", self.linked_item, "is_stock_item") - if is_stock_item: - frappe.throw(_("The Linked Item should be a service item")) - -def get_membership_type(razorpay_id): - return frappe.db.exists("Membership Type", {"razorpay_plan_id": razorpay_id}) diff --git a/erpnext/non_profit/doctype/membership_type/test_membership_type.py b/erpnext/non_profit/doctype/membership_type/test_membership_type.py deleted file mode 100644 index 98bc087acd..0000000000 --- a/erpnext/non_profit/doctype/membership_type/test_membership_type.py +++ /dev/null @@ -1,8 +0,0 @@ -# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt - -import unittest - - -class TestMembershipType(unittest.TestCase): - pass diff --git a/erpnext/non_profit/doctype/non_profit_settings/__init__.py b/erpnext/non_profit/doctype/non_profit_settings/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 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 deleted file mode 100644 index 4c4ca9834b..0000000000 --- a/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.js +++ /dev/null @@ -1,133 +0,0 @@ -// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -frappe.ui.form.on("Non Profit Settings", { - refresh: function(frm) { - frm.set_query("inv_print_format", function() { - return { - filters: { - "doc_type": "Sales Invoice" - } - }; - }); - - frm.set_query("membership_print_format", function() { - return { - filters: { - "doc_type": "Membership" - } - }; - }); - - frm.set_query("membership_debit_account", function() { - return { - filters: { - "account_type": "Receivable", - "is_group": 0, - "company": frm.doc.company - } - }; - }); - - 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 { - filters: { - "account_type": ["in", account_types], - "is_group": 0, - "company": frm.doc.company - } - }; - }); - - 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); - frm.trigger("setup_buttons_for_membership"); - frm.trigger("setup_buttons_for_donation"); - }, - - setup_buttons_for_membership: function(frm) { - let label; - - if (frm.doc.membership_webhook_secret) { - - frm.add_custom_button(__("Copy Webhook URL"), () => { - frappe.utils.copy_to_clipboard(`https://${frappe.boot.sitename}/api/method/erpnext.non_profit.doctype.membership.membership.trigger_razorpay_subscription`); - }, __("Memberships")); - - frm.add_custom_button(__("Revoke Key"), () => { - frm.call("revoke_key", { - key: "membership_webhook_secret" - }).then(() => { - frm.refresh(); - }); - }, __("Memberships")); - - label = __("Regenerate Webhook Secret"); - - } else { - label = __("Generate Webhook Secret"); - } - - frm.add_custom_button(label, () => { - frm.call("generate_webhook_secret", { - field: "membership_webhook_secret" - }).then(() => { - frm.refresh(); - }); - }, __("Memberships")); - }, - - setup_buttons_for_donation: function(frm) { - let label; - - if (frm.doc.donation_webhook_secret) { - label = __("Regenerate Webhook Secret"); - - frm.add_custom_button(__("Copy Webhook URL"), () => { - frappe.utils.copy_to_clipboard(`https://${frappe.boot.sitename}/api/method/erpnext.non_profit.doctype.donation.donation.capture_razorpay_donations`); - }, __("Donations")); - - frm.add_custom_button(__("Revoke Key"), () => { - frm.call("revoke_key", { - key: "donation_webhook_secret" - }).then(() => { - frm.refresh(); - }); - }, __("Donations")); - - } else { - label = __("Generate Webhook Secret"); - } - - frm.add_custom_button(label, () => { - frm.call("generate_webhook_secret", { - field: "donation_webhook_secret" - }).then(() => { - frm.refresh(); - }); - }, __("Donations")); - } -}); diff --git a/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.json b/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.json deleted file mode 100644 index 25ff0c1bb0..0000000000 --- a/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.json +++ /dev/null @@ -1,273 +0,0 @@ -{ - "actions": [], - "creation": "2020-03-29 12:57:03.005120", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "enable_razorpay_for_memberships", - "razorpay_settings_section", - "billing_cycle", - "billing_frequency", - "membership_webhook_secret", - "column_break_6", - "allow_invoicing", - "automate_membership_invoicing", - "automate_membership_payment_entries", - "company", - "membership_debit_account", - "membership_payment_account", - "column_break_9", - "send_email", - "send_invoice", - "membership_print_format", - "inv_print_format", - "email_template", - "donation_settings_section", - "donation_company", - "default_donor_type", - "donation_webhook_secret", - "column_break_22", - "automate_donation_payment_entries", - "donation_debit_account", - "donation_payment_account", - "section_break_27", - "creation_user" - ], - "fields": [ - { - "fieldname": "billing_cycle", - "fieldtype": "Select", - "label": "Billing Cycle", - "options": "Monthly\nYearly" - }, - { - "depends_on": "eval:doc.enable_razorpay_for_memberships", - "fieldname": "razorpay_settings_section", - "fieldtype": "Section Break", - "label": "RazorPay Settings for Memberships" - }, - { - "description": "The number of billing cycles for which the customer should be charged. For example, if a customer is buying a 1-year membership that should be billed on a monthly basis, this value should be 12.", - "fieldname": "billing_frequency", - "fieldtype": "Int", - "label": "Billing Frequency" - }, - { - "fieldname": "column_break_6", - "fieldtype": "Section Break", - "label": "Membership Invoicing" - }, - { - "fieldname": "column_break_9", - "fieldtype": "Column Break" - }, - { - "description": "This company will be set for the Memberships created via webhook.", - "fieldname": "company", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Company", - "options": "Company", - "reqd": 1 - }, - { - "default": "0", - "depends_on": "eval:doc.allow_invoicing && doc.send_email", - "fieldname": "send_invoice", - "fieldtype": "Check", - "label": "Send Invoice with Email" - }, - { - "default": "0", - "fieldname": "send_email", - "fieldtype": "Check", - "label": "Send Membership Acknowledgement" - }, - { - "depends_on": "eval: doc.send_invoice", - "fieldname": "inv_print_format", - "fieldtype": "Link", - "label": "Invoice Print Format", - "mandatory_depends_on": "eval: doc.send_invoice", - "options": "Print Format" - }, - { - "depends_on": "eval:doc.send_email", - "fieldname": "membership_print_format", - "fieldtype": "Link", - "label": "Membership Print Format", - "options": "Print Format" - }, - { - "depends_on": "eval:doc.send_email", - "fieldname": "email_template", - "fieldtype": "Link", - "label": "Email Template", - "mandatory_depends_on": "eval:doc.send_email", - "options": "Email Template" - }, - { - "default": "0", - "fieldname": "allow_invoicing", - "fieldtype": "Check", - "label": "Allow Invoicing for Memberships", - "mandatory_depends_on": "eval:doc.send_invoice || doc.make_payment_entry" - }, - { - "default": "0", - "depends_on": "eval:doc.allow_invoicing", - "description": "Automatically create an invoice when payment is authorized from a web form entry", - "fieldname": "automate_membership_invoicing", - "fieldtype": "Check", - "label": "Automate Invoicing for Web Forms" - }, - { - "default": "0", - "depends_on": "eval:doc.allow_invoicing", - "description": "Auto creates Payment Entry for Sales Invoices created for Membership from web forms.", - "fieldname": "automate_membership_payment_entries", - "fieldtype": "Check", - "label": "Automate Payment Entry Creation" - }, - { - "default": "0", - "fieldname": "enable_razorpay_for_memberships", - "fieldtype": "Check", - "label": "Enable RazorPay For Memberships" - }, - { - "depends_on": "eval:doc.automate_membership_payment_entries", - "description": "Account for accepting membership payments", - "fieldname": "membership_payment_account", - "fieldtype": "Link", - "label": "Membership Payment To", - "mandatory_depends_on": "eval:doc.automate_membership_payment_entries", - "options": "Account" - }, - { - "fieldname": "membership_webhook_secret", - "fieldtype": "Password", - "label": "Membership Webhook Secret", - "read_only": 1 - }, - { - "fieldname": "donation_webhook_secret", - "fieldtype": "Password", - "label": "Donation Webhook Secret", - "read_only": 1 - }, - { - "depends_on": "automate_donation_payment_entries", - "description": "Account for accepting donation payments", - "fieldname": "donation_payment_account", - "fieldtype": "Link", - "label": "Donation Payment To", - "mandatory_depends_on": "automate_donation_payment_entries", - "options": "Account" - }, - { - "default": "0", - "description": "Auto creates Payment Entry for Donations created from web forms.", - "fieldname": "automate_donation_payment_entries", - "fieldtype": "Check", - "label": "Automate Donation Payment Entries" - }, - { - "depends_on": "eval:doc.allow_invoicing", - "fieldname": "membership_debit_account", - "fieldtype": "Link", - "label": "Debit Account", - "mandatory_depends_on": "eval:doc.allow_invoicing", - "options": "Account" - }, - { - "depends_on": "automate_donation_payment_entries", - "fieldname": "donation_debit_account", - "fieldtype": "Link", - "label": "Debit Account", - "mandatory_depends_on": "automate_donation_payment_entries", - "options": "Account" - }, - { - "description": "This company will be set for the Donations created via webhook.", - "fieldname": "donation_company", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Company", - "options": "Company", - "reqd": 1 - }, - { - "fieldname": "donation_settings_section", - "fieldtype": "Section Break", - "label": "Donation Settings" - }, - { - "fieldname": "column_break_22", - "fieldtype": "Column Break" - }, - { - "description": "This Donor Type will be set for the Donor created via Donation web form entry.", - "fieldname": "default_donor_type", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Default Donor Type", - "options": "Donor Type", - "reqd": 1 - }, - { - "fieldname": "section_break_27", - "fieldtype": "Section Break" - }, - { - "description": "The user that will be used to create Donations, Memberships, Invoices, and Payment Entries. This user should have the relevant permissions.", - "fieldname": "creation_user", - "fieldtype": "Link", - "label": "Creation User", - "options": "User", - "reqd": 1 - } - ], - "index_web_pages_for_search": 1, - "issingle": 1, - "links": [], - "modified": "2021-03-11 10:43:38.124240", - "modified_by": "Administrator", - "module": "Non Profit", - "name": "Non Profit Settings", - "owner": "Administrator", - "permissions": [ - { - "create": 1, - "delete": 1, - "email": 1, - "print": 1, - "read": 1, - "role": "System Manager", - "share": 1, - "write": 1 - }, - { - "create": 1, - "delete": 1, - "email": 1, - "print": 1, - "read": 1, - "role": "Non Profit Manager", - "share": 1, - "write": 1 - }, - { - "email": 1, - "print": 1, - "read": 1, - "role": "Non Profit Member", - "share": 1 - } - ], - "quick_entry": 1, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1 -} \ No newline at end of file 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 deleted file mode 100644 index ace6605542..0000000000 --- a/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.py +++ /dev/null @@ -1,38 +0,0 @@ -# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - - -import frappe -from frappe import _ -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) - self.save() - - secret_for = "Membership" if field == "membership_webhook_secret" else "Donation" - - frappe.msgprint( - _("Here is your webhook secret for {0} API, this will be shown to you only once.").format(secret_for) + "{{ _(\"Certificate No. : \") }} {{ doc.name }}
\n\n \t{{ _(\"Date\") }} : {{ doc.get_formatted(\"date\") }}
\n
\n We thank you for your contribution towards the corpus of the {{ doc.company }} and helping support our work.\n
\n\n{{doc.company_address_display }}
\n\n ", - "idx": 0, - "line_breaks": 0, - "modified": "2021-02-22 00:20:08.516600", - "modified_by": "Administrator", - "module": "Regional", - "name": "80G Certificate for Donation", - "owner": "Administrator", - "print_format_builder": 0, - "print_format_type": "Jinja", - "raw_printing": 0, - "show_section_headings": 0, - "standard": "Yes" -} \ No newline at end of file diff --git a/erpnext/regional/print_format/80g_certificate_for_donation/__init__.py b/erpnext/regional/print_format/80g_certificate_for_donation/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/regional/print_format/80g_certificate_for_membership/80g_certificate_for_membership.json b/erpnext/regional/print_format/80g_certificate_for_membership/80g_certificate_for_membership.json deleted file mode 100644 index f1b15aab29..0000000000 --- a/erpnext/regional/print_format/80g_certificate_for_membership/80g_certificate_for_membership.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "absolute_value": 0, - "align_labels_right": 0, - "creation": "2021-02-15 16:53:55.026611", - "css": ".details {\n font-size: 15px;\n font-family: Tahoma, sans-serif;;\n line-height: 150%;\n}\n\n.certificate-footer {\n font-size: 15px;\n font-family: Tahoma, sans-serif;\n line-height: 140%;\n margin-top: 120px;\n}\n\n.company-address {\n color: #666666;\n font-size: 15px;\n font-family: Tahoma, sans-serif;;\n}", - "custom_format": 1, - "default_print_language": "en", - "disabled": 0, - "doc_type": "Tax Exemption 80G Certificate", - "docstatus": 0, - "doctype": "Print Format", - "font": "Default", - "html": "{% if letter_head and not no_letterhead -%}\n{{ _(\"Certificate No. : \") }} {{ doc.name }}
\n\n \t{{ _(\"Date\") }} : {{ doc.get_formatted(\"date\") }}
\n
{{ _(\"Date\") }} | \n \t\t\t{{ _(\"Amount\") }} | \n \t\t\t{{ _(\"Invoice ID\") }} | \n \t\t
---|---|---|
{{ payment.date }} | \n \t\t\t{{ payment.get_formatted(\"amount\") }} | \n \t\t\t{{ payment.invoice_id }} | \n \t\t
\n We thank you for your contribution towards the corpus of the {{ doc.company }} and helping support our work.\n
\n\n{{doc.company_address_display }}
\n\n ", - "idx": 0, - "line_breaks": 0, - "modified": "2021-02-21 23:29:00.778973", - "modified_by": "Administrator", - "module": "Regional", - "name": "80G Certificate for Membership", - "owner": "Administrator", - "print_format_builder": 0, - "print_format_type": "Jinja", - "raw_printing": 0, - "show_section_headings": 0, - "standard": "Yes" -} \ No newline at end of file diff --git a/erpnext/regional/print_format/80g_certificate_for_membership/__init__.py b/erpnext/regional/print_format/80g_certificate_for_membership/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/selling/doctype/customer/test_customer.py b/erpnext/selling/doctype/customer/test_customer.py index 5301fd0524..165ee81872 100644 --- a/erpnext/selling/doctype/customer/test_customer.py +++ b/erpnext/selling/doctype/customer/test_customer.py @@ -4,12 +4,13 @@ import frappe from frappe.test_runner import make_test_records +from frappe.tests.utils import FrappeTestCase from frappe.utils import flt from erpnext.accounts.party import get_due_date from erpnext.exceptions import PartyDisabled, PartyFrozen from erpnext.selling.doctype.customer.customer import get_credit_limit, get_customer_outstanding -from erpnext.tests.utils import ERPNextTestCase, create_test_contact_and_address +from erpnext.tests.utils import create_test_contact_and_address test_ignore = ["Price List"] test_dependencies = ['Payment Term', 'Payment Terms Template'] @@ -17,7 +18,7 @@ test_records = frappe.get_test_records('Customer') -class TestCustomer(ERPNextTestCase): +class TestCustomer(FrappeTestCase): def setUp(self): if not frappe.get_value('Item', '_Test Item'): make_test_records('Item') diff --git a/erpnext/selling/doctype/party_specific_item/test_party_specific_item.py b/erpnext/selling/doctype/party_specific_item/test_party_specific_item.py index b951044f33..9b672b4b5d 100644 --- a/erpnext/selling/doctype/party_specific_item/test_party_specific_item.py +++ b/erpnext/selling/doctype/party_specific_item/test_party_specific_item.py @@ -1,12 +1,10 @@ # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt -import unittest - import frappe +from frappe.tests.utils import FrappeTestCase from erpnext.controllers.queries import item_query -from erpnext.tests.utils import ERPNextTestCase test_dependencies = ['Item', 'Customer', 'Supplier'] @@ -18,7 +16,7 @@ def create_party_specific_item(**args): psi.based_on_value = args.get('based_on_value') psi.insert() -class TestPartySpecificItem(ERPNextTestCase): +class TestPartySpecificItem(FrappeTestCase): def setUp(self): self.customer = frappe.get_last_doc("Customer") self.supplier = frappe.get_last_doc("Supplier") diff --git a/erpnext/selling/doctype/quotation/quotation.js b/erpnext/selling/doctype/quotation/quotation.js index 0e1a915deb..34e9a52e11 100644 --- a/erpnext/selling/doctype/quotation/quotation.js +++ b/erpnext/selling/doctype/quotation/quotation.js @@ -40,7 +40,6 @@ frappe.ui.form.on('Quotation', { erpnext.selling.QuotationController = class QuotationController extends erpnext.selling.SellingController { onload(doc, dt, dn) { - var me = this; super.onload(doc, dt, dn); } party_name() { diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py index 4357201d23..a749d9e1f1 100644 --- a/erpnext/selling/doctype/quotation/test_quotation.py +++ b/erpnext/selling/doctype/quotation/test_quotation.py @@ -2,14 +2,13 @@ # License: GNU General Public License v3. See license.txt import frappe +from frappe.tests.utils import FrappeTestCase from frappe.utils import add_days, add_months, flt, getdate, nowdate -from erpnext.tests.utils import ERPNextTestCase - test_dependencies = ["Product Bundle"] -class TestQuotation(ERPNextTestCase): +class TestQuotation(FrappeTestCase): def test_make_quotation_without_terms(self): quotation = make_quotation(do_not_save=1) self.assertFalse(quotation.get('payment_schedule')) diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index eb98e6c0bf..f80eaf2757 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -562,6 +562,7 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex var me = this; var dialog = new frappe.ui.Dialog({ title: __("Select Items"), + size: "large", fields: [ { "fieldtype": "Check", @@ -663,7 +664,8 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex } else { let po_items = []; me.frm.doc.items.forEach(d => { - let pending_qty = (flt(d.stock_qty) - flt(d.ordered_qty)) / flt(d.conversion_factor); + let ordered_qty = me.get_ordered_qty(d, me.frm.doc); + let pending_qty = (flt(d.stock_qty) - ordered_qty) / flt(d.conversion_factor); if (pending_qty > 0) { po_items.push({ "doctype": "Sales Order Item", @@ -689,6 +691,24 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex dialog.show(); } + get_ordered_qty(item, so) { + let ordered_qty = item.ordered_qty; + if (so.packed_items) { + // calculate ordered qty based on packed items in case of product bundle + let packed_items = so.packed_items.filter( + (pi) => pi.parent_detail_docname == item.name + ); + if (packed_items) { + ordered_qty = packed_items.reduce( + (sum, pi) => sum + flt(pi.ordered_qty), + 0 + ); + ordered_qty = ordered_qty / packed_items.length; + } + } + return ordered_qty; + } + hold_sales_order(){ var me = this; var d = new frappe.ui.Dialog({ diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 0f5b1e3b89..abbb3c9b90 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -877,6 +877,9 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None): target.stock_qty = (flt(source.stock_qty) - flt(source.ordered_qty)) target.project = source_parent.project + def update_item_for_packed_item(source, target, source_parent): + target.qty = flt(source.qty) - flt(source.ordered_qty) + # po = frappe.get_list("Purchase Order", filters={"sales_order":source_name, "supplier":supplier, "docstatus": ("<", "2")}) doc = get_mapped_doc("Sales Order", source_name, { "Sales Order": { @@ -920,6 +923,7 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None): "Packed Item": { "doctype": "Purchase Order Item", "field_map": [ + ["name", "sales_order_packed_item"], ["parent", "sales_order"], ["uom", "uom"], ["conversion_factor", "conversion_factor"], @@ -934,6 +938,7 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None): "supplier", "pricing_rules" ], + "postprocess": update_item_for_packed_item, "condition": lambda doc: doc.parent_item in items_to_map } }, target_doc, set_missing_values) diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 73c5bd299a..b5284793e1 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -6,6 +6,7 @@ import json import frappe import frappe.permissions from frappe.core.doctype.user_permission.test_user_permission import create_user +from frappe.tests.utils import FrappeTestCase from frappe.utils import add_days, flt, getdate, nowdate, today from erpnext.controllers.accounts_controller import update_child_qty_rate @@ -27,10 +28,9 @@ from erpnext.selling.doctype.sales_order.sales_order import ( ) from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry -from erpnext.tests.utils import ERPNextTestCase -class TestSalesOrder(ERPNextTestCase): +class TestSalesOrder(FrappeTestCase): @classmethod def setUpClass(cls): @@ -959,6 +959,42 @@ class TestSalesOrder(ERPNextTestCase): self.assertEqual(purchase_order.items[0].item_code, "_Test Bundle Item 1") self.assertEqual(purchase_order.items[1].item_code, "_Test Bundle Item 2") + def test_purchase_order_updates_packed_item_ordered_qty(self): + """ + Tests if the packed item's `ordered_qty` is updated with the quantity of the Purchase Order + """ + from erpnext.selling.doctype.sales_order.sales_order import make_purchase_order + + product_bundle = make_item("_Test Product Bundle", {"is_stock_item": 0}) + make_item("_Test Bundle Item 1", {"is_stock_item": 1}) + make_item("_Test Bundle Item 2", {"is_stock_item": 1}) + + make_product_bundle("_Test Product Bundle", + ["_Test Bundle Item 1", "_Test Bundle Item 2"]) + + so_items = [ + { + "item_code": product_bundle.item_code, + "warehouse": "", + "qty": 2, + "rate": 400, + "delivered_by_supplier": 1, + "supplier": '_Test Supplier' + } + ] + + so = make_sales_order(item_list=so_items) + + purchase_order = make_purchase_order(so.name, selected_items=so_items) + purchase_order.supplier = "_Test Supplier" + purchase_order.set_warehouse = "_Test Warehouse - _TC" + purchase_order.save() + purchase_order.submit() + + so.reload() + self.assertEqual(so.packed_items[0].ordered_qty, 2) + self.assertEqual(so.packed_items[1].ordered_qty, 2) + 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/doctype/sales_order_item/sales_order_item.json b/erpnext/selling/doctype/sales_order_item/sales_order_item.json index 080d517d13..7e55499533 100644 --- a/erpnext/selling/doctype/sales_order_item/sales_order_item.json +++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.json @@ -83,8 +83,8 @@ "planned_qty", "column_break_69", "work_order_qty", - "produced_qty", "delivered_qty", + "produced_qty", "returned_qty", "shopping_cart_section", "additional_notes", @@ -701,8 +701,10 @@ "width": "50px" }, { + "description": "For Production", "fieldname": "produced_qty", "fieldtype": "Float", + "hidden": 1, "label": "Produced Quantity", "oldfieldname": "produced_qty", "oldfieldtype": "Currency", @@ -791,6 +793,7 @@ }, { "default": "0", + "fetch_from": "item_code.grant_commission", "fieldname": "grant_commission", "fieldtype": "Check", "label": "Grant Commission", @@ -800,7 +803,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2022-02-21 13:55:08.883104", + "modified": "2022-02-24 14:41:57.325799", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order Item", diff --git a/erpnext/selling/page/point_of_sale/pos_payment.js b/erpnext/selling/page/point_of_sale/pos_payment.js index 4d75e6ef1b..1e9f6d7d92 100644 --- a/erpnext/selling/page/point_of_sale/pos_payment.js +++ b/erpnext/selling/page/point_of_sale/pos_payment.js @@ -170,17 +170,20 @@ erpnext.PointOfSale.Payment = class { }); frappe.ui.form.on('POS Invoice', 'coupon_code', (frm) => { - if (!frm.doc.ignore_pricing_rule) { - if (frm.doc.coupon_code) { - frappe.run_serially([ - () => frm.doc.ignore_pricing_rule=1, - () => frm.trigger('ignore_pricing_rule'), - () => frm.doc.ignore_pricing_rule=0, - () => frm.trigger('apply_pricing_rule'), - () => frm.save(), - () => this.update_totals_section(frm.doc) - ]); - } + if (!frm.doc.ignore_pricing_rule && frm.doc.coupon_code) { + frappe.run_serially([ + () => frm.doc.ignore_pricing_rule=1, + () => frm.trigger('ignore_pricing_rule'), + () => frm.doc.ignore_pricing_rule=0, + () => frm.trigger('apply_pricing_rule'), + () => frm.save(), + () => this.update_totals_section(frm.doc) + ]); + } else if (frm.doc.ignore_pricing_rule && frm.doc.coupon_code) { + frappe.show_alert({ + message: __("Ignore Pricing Rule is enabled. Cannot apply coupon code."), + indicator: "orange" + }); } }); diff --git a/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py index cad41e1dc0..f7f8a5dbce 100644 --- a/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py +++ b/erpnext/selling/report/payment_terms_status_for_sales_order/test_payment_terms_status_for_sales_order.py @@ -1,6 +1,7 @@ import datetime import frappe +from frappe.tests.utils import FrappeTestCase from frappe.utils import add_days from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice @@ -9,12 +10,11 @@ from erpnext.selling.report.payment_terms_status_for_sales_order.payment_terms_s execute, ) from erpnext.stock.doctype.item.test_item import create_item -from erpnext.tests.utils import ERPNextTestCase test_dependencies = ["Sales Order", "Item", "Sales Invoice", "Payment Terms Template"] -class TestPaymentTermsStatusForSalesOrder(ERPNextTestCase): +class TestPaymentTermsStatusForSalesOrder(FrappeTestCase): def create_payment_terms_template(self): # create template for 50-50 payments template = None diff --git a/erpnext/selling/report/pending_so_items_for_purchase_request/test_pending_so_items_for_purchase_request.py b/erpnext/selling/report/pending_so_items_for_purchase_request/test_pending_so_items_for_purchase_request.py index d62915fc66..16162acc8f 100644 --- a/erpnext/selling/report/pending_so_items_for_purchase_request/test_pending_so_items_for_purchase_request.py +++ b/erpnext/selling/report/pending_so_items_for_purchase_request/test_pending_so_items_for_purchase_request.py @@ -2,6 +2,7 @@ # For license information, please see license.txt +from frappe.tests.utils import FrappeTestCase from frappe.utils import add_months, nowdate from erpnext.selling.doctype.sales_order.sales_order import make_material_request @@ -9,10 +10,9 @@ from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_orde from erpnext.selling.report.pending_so_items_for_purchase_request.pending_so_items_for_purchase_request import ( execute, ) -from erpnext.tests.utils import ERPNextTestCase -class TestPendingSOItemsForPurchaseRequest(ERPNextTestCase): +class TestPendingSOItemsForPurchaseRequest(FrappeTestCase): def test_result_for_partial_material_request(self): so = make_sales_order() mr=make_material_request(so.name) diff --git a/erpnext/selling/report/sales_analytics/test_analytics.py b/erpnext/selling/report/sales_analytics/test_analytics.py index f56cce2dfd..564f48fef3 100644 --- a/erpnext/selling/report/sales_analytics/test_analytics.py +++ b/erpnext/selling/report/sales_analytics/test_analytics.py @@ -3,13 +3,13 @@ import frappe +from frappe.tests.utils import FrappeTestCase from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order from erpnext.selling.report.sales_analytics.sales_analytics import execute -from erpnext.tests.utils import ERPNextTestCase -class TestAnalytics(ERPNextTestCase): +class TestAnalytics(FrappeTestCase): def test_sales_analytics(self): frappe.db.sql("delete from `tabSales Order` where company='_Test Company 2'") diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index 95b1e8b9c6..36ad8fec9f 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -3,7 +3,6 @@ import json -import os import frappe import frappe.defaults @@ -422,14 +421,14 @@ def get_name_with_abbr(name, company): return " - ".join(parts) def install_country_fixtures(company, country): - path = frappe.get_app_path('erpnext', 'regional', frappe.scrub(country)) - if os.path.exists(path.encode("utf-8")): - try: - module_name = "erpnext.regional.{0}.setup.setup".format(frappe.scrub(country)) - frappe.get_attr(module_name)(company, False) - except Exception as e: - frappe.log_error() - frappe.throw(_("Failed to setup defaults for country {0}. Please contact support.").format(frappe.bold(country))) + try: + module_name = f"erpnext.regional.{frappe.scrub(country)}.setup.setup" + frappe.get_attr(module_name)(company, False) + except ImportError: + pass + except Exception: + frappe.log_error() + frappe.throw(_("Failed to setup defaults for country {0}. Please contact support.").format(frappe.bold(country))) def update_company_current_month_sales(company): diff --git a/erpnext/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py index cd2738aeaa..cefa0f3887 100644 --- a/erpnext/setup/setup_wizard/operations/install_fixtures.py +++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py @@ -195,10 +195,8 @@ def install(country=None): {'doctype': "Party Type", "party_type": "Customer", "account_type": "Receivable"}, {'doctype': "Party Type", "party_type": "Supplier", "account_type": "Payable"}, {'doctype': "Party Type", "party_type": "Employee", "account_type": "Payable"}, - {'doctype': "Party Type", "party_type": "Member", "account_type": "Receivable"}, {'doctype': "Party Type", "party_type": "Shareholder", "account_type": "Payable"}, {'doctype': "Party Type", "party_type": "Student", "account_type": "Receivable"}, - {'doctype': "Party Type", "party_type": "Donor", "account_type": "Receivable"}, {'doctype': "Opportunity Type", "name": _("Sales")}, {'doctype': "Opportunity Type", "name": _("Support")}, diff --git a/erpnext/setup/utils.py b/erpnext/setup/utils.py index 4441bb9562..a4f2207f11 100644 --- a/erpnext/setup/utils.py +++ b/erpnext/setup/utils.py @@ -155,7 +155,7 @@ def insert_record(records): doc = frappe.new_doc(r.get("doctype")) doc.update(r) try: - doc.insert(ignore_permissions=True) + doc.insert(ignore_permissions=True, ignore_if_duplicate=True) except frappe.DuplicateEntryError as e: # pass DuplicateEntryError and continue if e.args and e.args[0]==doc.doctype and e.args[1]==doc.name: diff --git a/erpnext/stock/dashboard/item_dashboard.js b/erpnext/stock/dashboard/item_dashboard.js index 37e9e89a0a..c9d5f61f22 100644 --- a/erpnext/stock/dashboard/item_dashboard.js +++ b/erpnext/stock/dashboard/item_dashboard.js @@ -213,7 +213,14 @@ erpnext.stock.move_item = function (item, source, target, actual_qty, rate, call label: __('Target Warehouse'), fieldtype: 'Link', options: 'Warehouse', - reqd: 1 + reqd: 1, + get_query() { + return { + filters: { + is_group: 0 + } + } + } }, { fieldname: 'qty', @@ -252,52 +259,21 @@ erpnext.stock.move_item = function (item, source, target, actual_qty, rate, call dialog.get_field('target').refresh(); } - dialog.set_primary_action(__('Submit'), function () { - var values = dialog.get_values(); - if (!values) { - return; - } - if (source && values.qty > actual_qty) { - frappe.msgprint(__('Quantity must be less than or equal to {0}', [actual_qty])); - return; - } - if (values.source === values.target) { - frappe.msgprint(__('Source and target warehouse must be different')); - } - - frappe.call({ - method: 'erpnext.stock.doctype.stock_entry.stock_entry_utils.make_stock_entry', - args: values, - btn: dialog.get_primary_btn(), - freeze: true, - freeze_message: __('Creating Stock Entry'), - callback: function (r) { - frappe.show_alert(__('Stock Entry {0} created', - ['' + r.message.name + ''])); - dialog.hide(); - callback(r); - }, + dialog.set_primary_action(__('Create Stock Entry'), function () { + frappe.model.with_doctype('Stock Entry', function () { + let doc = frappe.model.get_new_doc('Stock Entry'); + doc.from_warehouse = dialog.get_value('source'); + doc.to_warehouse = dialog.get_value('target'); + doc.stock_entry_type = doc.from_warehouse ? "Material Transfer" : "Material Receipt"; + let row = frappe.model.add_child(doc, 'items'); + row.item_code = dialog.get_value('item_code'); + row.s_warehouse = dialog.get_value('source'); + row.t_warehouse = dialog.get_value('target'); + row.qty = dialog.get_value('qty'); + row.conversion_factor = 1; + row.transfer_qty = dialog.get_value('qty'); + row.basic_rate = dialog.get_value('rate'); + frappe.set_route('Form', doc.doctype, doc.name); }); }); - - $('' + - __("Add more items or open full form") + '
') - .appendTo(dialog.body) - .find('.link-open') - .on('click', function () { - frappe.model.with_doctype('Stock Entry', function () { - var doc = frappe.model.get_new_doc('Stock Entry'); - doc.from_warehouse = dialog.get_value('source'); - doc.to_warehouse = dialog.get_value('target'); - var row = frappe.model.add_child(doc, 'items'); - row.item_code = dialog.get_value('item_code'); - row.f_warehouse = dialog.get_value('target'); - row.t_warehouse = dialog.get_value('target'); - row.qty = dialog.get_value('qty'); - row.conversion_factor = 1; - row.transfer_qty = dialog.get_value('qty'); - row.basic_rate = dialog.get_value('rate'); - frappe.set_route('Form', doc.doctype, doc.name); - }); - }); }; diff --git a/erpnext/stock/doctype/batch/test_batch.py b/erpnext/stock/doctype/batch/test_batch.py index baa03024af..5763753853 100644 --- a/erpnext/stock/doctype/batch/test_batch.py +++ b/erpnext/stock/doctype/batch/test_batch.py @@ -5,6 +5,7 @@ import json import frappe from frappe.exceptions import ValidationError +from frappe.tests.utils import FrappeTestCase from frappe.utils import cint, flt from frappe.utils.data import add_to_date, getdate @@ -16,10 +17,9 @@ from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import ) from erpnext.stock.get_item_details import get_item_details from erpnext.stock.stock_ledger import get_valuation_rate -from erpnext.tests.utils import ERPNextTestCase -class TestBatch(ERPNextTestCase): +class TestBatch(FrappeTestCase): def test_item_has_batch_enabled(self): self.assertRaises(ValidationError, frappe.get_doc({ "doctype": "Batch", @@ -433,14 +433,13 @@ def create_price_list_for_batch(item_code, batch, rate): def make_new_batch(**args): args = frappe._dict(args) - try: + if frappe.db.exists("Batch", args.batch_id): + batch = frappe.get_doc("Batch", args.batch_id) + else: batch = frappe.get_doc({ "doctype": "Batch", "batch_id": args.batch_id, "item": args.item_code, }).insert() - except frappe.DuplicateEntryError: - batch = frappe.get_doc("Batch", args.batch_id) - return batch diff --git a/erpnext/stock/doctype/bin/test_bin.py b/erpnext/stock/doctype/bin/test_bin.py index 250126c6b9..ec0d8a88e3 100644 --- a/erpnext/stock/doctype/bin/test_bin.py +++ b/erpnext/stock/doctype/bin/test_bin.py @@ -2,13 +2,13 @@ # See license.txt import frappe +from frappe.tests.utils import FrappeTestCase from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.utils import _create_bin -from erpnext.tests.utils import ERPNextTestCase -class TestBin(ERPNextTestCase): +class TestBin(FrappeTestCase): def test_concurrent_inserts(self): diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index bd18e788ba..16c892128a 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -6,6 +6,7 @@ import json import frappe +from frappe.tests.utils import FrappeTestCase from frappe.utils import cstr, flt, nowdate, nowtime from erpnext.accounts.doctype.account.test_account import get_inventory_account @@ -35,10 +36,9 @@ from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import ) from erpnext.stock.doctype.warehouse.test_warehouse import get_warehouse from erpnext.stock.stock_ledger import get_previous_sle -from erpnext.tests.utils import ERPNextTestCase -class TestDeliveryNote(ERPNextTestCase): +class TestDeliveryNote(FrappeTestCase): def test_over_billing_against_dn(self): frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1) diff --git a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json index 51c88bed61..f1f5d96e62 100644 --- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json +++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json @@ -757,6 +757,7 @@ }, { "default": "0", + "fetch_from": "item_code.grant_commission", "fieldname": "grant_commission", "fieldtype": "Check", "label": "Grant Commission", @@ -767,12 +768,14 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-10-06 12:12:44.018872", + "modified": "2022-02-24 14:42:20.211085", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note Item", + "naming_rule": "Random", "owner": "Administrator", "permissions": [], "sort_field": "modified", - "sort_order": "DESC" -} + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/erpnext/stock/doctype/delivery_trip/test_delivery_trip.py b/erpnext/stock/doctype/delivery_trip/test_delivery_trip.py index 321f48b2c5..dcdff4a0f1 100644 --- a/erpnext/stock/doctype/delivery_trip/test_delivery_trip.py +++ b/erpnext/stock/doctype/delivery_trip/test_delivery_trip.py @@ -4,6 +4,7 @@ import unittest import frappe +from frappe.tests.utils import FrappeTestCase from frappe.utils import add_days, flt, now_datetime, nowdate import erpnext @@ -12,10 +13,10 @@ from erpnext.stock.doctype.delivery_trip.delivery_trip import ( make_expense_claim, notify_customers, ) -from erpnext.tests.utils import ERPNextTestCase, create_test_contact_and_address +from erpnext.tests.utils import create_test_contact_and_address -class TestDeliveryTrip(ERPNextTestCase): +class TestDeliveryTrip(FrappeTestCase): def setUp(self): super().setUp() driver = create_driver() diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index b9e8b3f2f1..494fb3b8bb 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -398,6 +398,7 @@ class Item(Document): if merge: self.validate_properties_before_merge(new_name) + self.validate_duplicate_product_bundles_before_merge(old_name, new_name) self.validate_duplicate_website_item_before_merge(old_name, new_name) def after_rename(self, old_name, new_name, merge): @@ -462,6 +463,20 @@ class Item(Document): msg += ": \n" + ", ".join([self.meta.get_label(fld) for fld in field_list]) frappe.throw(msg, title=_("Cannot Merge"), exc=DataValidationError) + def validate_duplicate_product_bundles_before_merge(self, old_name, new_name): + "Block merge if both old and new items have product bundles." + old_bundle = frappe.get_value("Product Bundle",filters={"new_item_code": old_name}) + new_bundle = frappe.get_value("Product Bundle",filters={"new_item_code": new_name}) + + if old_bundle and new_bundle: + bundle_link = get_link_to_form("Product Bundle", old_bundle) + old_name, new_name = frappe.bold(old_name), frappe.bold(new_name) + + msg = _("Please delete Product Bundle {0}, before merging {1} into {2}").format( + bundle_link, old_name, new_name + ) + frappe.throw(msg, title=_("Cannot Merge"), exc=DataValidationError) + def validate_duplicate_website_item_before_merge(self, old_name, new_name): """ Block merge if both old and new items have website items against them. @@ -479,8 +494,9 @@ class Item(Document): old_web_item = [d.get("name") for d in web_items if d.get("item_code") == old_name][0] web_item_link = get_link_to_form("Website Item", old_web_item) + old_name, new_name = frappe.bold(old_name), frappe.bold(new_name) - msg = f"Please delete linked Website Item {frappe.bold(web_item_link)} before merging {old_name} and {new_name}" + msg = f"Please delete linked Website Item {frappe.bold(web_item_link)} before merging {old_name} into {new_name}" frappe.throw(_(msg), title=_("Cannot Merge"), exc=DataValidationError) def set_last_purchase_rate(self, new_name): diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py index fd4df42187..d7671b1d71 100644 --- a/erpnext/stock/doctype/item/test_item.py +++ b/erpnext/stock/doctype/item/test_item.py @@ -6,6 +6,7 @@ import json import frappe from frappe.test_runner import make_test_objects +from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import add_days, today from erpnext.controllers.item_variant import ( @@ -15,6 +16,7 @@ from erpnext.controllers.item_variant import ( get_variant, ) from erpnext.stock.doctype.item.item import ( + DataValidationError, InvalidBarcode, StockExistsForTemplate, get_item_attribute, @@ -24,7 +26,6 @@ from erpnext.stock.doctype.item.item import ( ) from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.get_item_details import get_item_details -from erpnext.tests.utils import ERPNextTestCase, change_settings test_ignore = ["BOM"] test_dependencies = ["Warehouse", "Item Group", "Item Tax Template", "Brand", "Item Attribute"] @@ -52,7 +53,7 @@ def make_item(item_code, properties=None): return item -class TestItem(ERPNextTestCase): +class TestItem(FrappeTestCase): def setUp(self): super().setUp() frappe.flags.attribute_values = None @@ -388,6 +389,26 @@ class TestItem(ERPNextTestCase): self.assertTrue(frappe.db.get_value("Bin", {"item_code": "Test Item for Merging 2", "warehouse": "_Test Warehouse 1 - _TC"})) + def test_item_merging_with_product_bundle(self): + from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle + + create_item("Test Item Bundle Item 1", is_stock_item=False) + create_item("Test Item Bundle Item 2", is_stock_item=False) + create_item("Test Item inside Bundle") + bundle_items = ["Test Item inside Bundle"] + + # make bundles for both items + bundle1 = make_product_bundle("Test Item Bundle Item 1", bundle_items, qty=2) + make_product_bundle("Test Item Bundle Item 2", bundle_items, qty=2) + + with self.assertRaises(DataValidationError): + frappe.rename_doc("Item", "Test Item Bundle Item 1", "Test Item Bundle Item 2", merge=True) + + bundle1.delete() + frappe.rename_doc("Item", "Test Item Bundle Item 1", "Test Item Bundle Item 2", merge=True) + + self.assertFalse(frappe.db.exists("Item", "Test Item Bundle Item 1")) + def test_uom_conversion_factor(self): if frappe.db.exists('Item', 'Test Item UOM'): frappe.delete_doc('Item', 'Test Item UOM') diff --git a/erpnext/stock/doctype/item_alternative/test_item_alternative.py b/erpnext/stock/doctype/item_alternative/test_item_alternative.py index 3976af4e88..501c1c1ad3 100644 --- a/erpnext/stock/doctype/item_alternative/test_item_alternative.py +++ b/erpnext/stock/doctype/item_alternative/test_item_alternative.py @@ -4,6 +4,7 @@ import json import frappe +from frappe.tests.utils import FrappeTestCase from frappe.utils import flt from erpnext.buying.doctype.purchase_order.purchase_order import ( @@ -18,10 +19,9 @@ from erpnext.stock.doctype.item.test_item import create_item from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import ( create_stock_reconciliation, ) -from erpnext.tests.utils import ERPNextTestCase -class TestItemAlternative(ERPNextTestCase): +class TestItemAlternative(FrappeTestCase): def setUp(self): super().setUp() make_items() diff --git a/erpnext/stock/doctype/item_attribute/test_item_attribute.py b/erpnext/stock/doctype/item_attribute/test_item_attribute.py index 0b7ca25715..055c22e0c5 100644 --- a/erpnext/stock/doctype/item_attribute/test_item_attribute.py +++ b/erpnext/stock/doctype/item_attribute/test_item_attribute.py @@ -6,11 +6,12 @@ import frappe test_records = frappe.get_test_records('Item Attribute') +from frappe.tests.utils import FrappeTestCase + from erpnext.stock.doctype.item_attribute.item_attribute import ItemAttributeIncrementError -from erpnext.tests.utils import ERPNextTestCase -class TestItemAttribute(ERPNextTestCase): +class TestItemAttribute(FrappeTestCase): def setUp(self): super().setUp() if frappe.db.exists("Item Attribute", "_Test_Length"): diff --git a/erpnext/stock/doctype/item_price/test_item_price.py b/erpnext/stock/doctype/item_price/test_item_price.py index f81770e487..6ceba3f8d3 100644 --- a/erpnext/stock/doctype/item_price/test_item_price.py +++ b/erpnext/stock/doctype/item_price/test_item_price.py @@ -4,13 +4,13 @@ import frappe from frappe.test_runner import make_test_records_for_doctype +from frappe.tests.utils import FrappeTestCase from erpnext.stock.doctype.item_price.item_price import ItemPriceDuplicateItem from erpnext.stock.get_item_details import get_price_list_rate_for, process_args -from erpnext.tests.utils import ERPNextTestCase -class TestItemPrice(ERPNextTestCase): +class TestItemPrice(FrappeTestCase): def setUp(self): super().setUp() frappe.db.sql("delete from `tabItem Price`") diff --git a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py index df8cadd7f8..6dc4fee569 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py @@ -4,20 +4,21 @@ import frappe +from frappe.tests.utils import FrappeTestCase from frappe.utils import add_to_date, flt, now from erpnext.accounts.doctype.account.test_account import create_account, get_inventory_account from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice from erpnext.accounts.utils import update_gl_entries_after from erpnext.assets.doctype.asset.test_asset import create_asset_category, create_fixed_asset_item +from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import ( get_gl_entries, make_purchase_receipt, ) -from erpnext.tests.utils import ERPNextTestCase -class TestLandedCostVoucher(ERPNextTestCase): +class TestLandedCostVoucher(FrappeTestCase): def test_landed_cost_voucher(self): frappe.db.set_value("Buying Settings", None, "allow_multiple_items", 1) @@ -177,6 +178,53 @@ class TestLandedCostVoucher(ERPNextTestCase): self.assertEqual(serial_no.purchase_rate - serial_no_rate, 5.0) self.assertEqual(serial_no.warehouse, "Stores - TCP1") + def test_serialized_lcv_delivered(self): + """In some cases you'd want to deliver before you can know all the + landed costs, this should be allowed for serial nos too. + + Case: + - receipt a serial no @ X rate + - delivery the serial no @ X rate + - add LCV to receipt X + Y + - LCV should be successful + - delivery should reflect X+Y valuation. + """ + serial_no = "LCV_TEST_SR_NO" + item_code = "_Test Serialized Item" + warehouse = "Stores - TCP1" + + pr = make_purchase_receipt(company="_Test Company with perpetual inventory", + warehouse=warehouse, qty=1, rate=200, + item_code=item_code, serial_no=serial_no) + + serial_no_rate = frappe.db.get_value("Serial No", serial_no, "purchase_rate") + + # deliver it before creating LCV + dn = create_delivery_note(item_code=item_code, + company='_Test Company with perpetual inventory', warehouse='Stores - TCP1', + serial_no=serial_no, qty=1, rate=500, + cost_center = 'Main - TCP1', expense_account = "Cost of Goods Sold - TCP1") + + charges = 10 + create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company, charges=charges) + + new_purchase_rate = serial_no_rate + charges + + serial_no = frappe.db.get_value("Serial No", serial_no, + ["warehouse", "purchase_rate"], as_dict=1) + + self.assertEqual(serial_no.purchase_rate, new_purchase_rate) + + stock_value_difference = frappe.db.get_value("Stock Ledger Entry", + filters={ + "voucher_no": dn.name, + "voucher_type": dn.doctype, + "is_cancelled": 0 # LCV cancels with same name. + }, + fieldname="stock_value_difference") + + # reposting should update the purchase rate in future delivery + self.assertEqual(stock_value_difference, -new_purchase_rate) def test_landed_cost_voucher_for_odd_numbers (self): pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1", do_not_save=True) diff --git a/erpnext/stock/doctype/material_request/test_material_request.py b/erpnext/stock/doctype/material_request/test_material_request.py index 383b0ae806..866f3ab2d5 100644 --- a/erpnext/stock/doctype/material_request/test_material_request.py +++ b/erpnext/stock/doctype/material_request/test_material_request.py @@ -6,6 +6,7 @@ import frappe +from frappe.tests.utils import FrappeTestCase from frappe.utils import flt, today from erpnext.stock.doctype.item.test_item import create_item @@ -15,10 +16,9 @@ from erpnext.stock.doctype.material_request.material_request import ( make_supplier_quotation, raise_work_orders, ) -from erpnext.tests.utils import ERPNextTestCase -class TestMaterialRequest(ERPNextTestCase): +class TestMaterialRequest(FrappeTestCase): def test_make_purchase_order(self): mr = frappe.copy_doc(test_records[0]).insert() @@ -626,13 +626,13 @@ class TestMaterialRequest(ERPNextTestCase): mr.schedule_date = today() if not frappe.db.get_value('UOM Conversion Detail', - {'parent': item.item_code, 'uom': 'Kg'}): - item_doc = frappe.get_doc('Item', item.item_code) - item_doc.append('uoms', { - 'uom': 'Kg', - 'conversion_factor': 5 - }) - item_doc.save(ignore_permissions=True) + {'parent': item.item_code, 'uom': 'Kg'}): + item_doc = frappe.get_doc('Item', item.item_code) + item_doc.append('uoms', { + 'uom': 'Kg', + 'conversion_factor': 5 + }) + item_doc.save(ignore_permissions=True) item.uom = 'Kg' for item in mr.items: diff --git a/erpnext/stock/doctype/packed_item/packed_item.json b/erpnext/stock/doctype/packed_item/packed_item.json index d2d4789765..d6e2e9ce2d 100644 --- a/erpnext/stock/doctype/packed_item/packed_item.json +++ b/erpnext/stock/doctype/packed_item/packed_item.json @@ -26,6 +26,7 @@ "section_break_13", "actual_qty", "projected_qty", + "ordered_qty", "column_break_16", "incoming_rate", "page_break", @@ -224,13 +225,21 @@ "label": "Rate", "print_hide": 1, "read_only": 1 + }, + { + "allow_on_submit": 1, + "fieldname": "ordered_qty", + "fieldtype": "Float", + "label": "Ordered Qty", + "no_copy": 1, + "read_only": 1 } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2022-01-28 16:03:30.780111", + "modified": "2022-02-22 12:57:45.325488", "modified_by": "Administrator", "module": "Stock", "name": "Packed Item", diff --git a/erpnext/stock/doctype/packed_item/test_packed_item.py b/erpnext/stock/doctype/packed_item/test_packed_item.py index 2521ac9fe7..94268a8ef3 100644 --- a/erpnext/stock/doctype/packed_item/test_packed_item.py +++ b/erpnext/stock/doctype/packed_item/test_packed_item.py @@ -1,6 +1,7 @@ # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt +from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import add_to_date, nowdate from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle @@ -9,10 +10,9 @@ from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_orde from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import get_gl_entries from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry -from erpnext.tests.utils import ERPNextTestCase, change_settings -class TestPackedItem(ERPNextTestCase): +class TestPackedItem(FrappeTestCase): "Test impact on Packed Items table in various scenarios." @classmethod def setUpClass(cls) -> None: diff --git a/erpnext/stock/doctype/packing_slip/test_packing_slip.py b/erpnext/stock/doctype/packing_slip/test_packing_slip.py index 5eb6b7399a..bc405b2099 100644 --- a/erpnext/stock/doctype/packing_slip/test_packing_slip.py +++ b/erpnext/stock/doctype/packing_slip/test_packing_slip.py @@ -4,7 +4,7 @@ import unittest # test_records = frappe.get_test_records('Packing Slip') -from erpnext.tests.utils import ERPNextTestCase +from frappe.tests.utils import FrappeTestCase class TestPackingSlip(unittest.TestCase): diff --git a/erpnext/stock/doctype/pick_list/test_pick_list.py b/erpnext/stock/doctype/pick_list/test_pick_list.py index 41e3150f0d..f3b6b89784 100644 --- a/erpnext/stock/doctype/pick_list/test_pick_list.py +++ b/erpnext/stock/doctype/pick_list/test_pick_list.py @@ -6,16 +6,17 @@ from frappe import _dict test_dependencies = ['Item', 'Sales Invoice', 'Stock Entry', 'Batch'] +from frappe.tests.utils import FrappeTestCase + from erpnext.stock.doctype.item.test_item import create_item from erpnext.stock.doctype.pick_list.pick_list import create_delivery_note from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import ( EmptyStockReconciliationItemsError, ) -from erpnext.tests.utils import ERPNextTestCase -class TestPickList(ERPNextTestCase): +class TestPickList(FrappeTestCase): def test_pick_list_picks_warehouse_for_each_item(self): try: diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index d481689c13..fa28f2252d 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -7,6 +7,7 @@ import unittest from collections import defaultdict import frappe +from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import add_days, cint, cstr, flt, today import erpnext @@ -17,10 +18,9 @@ from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchas from erpnext.stock.doctype.serial_no.serial_no import SerialNoDuplicateError, get_serial_nos from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse from erpnext.stock.stock_ledger import SerialNoExistsInFutureTransaction -from erpnext.tests.utils import ERPNextTestCase, change_settings -class TestPurchaseReceipt(ERPNextTestCase): +class TestPurchaseReceipt(FrappeTestCase): def setUp(self): frappe.db.set_value("Buying Settings", None, "allow_multiple_items", 1) @@ -161,6 +161,15 @@ class TestPurchaseReceipt(ERPNextTestCase): qty=abs(existing_bin_qty) ) + existing_bin_qty, existing_bin_stock_value = frappe.db.get_value( + "Bin", + { + "item_code": "_Test Item", + "warehouse": "_Test Warehouse - _TC" + }, + ["actual_qty", "stock_value"] + ) + pr = make_purchase_receipt() stock_value_difference = frappe.db.get_value( diff --git a/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py b/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py index ff1c19a827..4e8d71fe5e 100644 --- a/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py +++ b/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py @@ -2,6 +2,7 @@ # See license.txt import frappe +from frappe.tests.utils import FrappeTestCase from erpnext.stock.doctype.batch.test_batch import make_new_batch from erpnext.stock.doctype.item.test_item import make_item @@ -9,10 +10,9 @@ from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_pu from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse from erpnext.stock.get_item_details import get_conversion_factor -from erpnext.tests.utils import ERPNextTestCase -class TestPutawayRule(ERPNextTestCase): +class TestPutawayRule(FrappeTestCase): def setUp(self): if not frappe.db.exists("Item", "_Rice"): make_item("_Rice", { diff --git a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py index 308c62875d..601ca054b5 100644 --- a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py +++ b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py @@ -2,6 +2,7 @@ # See license.txt import frappe +from frappe.tests.utils import FrappeTestCase from frappe.utils import nowdate from erpnext.controllers.stock_controller import ( @@ -13,12 +14,11 @@ from erpnext.controllers.stock_controller import ( from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note from erpnext.stock.doctype.item.test_item import create_item from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry -from erpnext.tests.utils import ERPNextTestCase # test_records = frappe.get_test_records('Quality Inspection') -class TestQualityInspection(ERPNextTestCase): +class TestQualityInspection(FrappeTestCase): def setUp(self): super().setUp() create_item("_Test Item with QA") diff --git a/erpnext/stock/doctype/serial_no/test_serial_no.py b/erpnext/stock/doctype/serial_no/test_serial_no.py index f8cea71725..057a7d4c01 100644 --- a/erpnext/stock/doctype/serial_no/test_serial_no.py +++ b/erpnext/stock/doctype/serial_no/test_serial_no.py @@ -18,11 +18,12 @@ from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse test_dependencies = ["Item"] test_records = frappe.get_test_records('Serial No') +from frappe.tests.utils import FrappeTestCase + from erpnext.stock.doctype.serial_no.serial_no import * -from erpnext.tests.utils import ERPNextTestCase -class TestSerialNo(ERPNextTestCase): +class TestSerialNo(FrappeTestCase): def tearDown(self): frappe.db.rollback() diff --git a/erpnext/stock/doctype/shipment/test_shipment.py b/erpnext/stock/doctype/shipment/test_shipment.py index afe821845a..317abb6d03 100644 --- a/erpnext/stock/doctype/shipment/test_shipment.py +++ b/erpnext/stock/doctype/shipment/test_shipment.py @@ -4,12 +4,12 @@ from datetime import date, timedelta import frappe +from frappe.tests.utils import FrappeTestCase from erpnext.stock.doctype.delivery_note.delivery_note import make_shipment -from erpnext.tests.utils import ERPNextTestCase -class TestShipment(ERPNextTestCase): +class TestShipment(FrappeTestCase): def test_shipment_from_delivery_note(self): delivery_note = create_test_delivery_note() delivery_note.submit() diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 5c9da3a205..324ca7ac59 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -629,6 +629,12 @@ frappe.ui.form.on('Stock Entry Detail', { frm.events.set_serial_no(frm, cdt, cdn, () => { frm.events.get_warehouse_details(frm, cdt, cdn); }); + + // set allow_zero_valuation_rate to 0 if s_warehouse is selected. + let item = frappe.get_doc(cdt, cdn); + if (item.s_warehouse) { + item.allow_zero_valuation_rate = 0; + } }, t_warehouse: function(frm, cdt, cdn) { diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 6c6513beff..54c0e43c5e 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -6,6 +6,7 @@ import unittest import frappe from frappe.permissions import add_user_permission, remove_user_permission +from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import flt, nowdate, nowtime from erpnext.accounts.doctype.account.test_account import get_inventory_account @@ -28,7 +29,6 @@ from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import create_stock_reconciliation, ) from erpnext.stock.stock_ledger import NegativeStockError, get_previous_sle -from erpnext.tests.utils import ERPNextTestCase, change_settings def get_sle(**args): @@ -42,8 +42,9 @@ def get_sle(**args): order by timestamp(posting_date, posting_time) desc, creation desc limit 1"""% condition, values, as_dict=1) -class TestStockEntry(ERPNextTestCase): +class TestStockEntry(FrappeTestCase): def tearDown(self): + frappe.db.rollback() frappe.set_user("Administrator") frappe.db.set_value("Manufacturing Settings", None, "material_consumption", "0") @@ -565,6 +566,7 @@ class TestStockEntry(ERPNextTestCase): st1.set_stock_entry_type() st1.insert() st1.submit() + st1.cancel() frappe.set_user("Administrator") remove_user_permission("Warehouse", "_Test Warehouse 1 - _TC", "test@example.com") @@ -689,6 +691,8 @@ class TestStockEntry(ERPNextTestCase): bom_no = frappe.db.get_value("BOM", {"item": "_Test Variant Item", "is_default": 1, "docstatus": 1}) + make_item_variant() # make variant of _Test Variant Item if absent + work_order = frappe.new_doc("Work Order") work_order.update({ "company": "_Test Company", @@ -1023,13 +1027,10 @@ class TestStockEntry(ERPNextTestCase): # Check if FG cost is calculated based on RM total cost # RM total cost = 200, FG rate = 200/4(FG qty) = 50 - self.assertEqual(se.items[1].basic_rate, 50) + self.assertEqual(se.items[1].basic_rate, flt(se.items[0].basic_rate/4)) self.assertEqual(se.value_difference, 0.0) self.assertEqual(se.total_incoming_value, se.total_outgoing_value) - # teardown - se.delete() - @change_settings("Stock Settings", {"allow_negative_stock": 0}) def test_future_negative_sle(self): # Initialize item, batch, warehouse, opening qty diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json index df65706c39..83aed904dd 100644 --- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json +++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json @@ -1,7 +1,7 @@ { "actions": [], "autoname": "hash", - "creation": "2013-03-29 18:22:12", + "creation": "2022-02-05 00:17:49.860824", "doctype": "DocType", "document_type": "Other", "editable_grid": 1, @@ -340,13 +340,13 @@ "label": "More Information" }, { - "allow_on_submit": 1, "default": "0", "fieldname": "allow_zero_valuation_rate", "fieldtype": "Check", "label": "Allow Zero Valuation Rate", "no_copy": 1, - "print_hide": 1 + "print_hide": 1, + "read_only_depends_on": "eval:doc.s_warehouse" }, { "allow_on_submit": 1, @@ -556,12 +556,14 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-06-22 16:47:11.268975", + "modified": "2022-02-26 00:51:24.963653", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry Detail", + "naming_rule": "Random", "owner": "Administrator", "permissions": [], "sort_field": "modified", - "sort_order": "ASC" + "sort_order": "ASC", + "states": [] } \ No newline at end of file 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 0864ece995..684a8d4d7c 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 @@ -7,6 +7,7 @@ from uuid import uuid4 import frappe from frappe.core.page.permission_manager.permission_manager import reset +from frappe.tests.utils import FrappeTestCase from frappe.utils import add_days, today from erpnext.stock.doctype.delivery_note.test_delivery_note import ( @@ -24,10 +25,9 @@ from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import create_stock_reconciliation, ) from erpnext.stock.stock_ledger import get_previous_sle -from erpnext.tests.utils import ERPNextTestCase -class TestStockLedgerEntry(ERPNextTestCase): +class TestStockLedgerEntry(FrappeTestCase): def setUp(self): items = create_items() reset('Stock Entry') @@ -389,10 +389,13 @@ class TestStockLedgerEntry(ERPNextTestCase): ) - def assertSLEs(self, doc, expected_sles): + def assertSLEs(self, doc, expected_sles, sle_filters=None): """ Compare sorted SLEs, useful for vouchers that create multiple SLEs for same line""" - sles = frappe.get_all("Stock Ledger Entry", fields=["*"], - filters={"voucher_no": doc.name, "voucher_type": doc.doctype, "is_cancelled":0}, + + filters = {"voucher_no": doc.name, "voucher_type": doc.doctype, "is_cancelled": 0} + if sle_filters: + filters.update(sle_filters) + sles = frappe.get_all("Stock Ledger Entry", fields=["*"], filters=filters, order_by="timestamp(posting_date, posting_time), creation") for exp_sle, act_sle in zip(expected_sles, sles): @@ -665,6 +668,78 @@ class TestStockLedgerEntry(ERPNextTestCase): {"actual_qty": -10, "stock_value_difference": -10*40, "stock_queue": []}, ])) + def test_fifo_dependent_consumption(self): + item = make_item("_TestFifoTransferRates") + source = "_Test Warehouse - _TC" + target = "Stores - _TC" + + rates = [10 * i for i in range(1, 20)] + + receipt = make_stock_entry(item_code=item.name, target=source, qty=10, do_not_save=True, rate=10) + for rate in rates[1:]: + row = frappe.copy_doc(receipt.items[0], ignore_no_copy=False) + row.basic_rate = rate + receipt.append("items", row) + + receipt.save() + receipt.submit() + + expected_queues = [] + for idx, rate in enumerate(rates, start=1): + expected_queues.append( + {"stock_queue": [[10, 10 * i] for i in range(1, idx + 1)]} + ) + self.assertSLEs(receipt, expected_queues) + + transfer = make_stock_entry(item_code=item.name, source=source, target=target, qty=10, do_not_save=True, rate=10) + for rate in rates[1:]: + row = frappe.copy_doc(transfer.items[0], ignore_no_copy=False) + transfer.append("items", row) + + transfer.save() + transfer.submit() + + # same exact queue should be transferred + self.assertSLEs(transfer, expected_queues, sle_filters={"warehouse": target}) + + def test_fifo_multi_item_repack_consumption(self): + rm = make_item("_TestFifoRepackRM") + packed = make_item("_TestFifoRepackFinished") + warehouse = "_Test Warehouse - _TC" + + rates = [10 * i for i in range(1, 5)] + + receipt = make_stock_entry(item_code=rm.name, target=warehouse, qty=10, do_not_save=True, rate=10) + for rate in rates[1:]: + row = frappe.copy_doc(receipt.items[0], ignore_no_copy=False) + row.basic_rate = rate + receipt.append("items", row) + + receipt.save() + receipt.submit() + + repack = make_stock_entry(item_code=rm.name, source=warehouse, qty=10, + do_not_save=True, rate=10, purpose="Repack") + for rate in rates[1:]: + row = frappe.copy_doc(repack.items[0], ignore_no_copy=False) + repack.append("items", row) + + repack.append("items", { + "item_code": packed.name, + "t_warehouse": warehouse, + "qty": 1, + "transfer_qty": 1, + }) + + repack.save() + repack.submit() + + # same exact queue should be transferred + self.assertSLEs(repack, [ + {"incoming_rate": sum(rates) * 10} + ], sle_filters={"item_code": packed.name}) + + def create_repack_entry(**args): args = frappe._dict(args) repack = frappe.new_doc("Stock Entry") diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index 2ffe127d9a..e6b252e856 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -6,6 +6,7 @@ import frappe +from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import add_days, cstr, flt, nowdate, nowtime, random_string from erpnext.accounts.utils import get_stock_and_account_balance @@ -19,10 +20,9 @@ from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import ( from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse from erpnext.stock.stock_ledger import get_previous_sle, update_entries_after from erpnext.stock.utils import get_incoming_rate, get_stock_value_on, get_valuation_method -from erpnext.tests.utils import ERPNextTestCase, change_settings -class TestStockReconciliation(ERPNextTestCase): +class TestStockReconciliation(FrappeTestCase): @classmethod def setUpClass(cls): create_batch_or_serial_no_items() diff --git a/erpnext/stock/doctype/stock_settings/test_stock_settings.py b/erpnext/stock/doctype/stock_settings/test_stock_settings.py index 072b54b820..13496718ea 100644 --- a/erpnext/stock/doctype/stock_settings/test_stock_settings.py +++ b/erpnext/stock/doctype/stock_settings/test_stock_settings.py @@ -4,11 +4,10 @@ import unittest import frappe - -from erpnext.tests.utils import ERPNextTestCase +from frappe.tests.utils import FrappeTestCase -class TestStockSettings(ERPNextTestCase): +class TestStockSettings(FrappeTestCase): def setUp(self): super().setUp() frappe.db.set_value("Stock Settings", None, "clean_description_html", 0) diff --git a/erpnext/stock/doctype/warehouse/test_warehouse.py b/erpnext/stock/doctype/warehouse/test_warehouse.py index 26db2642e4..cdb771935b 100644 --- a/erpnext/stock/doctype/warehouse/test_warehouse.py +++ b/erpnext/stock/doctype/warehouse/test_warehouse.py @@ -3,17 +3,17 @@ import frappe from frappe.test_runner import make_test_records +from frappe.tests.utils import FrappeTestCase from frappe.utils import cint import erpnext from erpnext.accounts.doctype.account.test_account import create_account, get_inventory_account from erpnext.stock.doctype.item.test_item import create_item from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry -from erpnext.tests.utils import ERPNextTestCase test_records = frappe.get_test_records('Warehouse') -class TestWarehouse(ERPNextTestCase): +class TestWarehouse(FrappeTestCase): def setUp(self): super().setUp() if not frappe.get_value('Item', '_Test Item'): diff --git a/erpnext/stock/doctype/warehouse/warehouse.json b/erpnext/stock/doctype/warehouse/warehouse.json index 05076b51a3..c695d541bf 100644 --- a/erpnext/stock/doctype/warehouse/warehouse.json +++ b/erpnext/stock/doctype/warehouse/warehouse.json @@ -244,7 +244,7 @@ "idx": 1, "is_tree": 1, "links": [], - "modified": "2021-12-03 04:40:06.414630", + "modified": "2022-03-01 02:37:48.034944", "modified_by": "Administrator", "module": "Stock", "name": "Warehouse", @@ -301,5 +301,7 @@ "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "DESC", - "title_field": "warehouse_name" + "states": [], + "title_field": "warehouse_name", + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/stock/report/stock_ageing/test_stock_ageing.py b/erpnext/stock/report/stock_ageing/test_stock_ageing.py index 3fc357e8d4..ca963b7486 100644 --- a/erpnext/stock/report/stock_ageing/test_stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/test_stock_ageing.py @@ -1,13 +1,13 @@ -# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt import frappe +from frappe.tests.utils import FrappeTestCase from erpnext.stock.report.stock_ageing.stock_ageing import FIFOSlots, format_report_data -from erpnext.tests.utils import ERPNextTestCase -class TestStockAgeing(ERPNextTestCase): +class TestStockAgeing(FrappeTestCase): def setUp(self) -> None: self.filters = frappe._dict( company="_Test Company", @@ -610,4 +610,4 @@ def generate_item_and_item_wh_wise_slots(filters, sle): item_wh_wise_slots = FIFOSlots(filters, sle).generate() filters.show_warehouse_wise_stock = False - return item_wise_slots, item_wh_wise_slots \ No newline at end of file + return item_wise_slots, item_wh_wise_slots diff --git a/erpnext/stock/report/stock_analytics/test_stock_analytics.py b/erpnext/stock/report/stock_analytics/test_stock_analytics.py index 32df585937..f6c98f914d 100644 --- a/erpnext/stock/report/stock_analytics/test_stock_analytics.py +++ b/erpnext/stock/report/stock_analytics/test_stock_analytics.py @@ -1,14 +1,13 @@ import datetime -import unittest from frappe import _dict +from frappe.tests.utils import FrappeTestCase from erpnext.accounts.utils import get_fiscal_year from erpnext.stock.report.stock_analytics.stock_analytics import get_period_date_ranges -from erpnext.tests.utils import ERPNextTestCase -class TestStockAnalyticsReport(ERPNextTestCase): +class TestStockAnalyticsReport(FrappeTestCase): def test_get_period_date_ranges(self): filters = _dict(range="Monthly", from_date="2020-12-28", to_date="2021-02-06") diff --git a/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py index 7826d34422..1ba2482935 100644 --- a/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py +++ b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py @@ -21,6 +21,7 @@ SLE_FIELDS = ( "stock_value", "stock_value_difference", "valuation_rate", + "voucher_detail_no", ) @@ -66,7 +67,9 @@ def add_invariant_check_fields(sles): balance_qty += sle.actual_qty balance_stock_value += sle.stock_value_difference if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no: - balance_qty = sle.qty_after_transaction + balance_qty = frappe.db.get_value("Stock Reconciliation Item", sle.voucher_detail_no, "qty") + if balance_qty is None: + balance_qty = sle.qty_after_transaction sle.fifo_queue_qty = fifo_qty sle.fifo_stock_value = fifo_value diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 1b90086440..ba1081f4dc 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -28,6 +28,16 @@ class SerialNoExistsInFutureTransaction(frappe.ValidationError): def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False): + """ Create SL entries from SL entry dicts + + args: + - allow_negative_stock: disable negative stock valiations if true + - via_landed_cost_voucher: landed cost voucher cancels and reposts + entries of purchase document. This flag is used to identify if + cancellation and repost is happening via landed cost voucher, in + such cases certain validations need to be ignored (like negative + stock) + """ from erpnext.controllers.stock_controller import future_sle_exists if sl_entries: cancel = sl_entries[0].get("is_cancelled") @@ -39,7 +49,7 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc future_sle_exists(args, sl_entries) for sle in sl_entries: - if sle.serial_no: + if sle.serial_no and not via_landed_cost_voucher: validate_serial_no(sle) if cancel: @@ -819,7 +829,7 @@ class update_entries_after(object): if msg_list: message = "\n\n".join(msg_list) if self.verbose: - frappe.throw(message, NegativeStockError, title='Insufficient Stock') + frappe.throw(message, NegativeStockError, title=_('Insufficient Stock')) else: raise NegativeStockError(message) @@ -1147,7 +1157,7 @@ def validate_negative_qty_in_future_sle(args, allow_negative_stock=False): neg_sle[0]["posting_date"], neg_sle[0]["posting_time"], frappe.get_desk_link(neg_sle[0]["voucher_type"], neg_sle[0]["voucher_no"])) - frappe.throw(message, NegativeStockError, title='Insufficient Stock') + frappe.throw(message, NegativeStockError, title=_('Insufficient Stock')) if not args.batch_no: @@ -1161,7 +1171,7 @@ def validate_negative_qty_in_future_sle(args, allow_negative_stock=False): frappe.get_desk_link('Warehouse', args.warehouse), neg_batch_sle[0]["posting_date"], neg_batch_sle[0]["posting_time"], frappe.get_desk_link(neg_batch_sle[0]["voucher_type"], neg_batch_sle[0]["voucher_no"])) - frappe.throw(message, NegativeStockError, title="Insufficient Stock for Batch") + frappe.throw(message, NegativeStockError, title=_("Insufficient Stock for Batch")) def get_future_sle_with_negative_qty(args): diff --git a/erpnext/stock/tests/test_valuation.py b/erpnext/stock/tests/test_valuation.py index bdb768f1ad..b64ff8e28c 100644 --- a/erpnext/stock/tests/test_valuation.py +++ b/erpnext/stock/tests/test_valuation.py @@ -2,13 +2,13 @@ import json import unittest import frappe +from frappe.tests.utils import FrappeTestCase from hypothesis import given from hypothesis import strategies as st from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.valuation import FIFOValuation, LIFOValuation, round_off_if_near_zero -from erpnext.tests.utils import ERPNextTestCase qty_gen = st.floats(min_value=-1e6, max_value=1e6) value_gen = st.floats(min_value=1, max_value=1e6) @@ -290,7 +290,7 @@ class TestLIFOValuation(unittest.TestCase): self.assertTotalQty(total_qty) self.assertTotalValue(total_value) -class TestLIFOValuationSLE(ERPNextTestCase): +class TestLIFOValuationSLE(FrappeTestCase): ITEM_CODE = "_Test LIFO item" WAREHOUSE = "_Test Warehouse - _TC" diff --git a/erpnext/templates/includes/footer/footer_powered.html b/erpnext/templates/includes/footer/footer_powered.html index 82b2716a92..faf5e9278c 100644 --- a/erpnext/templates/includes/footer/footer_powered.html +++ b/erpnext/templates/includes/footer/footer_powered.html @@ -1,27 +1 @@ -{% set domains = frappe.get_doc("Domain Settings").active_domains %} -{% set links = { - 'Manufacturing': '/manufacturing', - 'Services': '/services', - 'Retail': '/retail', - 'Distribution': '/distribution', - 'Non Profit': '/non-profit', - 'Education': '/education', - 'Healthcare': '/healthcare', - 'Agriculture': '/agriculture', - 'Hospitality': '' -} %} - -{% set link = '' %} -{% set label = '' %} -{% if domains %} - {% set label = domains[0].domain %} - {% set link = links[label] %} -{% endif %} - -{% if label == "Services" %} - {% set label = "Service" %} -{% endif %} - - - -Powered by ERPNext - {{ '' if domains else 'Open Source' }} ERP Software {{ ('for ' + label + ' Companies') if domains else '' }} +Powered by ERPNext diff --git a/erpnext/templates/includes/navbar/navbar_items.html b/erpnext/templates/includes/navbar/navbar_items.html index 327552117b..d7adae562e 100644 --- a/erpnext/templates/includes/navbar/navbar_items.html +++ b/erpnext/templates/includes/navbar/navbar_items.html @@ -13,7 +13,7 @@