diff --git a/CODEOWNERS b/CODEOWNERS index 7f8c4d1ac87..713941e589c 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -4,22 +4,22 @@ # the repo. Unless a later match takes precedence, erpnext/accounts/ @deepeshgarg007 @ruthra-kumar -erpnext/assets/ @anandbaburajan @deepeshgarg007 +erpnext/assets/ @khushi8112 @deepeshgarg007 erpnext/loan_management/ @deepeshgarg007 erpnext/regional @deepeshgarg007 @ruthra-kumar erpnext/selling @deepeshgarg007 @ruthra-kumar erpnext/support/ @deepeshgarg007 pos* -erpnext/buying/ @rohitwaghchaure @s-aga-r -erpnext/maintenance/ @rohitwaghchaure @s-aga-r -erpnext/manufacturing/ @rohitwaghchaure @s-aga-r -erpnext/quality_management/ @rohitwaghchaure @s-aga-r -erpnext/stock/ @rohitwaghchaure @s-aga-r -erpnext/subcontracting @rohitwaghchaure @s-aga-r +erpnext/buying/ @rohitwaghchaure +erpnext/maintenance/ @rohitwaghchaure +erpnext/manufacturing/ @rohitwaghchaure +erpnext/quality_management/ @rohitwaghchaure +erpnext/stock/ @rohitwaghchaure +erpnext/subcontracting @rohitwaghchaure erpnext/controllers/ @deepeshgarg007 @rohitwaghchaure erpnext/patches/ @deepeshgarg007 .github/ @deepeshgarg007 -pyproject.toml @ankush +pyproject.toml @akhilnarang diff --git a/erpnext/accounts/report/sales_register/test_sales_register.py b/erpnext/accounts/report/sales_register/test_sales_register.py new file mode 100644 index 00000000000..95aa5add24c --- /dev/null +++ b/erpnext/accounts/report/sales_register/test_sales_register.py @@ -0,0 +1,179 @@ +import frappe +from frappe.tests.utils import FrappeTestCase +from frappe.utils import getdate, today + +from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice +from erpnext.accounts.report.sales_register.sales_register import execute +from erpnext.accounts.test.accounts_mixin import AccountsTestMixin + + +class TestItemWiseSalesRegister(AccountsTestMixin, FrappeTestCase): + def setUp(self): + self.create_company() + self.create_customer() + self.create_item() + self.create_child_cost_center() + + def tearDown(self): + frappe.db.rollback() + + def create_child_cost_center(self): + cc_name = "South Wing" + if frappe.db.exists("Cost Center", cc_name): + cc = frappe.get_doc("Cost Center", cc_name) + else: + parent = frappe.db.get_value("Cost Center", self.cost_center, "parent_cost_center") + cc = frappe.get_doc( + { + "doctype": "Cost Center", + "company": self.company, + "is_group": False, + "parent_cost_center": parent, + "cost_center_name": cc_name, + } + ) + cc = cc.save() + self.south_cc = cc.name + + def create_sales_invoice(self, rate=100, do_not_submit=False): + si = create_sales_invoice( + item=self.item, + company=self.company, + customer=self.customer, + debit_to=self.debit_to, + posting_date=today(), + parent_cost_center=self.cost_center, + cost_center=self.cost_center, + rate=rate, + price_list_rate=rate, + do_not_save=1, + ) + si = si.save() + if not do_not_submit: + si = si.submit() + return si + + def test_basic_report_output(self): + si = self.create_sales_invoice(rate=98) + + filters = frappe._dict({"from_date": today(), "to_date": today(), "company": self.company}) + report = execute(filters) + + res = [x for x in report[1] if x.get("voucher_no") == si.name] + + expected_result = { + "voucher_type": si.doctype, + "voucher_no": si.name, + "posting_date": getdate(), + "customer": self.customer, + "receivable_account": self.debit_to, + "net_total": 98.0, + "grand_total": 98.0, + "debit": 98.0, + } + + report_output = {k: v for k, v in res[0].items() if k in expected_result} + self.assertDictEqual(report_output, expected_result) + + def test_journal_with_cost_center_filter(self): + je1 = frappe.get_doc( + { + "doctype": "Journal Entry", + "voucher_type": "Journal Entry", + "company": self.company, + "posting_date": getdate(), + "accounts": [ + { + "account": self.debit_to, + "party_type": "Customer", + "party": self.customer, + "credit_in_account_currency": 77, + "credit": 77, + "is_advance": "Yes", + "cost_center": self.cost_center, + }, + { + "account": self.cash, + "debit_in_account_currency": 77, + "debit": 77, + }, + ], + } + ) + je1.submit() + + je2 = frappe.get_doc( + { + "doctype": "Journal Entry", + "voucher_type": "Journal Entry", + "company": self.company, + "posting_date": getdate(), + "accounts": [ + { + "account": self.debit_to, + "party_type": "Customer", + "party": self.customer, + "credit_in_account_currency": 98, + "credit": 98, + "is_advance": "Yes", + "cost_center": self.south_cc, + }, + { + "account": self.cash, + "debit_in_account_currency": 98, + "debit": 98, + }, + ], + } + ) + je2.submit() + + filters = frappe._dict( + { + "from_date": today(), + "to_date": today(), + "company": self.company, + "include_payments": True, + "customer": self.customer, + "cost_center": self.cost_center, + } + ) + report_output = execute(filters)[1] + filtered_output = [x for x in report_output if x.get("voucher_no") == je1.name] + self.assertEqual(len(filtered_output), 1) + expected_result = { + "voucher_type": je1.doctype, + "voucher_no": je1.name, + "posting_date": je1.posting_date, + "customer": self.customer, + "receivable_account": self.debit_to, + "net_total": 77.0, + "credit": 77.0, + } + result_fields = {k: v for k, v in filtered_output[0].items() if k in expected_result} + self.assertDictEqual(result_fields, expected_result) + + filters = frappe._dict( + { + "from_date": today(), + "to_date": today(), + "company": self.company, + "include_payments": True, + "customer": self.customer, + "cost_center": self.south_cc, + } + ) + report_output = execute(filters)[1] + filtered_output = [x for x in report_output if x.get("voucher_no") == je2.name] + self.assertEqual(len(filtered_output), 1) + expected_result = { + "voucher_type": je2.doctype, + "voucher_no": je2.name, + "posting_date": je2.posting_date, + "customer": self.customer, + "receivable_account": self.debit_to, + "net_total": 98.0, + "credit": 98.0, + } + result_output = {k: v for k, v in filtered_output[0].items() if k in expected_result} + self.assertDictEqual(result_output, expected_result) diff --git a/erpnext/accounts/report/utils.py b/erpnext/accounts/report/utils.py index 8ecabaf73ec..5d606f648fa 100644 --- a/erpnext/accounts/report/utils.py +++ b/erpnext/accounts/report/utils.py @@ -255,7 +255,9 @@ def get_journal_entries(filters, args): ) .orderby(je.posting_date, je.name, order=Order.desc) ) - query = apply_common_conditions(filters, query, doctype="Journal Entry", payments=True) + query = apply_common_conditions( + filters, query, doctype="Journal Entry", child_doctype="Journal Entry Account", payments=True + ) journal_entries = query.run(as_dict=True) return journal_entries @@ -306,7 +308,9 @@ def apply_common_conditions(filters, query, doctype, child_doctype=None, payment query = query.where(parent_doc.posting_date <= filters.to_date) if payments: - if filters.get("cost_center"): + if doctype == "Journal Entry" and filters.get("cost_center"): + query = query.where(child_doc.cost_center == filters.cost_center) + elif filters.get("cost_center"): query = query.where(parent_doc.cost_center == filters.cost_center) else: if filters.get("cost_center"): diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 705e8fd6dd8..a828c117edb 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -456,9 +456,11 @@ class PurchaseOrder(BuyingController): if not self.is_against_so(): return for item in removed_items: - prev_ordered_qty = frappe.get_cached_value( - "Sales Order Item", item.get("sales_order_item"), "ordered_qty" + prev_ordered_qty = ( + frappe.get_cached_value("Sales Order Item", item.get("sales_order_item"), "ordered_qty") + or 0.0 ) + frappe.db.set_value( "Sales Order Item", item.get("sales_order_item"), "ordered_qty", prev_ordered_qty - item.qty ) diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py index dd3fab05ce6..e04618c857f 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -91,7 +91,8 @@ status_map = { ], "Purchase Receipt": [ ["Draft", None], - ["To Bill", "eval:self.per_billed < 100 and self.docstatus == 1"], + ["To Bill", "eval:self.per_billed == 0 and self.docstatus == 1"], + ["Partly Billed", "eval:self.per_billed > 0 and self.per_billed < 100 and self.docstatus == 1"], ["Return Issued", "eval:self.per_returned == 100 and self.docstatus == 1"], ["Completed", "eval:self.per_billed == 100 and self.docstatus == 1"], ["Cancelled", "eval:self.docstatus==2"], diff --git a/erpnext/projects/doctype/task/task.py b/erpnext/projects/doctype/task/task.py index b051494a651..481c64a39c0 100755 --- a/erpnext/projects/doctype/task/task.py +++ b/erpnext/projects/doctype/task/task.py @@ -118,14 +118,14 @@ class Task(NestedSet): def validate_parent_template_task(self): if self.parent_task: if not frappe.db.get_value("Task", self.parent_task, "is_template"): - parent_task_format = f"""{self.parent_task}""" + parent_task_format = f"""{self.parent_task}""" frappe.throw(_("Parent Task {0} is not a Template Task").format(parent_task_format)) def validate_depends_on_tasks(self): if self.depends_on: for task in self.depends_on: if not frappe.db.get_value("Task", task.task, "is_template"): - dependent_task_format = f"""{task.task}""" + dependent_task_format = f"""{task.task}""" frappe.throw(_("Dependent Task {0} is not a Template Task").format(dependent_task_format)) def validate_completed_on(self): diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 79e14b9eb5b..c3ec62c34da 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -822,9 +822,14 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe if (frappe.meta.get_docfield(this.frm.doctype, "shipping_address") && ['Purchase Order', 'Purchase Receipt', 'Purchase Invoice'].includes(this.frm.doctype)) { - erpnext.utils.get_shipping_address(this.frm, function() { - set_party_account(set_pricing); - }); + let is_drop_ship = me.frm.doc.items.some(item => item.delivered_by_supplier); + + if (!is_drop_ship) { + console.log('get_shipping_address'); + erpnext.utils.get_shipping_address(this.frm, function() { + set_party_account(set_pricing); + }); + } } else { set_party_account(set_pricing); @@ -2213,7 +2218,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe payment_terms_template() { var me = this; const doc = this.frm.doc; - if(doc.payment_terms_template && doc.doctype !== 'Delivery Note' && doc.is_return == 0) { + if(doc.payment_terms_template && doc.doctype !== 'Delivery Note' && !doc.is_return) { var posting_date = doc.posting_date || doc.transaction_date; frappe.call({ method: "erpnext.controllers.accounts_controller.get_payment_terms", diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 293066cde38..34f839b563d 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -1035,6 +1035,8 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=None, t "discount_percentage", "discount_amount", "pricing_rules", + "margin_type", + "margin_rate_or_amount", ], "postprocess": update_item, "condition": lambda doc: doc.ordered_qty < doc.stock_qty @@ -1088,9 +1090,17 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None): target.payment_schedule = [] if is_drop_ship_order(target): - target.customer = source.customer - target.customer_name = source.customer_name - target.shipping_address = source.shipping_address_name + if source.shipping_address_name: + target.shipping_address = source.shipping_address_name + target.shipping_address_display = source.shipping_address + else: + target.shipping_address = source.customer_address + target.shipping_address_display = source.address_display + + target.customer_contact_person = source.contact_person + target.customer_contact_display = source.contact_display + target.customer_contact_mobile = source.contact_mobile + target.customer_contact_email = source.contact_email else: target.customer = target.customer_name = target.shipping_address = None diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index aa25d625ffe..948437a8287 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -61,7 +61,7 @@ class PickList(Document): "actual_qty", ) - if row.qty > bin_qty: + if row.qty > flt(bin_qty): frappe.throw( _( "At Row #{0}: The picked quantity {1} for the item {2} is greater than available stock {3} in the warehouse {4}." diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json index 28c54dab9b2..fabeb74462f 100755 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json @@ -885,7 +885,7 @@ "no_copy": 1, "oldfieldname": "status", "oldfieldtype": "Select", - "options": "\nDraft\nTo Bill\nCompleted\nReturn Issued\nCancelled\nClosed", + "options": "\nDraft\nPartly Billed\nTo Bill\nCompleted\nReturn Issued\nCancelled\nClosed", "print_hide": 1, "print_width": "150px", "read_only": 1, @@ -1242,7 +1242,7 @@ "idx": 261, "is_submittable": 1, "links": [], - "modified": "2024-03-20 16:05:31.713453", + "modified": "2024-11-13 16:55:14.129055", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt", diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 8ed59f452e2..faf305cfe9c 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -883,6 +883,8 @@ def get_billed_amount_against_po(po_items): def update_billing_percentage(pr_doc, update_modified=True, adjust_incoming_rate=False): # Update Billing % based on pending accepted qty + buying_settings = frappe.get_single("Buying Settings") + total_amount, total_billed_amount = 0, 0 item_wise_returned_qty = get_item_wise_returned_qty(pr_doc) @@ -890,10 +892,15 @@ def update_billing_percentage(pr_doc, update_modified=True, adjust_incoming_rate returned_qty = flt(item_wise_returned_qty.get(item.name)) returned_amount = flt(returned_qty) * flt(item.rate) pending_amount = flt(item.amount) - returned_amount - total_billable_amount = pending_amount if item.billed_amt <= pending_amount else item.billed_amt + if buying_settings.bill_for_rejected_quantity_in_purchase_invoice: + pending_amount = flt(item.amount) + + total_billable_amount = abs(flt(item.amount)) + if pending_amount > 0: + total_billable_amount = pending_amount if item.billed_amt <= pending_amount else item.billed_amt total_amount += total_billable_amount - total_billed_amount += flt(item.billed_amt) + total_billed_amount += abs(flt(item.billed_amt)) if pr_doc.get("is_return") and not total_amount and total_billed_amount: total_amount = total_billed_amount diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt_list.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt_list.js index ddc9bb026fb..fc4aabdaa18 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt_list.js +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt_list.js @@ -17,8 +17,10 @@ frappe.listview_settings["Purchase Receipt"] = { return [__("Closed"), "green", "status,=,Closed"]; } else if (flt(doc.per_returned, 2) === 100) { return [__("Return Issued"), "grey", "per_returned,=,100"]; - } else if (flt(doc.grand_total) !== 0 && flt(doc.per_billed, 2) < 100) { + } else if (flt(doc.grand_total) !== 0 && flt(doc.per_billed, 2) == 0) { return [__("To Bill"), "orange", "per_billed,<,100"]; + } else if (flt(doc.per_billed, 2) > 0 && flt(doc.per_billed, 2) < 100) { + return [__("Partly Billed"), "yellow", "per_billed,<,100"]; } else if (flt(doc.grand_total) === 0 || flt(doc.per_billed, 2) === 100) { return [__("Completed"), "green", "per_billed,=,100"]; } diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 1c4feedbcfb..adb6e690596 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -595,7 +595,7 @@ class TestPurchaseReceipt(FrappeTestCase): pr2.load_from_db() self.assertEqual(pr2.get("items")[0].billed_amt, 2000) self.assertEqual(pr2.per_billed, 80) - self.assertEqual(pr2.status, "To Bill") + self.assertEqual(pr2.status, "Partly Billed") pr2.cancel() pi2.reload() @@ -1006,7 +1006,7 @@ class TestPurchaseReceipt(FrappeTestCase): pi.load_from_db() pr.load_from_db() - self.assertEqual(pr.status, "To Bill") + self.assertEqual(pr.status, "Partly Billed") self.assertAlmostEqual(pr.per_billed, 50.0, places=2) def test_purchase_receipt_with_exchange_rate_difference(self): @@ -2683,6 +2683,54 @@ class TestPurchaseReceipt(FrappeTestCase): self.assertEqual(pr.items[0].conversion_factor, 1.0) + def test_purchase_return_partial_debit_note(self): + pr = make_purchase_receipt( + company="_Test Company with perpetual inventory", + warehouse="Stores - TCP1", + supplier_warehouse="Work In Progress - TCP1", + ) + + return_pr = make_purchase_receipt( + company="_Test Company with perpetual inventory", + warehouse="Stores - TCP1", + supplier_warehouse="Work In Progress - TCP1", + is_return=1, + return_against=pr.name, + qty=-2, + do_not_submit=1, + ) + return_pr.items[0].purchase_receipt_item = pr.items[0].name + return_pr.submit() + + # because new_doc isn't considering is_return portion of status_updater + returned = frappe.get_doc("Purchase Receipt", return_pr.name) + returned.update_prevdoc_status() + pr.load_from_db() + + # Check if Original PR updated + self.assertEqual(pr.items[0].returned_qty, 2) + self.assertEqual(pr.per_returned, 40) + + # Create first partial debit_note + pi_1 = make_purchase_invoice(return_pr.name) + pi_1.items[0].qty = -1 + pi_1.submit() + + # Check if the first partial debit billing percentage got updated + return_pr.reload() + self.assertEqual(return_pr.per_billed, 50) + self.assertEqual(return_pr.status, "Partly Billed") + + # Create second partial debit_note to complete the debit note + pi_2 = make_purchase_invoice(return_pr.name) + pi_2.items[0].qty = -1 + pi_2.submit() + + # Check if the second partial debit note billing percentage got updated + return_pr.reload() + self.assertEqual(return_pr.per_billed, 100) + self.assertEqual(return_pr.status, "Completed") + def prepare_data_for_internal_transfer(): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier