mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-21 10:19:57 +00:00
Removes the duplication left over from adding the doctypes one at a time. `is_bundle_of_closed_row` was copied between the Sales Order and Delivery Note mappers, differing only in a doctype name; it now derives that from the packed item's parenttype. `is_item_closable` was identical on Delivery Note and Purchase Receipt and repeated the billing clause on the order doctypes; billing is now the default on AccountsController and the orders add their own fulfilment axis. The close dialog config was written out per doctype, differing by a qty field, a column label and a sentence, and is now two builders in erpnext/public/js/utils/item_close.js. Also stops counting closed rows as committed spend in the budget's ordered amount, which sums per row but only guarded the parent status. When every row is closed there is nothing left to measure against, so the percentage falls back to the whole table and reports what actually happened. Writing off two unbilled rows leaves per_billed at 0; writing off two rows that were fully received leaves per_received at 100 and per_billed at 0. A constant would have been wrong in one direction or the other.
180 lines
5.7 KiB
Python
180 lines
5.7 KiB
Python
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
|
# License: GNU General Public License v3. See license.txt
|
|
|
|
import frappe
|
|
|
|
from erpnext.controllers.item_close import update_closed_status
|
|
from erpnext.stock.doctype.delivery_note.mapper import make_sales_invoice
|
|
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
|
from erpnext.stock.doctype.item.test_item import make_item
|
|
from erpnext.stock.doctype.purchase_receipt.mapper import make_purchase_invoice
|
|
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
|
|
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
|
from erpnext.tests.utils import ERPNextTestSuite
|
|
|
|
WAREHOUSE = "_Test Warehouse - _TC"
|
|
|
|
|
|
class TestPurchaseReceiptItemClose(ERPNextTestSuite):
|
|
def setUp(self):
|
|
self.first_item = make_item(properties={"is_stock_item": 1}).name
|
|
self.second_item = make_item(properties={"is_stock_item": 1}).name
|
|
|
|
def make_purchase_receipt(self):
|
|
receipt = make_purchase_receipt(
|
|
item_code=self.first_item, qty=10, rate=100, warehouse=WAREHOUSE, do_not_submit=True
|
|
)
|
|
receipt.append(
|
|
"items",
|
|
{
|
|
"item_code": self.second_item,
|
|
"warehouse": WAREHOUSE,
|
|
"qty": 10,
|
|
"rate": 100,
|
|
},
|
|
)
|
|
receipt.save()
|
|
receipt.submit()
|
|
return receipt
|
|
|
|
def close_items(self, doc, rows, closed=1):
|
|
update_closed_status(doc.doctype, doc.name, [row.name for row in rows], closed)
|
|
doc.reload()
|
|
|
|
def test_closing_a_row_does_not_inflate_billing_percentage(self):
|
|
receipt = self.make_purchase_receipt()
|
|
self.assertEqual(receipt.per_billed, 0)
|
|
|
|
self.close_items(receipt, [receipt.items[1]])
|
|
|
|
# nothing was billed, so the receipt must not read as partly billed
|
|
self.assertEqual(receipt.per_billed, 0)
|
|
self.assertEqual(receipt.status, "To Bill")
|
|
|
|
def test_closing_every_row_closes_the_receipt(self):
|
|
receipt = self.make_purchase_receipt()
|
|
|
|
self.close_items(receipt, receipt.items)
|
|
|
|
# nothing was billed, and writing every row off must not claim otherwise
|
|
self.assertEqual(receipt.per_billed, 0)
|
|
self.assertEqual(receipt.status, "Closed")
|
|
|
|
def test_closed_row_is_not_mapped_to_purchase_invoice(self):
|
|
receipt = self.make_purchase_receipt()
|
|
self.close_items(receipt, [receipt.items[1]])
|
|
|
|
invoice = make_purchase_invoice(receipt.name)
|
|
|
|
self.assertEqual([item.item_code for item in invoice.items], [self.first_item])
|
|
|
|
def test_billing_a_closed_row_is_blocked(self):
|
|
receipt = self.make_purchase_receipt()
|
|
invoice = make_purchase_invoice(receipt.name)
|
|
|
|
self.close_items(receipt, [receipt.items[1]])
|
|
|
|
invoice.insert()
|
|
self.assertRaises(frappe.ValidationError, invoice.submit)
|
|
|
|
def test_parent_reopen_is_blocked_when_all_rows_are_closed(self):
|
|
receipt = self.make_purchase_receipt()
|
|
self.close_items(receipt, receipt.items)
|
|
|
|
self.assertRaises(frappe.ValidationError, receipt.update_status, "Submitted")
|
|
|
|
def test_reopening_one_row_reopens_the_receipt(self):
|
|
receipt = self.make_purchase_receipt()
|
|
self.close_items(receipt, receipt.items)
|
|
|
|
self.close_items(receipt, [receipt.items[1]], closed=0)
|
|
|
|
self.assertNotEqual(receipt.status, "Closed")
|
|
self.assertEqual(receipt.per_billed, 0)
|
|
|
|
|
|
class TestDeliveryNoteItemClose(ERPNextTestSuite):
|
|
def setUp(self):
|
|
self.first_item = make_item(properties={"is_stock_item": 1}).name
|
|
self.second_item = make_item(properties={"is_stock_item": 1}).name
|
|
for item_code in (self.first_item, self.second_item):
|
|
make_stock_entry(item_code=item_code, target=WAREHOUSE, qty=100, basic_rate=50)
|
|
|
|
def make_delivery_note(self):
|
|
note = create_delivery_note(
|
|
item_code=self.first_item, qty=10, rate=100, warehouse=WAREHOUSE, do_not_save=True
|
|
)
|
|
note.append(
|
|
"items",
|
|
{
|
|
"item_code": self.second_item,
|
|
"warehouse": WAREHOUSE,
|
|
"qty": 10,
|
|
"rate": 100,
|
|
},
|
|
)
|
|
note.insert()
|
|
note.submit()
|
|
return note
|
|
|
|
def close_items(self, doc, rows, closed=1):
|
|
update_closed_status(doc.doctype, doc.name, [row.name for row in rows], closed)
|
|
doc.reload()
|
|
|
|
def test_closing_a_row_does_not_inflate_billing_percentage(self):
|
|
note = self.make_delivery_note()
|
|
self.assertEqual(note.per_billed, 0)
|
|
|
|
self.close_items(note, [note.items[1]])
|
|
|
|
# nothing was billed, so the note must not read as partially billed
|
|
self.assertEqual(note.per_billed, 0)
|
|
self.assertEqual(note.status, "To Bill")
|
|
|
|
def test_closing_every_row_closes_the_note(self):
|
|
note = self.make_delivery_note()
|
|
|
|
self.close_items(note, note.items)
|
|
|
|
# nothing was billed, and writing every row off must not claim otherwise
|
|
self.assertEqual(note.per_billed, 0)
|
|
self.assertEqual(note.status, "Closed")
|
|
|
|
def test_closed_row_is_not_mapped_to_sales_invoice(self):
|
|
note = self.make_delivery_note()
|
|
self.close_items(note, [note.items[1]])
|
|
|
|
invoice = make_sales_invoice(note.name)
|
|
|
|
self.assertEqual([item.item_code for item in invoice.items], [self.first_item])
|
|
|
|
def test_billing_a_closed_row_is_blocked(self):
|
|
note = self.make_delivery_note()
|
|
invoice = make_sales_invoice(note.name)
|
|
|
|
self.close_items(note, [note.items[1]])
|
|
|
|
invoice.insert()
|
|
self.assertRaises(frappe.ValidationError, invoice.submit)
|
|
|
|
def test_closing_a_row_does_not_mark_it_returned(self):
|
|
note = self.make_delivery_note()
|
|
|
|
self.close_items(note, note.items)
|
|
|
|
self.assertEqual(note.per_returned, 0)
|
|
self.assertEqual(note.status, "Closed")
|
|
|
|
def test_amending_clears_closed_rows(self):
|
|
"""Frappe keeps no_copy fields when amending, so the flag must be cleared."""
|
|
note = self.make_delivery_note()
|
|
self.close_items(note, [note.items[1]])
|
|
note.cancel()
|
|
|
|
amended = frappe.copy_doc(note, ignore_no_copy=True)
|
|
amended.docstatus = 0
|
|
amended.amended_from = note.name
|
|
amended.insert()
|
|
|
|
self.assertFalse(any(row.closed for row in amended.items))
|