diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index b6520d3c82d..6ca4aa2ada6 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -931,6 +931,46 @@ def get_currency_precision(): return precision +def get_stock_rbnb_difference(posting_date, company): + stock_items = frappe.db.sql_list( + """select distinct item_code + from `tabStock Ledger Entry` where company=%s""", + company, + ) + + pr_valuation_amount = frappe.db.sql( + """ + select sum(pr_item.valuation_rate * pr_item.qty * pr_item.conversion_factor) + from `tabPurchase Receipt Item` pr_item, `tabPurchase Receipt` pr + where pr.name = pr_item.parent and pr.docstatus=1 and pr.company=%s + and pr.posting_date <= %s and pr_item.item_code in (%s)""" + % ("%s", "%s", ", ".join(["%s"] * len(stock_items))), + tuple([company, posting_date] + stock_items), + )[0][0] + + pi_valuation_amount = frappe.db.sql( + """ + select sum(pi_item.valuation_rate * pi_item.qty * pi_item.conversion_factor) + from `tabPurchase Invoice Item` pi_item, `tabPurchase Invoice` pi + where pi.name = pi_item.parent and pi.docstatus=1 and pi.company=%s + and pi.posting_date <= %s and pi_item.item_code in (%s)""" + % ("%s", "%s", ", ".join(["%s"] * len(stock_items))), + tuple([company, posting_date] + stock_items), + )[0][0] + + # Balance should be + stock_rbnb = flt(pr_valuation_amount, 2) - flt(pi_valuation_amount, 2) + + # Balance as per system + stock_rbnb_account = "Stock Received But Not Billed - " + frappe.get_cached_value( + "Company", company, "abbr" + ) + sys_bal = get_balance_on(stock_rbnb_account, posting_date, in_account_currency=False) + + # Amount should be credited + return flt(stock_rbnb) + flt(sys_bal) + + def get_held_invoices(party_type, party): """ Returns a list of names Purchase Invoices for the given party that are on hold diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py deleted file mode 100644 index b932c1371d6..00000000000 --- a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py +++ /dev/null @@ -1,590 +0,0 @@ -# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt - -import json - -import frappe -from frappe.tests.utils import FrappeTestCase, change_settings -from frappe.utils import flt, nowtime, today - -from erpnext.stock.doctype.item.test_item import make_item -from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import ( - add_serial_batch_ledgers, - make_batch_nos, - make_serial_nos, -) -from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry - - -class TestSerialandBatchBundle(FrappeTestCase): - def test_inward_outward_serial_valuation(self): - from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note - from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt - - serial_item_code = "New Serial No Valuation 1" - make_item( - serial_item_code, - { - "has_serial_no": 1, - "serial_no_series": "TEST-SER-VAL-.#####", - "is_stock_item": 1, - }, - ) - - pr = make_purchase_receipt( - item_code=serial_item_code, warehouse="_Test Warehouse - _TC", qty=1, rate=500 - ) - - serial_no1 = get_serial_nos_from_bundle(pr.items[0].serial_and_batch_bundle)[0] - - pr = make_purchase_receipt( - item_code=serial_item_code, warehouse="_Test Warehouse - _TC", qty=1, rate=300 - ) - - serial_no2 = get_serial_nos_from_bundle(pr.items[0].serial_and_batch_bundle)[0] - - dn = create_delivery_note( - item_code=serial_item_code, - warehouse="_Test Warehouse - _TC", - qty=1, - rate=1500, - serial_no=[serial_no2], - ) - - stock_value_difference = frappe.db.get_value( - "Stock Ledger Entry", - {"voucher_no": dn.name, "is_cancelled": 0, "voucher_type": "Delivery Note"}, - "stock_value_difference", - ) - - self.assertEqual(flt(stock_value_difference, 2), -300) - - dn = create_delivery_note( - item_code=serial_item_code, - warehouse="_Test Warehouse - _TC", - qty=1, - rate=1500, - serial_no=[serial_no1], - ) - - stock_value_difference = frappe.db.get_value( - "Stock Ledger Entry", - {"voucher_no": dn.name, "is_cancelled": 0, "voucher_type": "Delivery Note"}, - "stock_value_difference", - ) - - self.assertEqual(flt(stock_value_difference, 2), -500) - - def test_inward_outward_batch_valuation(self): - from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note - from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt - - batch_item_code = "New Batch No Valuation 1" - make_item( - batch_item_code, - { - "has_batch_no": 1, - "create_new_batch": 1, - "batch_number_series": "TEST-BATTCCH-VAL-.#####", - "is_stock_item": 1, - }, - ) - - pr = make_purchase_receipt( - item_code=batch_item_code, warehouse="_Test Warehouse - _TC", qty=10, rate=500 - ) - - batch_no1 = get_batch_from_bundle(pr.items[0].serial_and_batch_bundle) - - pr = make_purchase_receipt( - item_code=batch_item_code, warehouse="_Test Warehouse - _TC", qty=10, rate=300 - ) - - batch_no2 = get_batch_from_bundle(pr.items[0].serial_and_batch_bundle) - - dn = create_delivery_note( - item_code=batch_item_code, - warehouse="_Test Warehouse - _TC", - qty=10, - rate=1500, - batch_no=batch_no2, - ) - - stock_value_difference = frappe.db.get_value( - "Stock Ledger Entry", - {"voucher_no": dn.name, "is_cancelled": 0, "voucher_type": "Delivery Note"}, - "stock_value_difference", - ) - - self.assertEqual(flt(stock_value_difference, 2), -3000) - - dn = create_delivery_note( - item_code=batch_item_code, - warehouse="_Test Warehouse - _TC", - qty=10, - rate=1500, - batch_no=batch_no1, - ) - - stock_value_difference = frappe.db.get_value( - "Stock Ledger Entry", - {"voucher_no": dn.name, "is_cancelled": 0, "voucher_type": "Delivery Note"}, - "stock_value_difference", - ) - - self.assertEqual(flt(stock_value_difference, 2), -5000) - - def test_old_batch_valuation(self): - frappe.flags.ignore_serial_batch_bundle_validation = True - frappe.flags.use_serial_and_batch_fields = True - batch_item_code = "Old Batch Item Valuation 1" - make_item( - batch_item_code, - { - "has_batch_no": 1, - "is_stock_item": 1, - }, - ) - - batch_id = "Old Batch 1" - if not frappe.db.exists("Batch", batch_id): - batch_doc = frappe.get_doc( - { - "doctype": "Batch", - "batch_id": batch_id, - "item": batch_item_code, - "use_batchwise_valuation": 0, - } - ).insert(ignore_permissions=True) - - self.assertTrue(batch_doc.use_batchwise_valuation) - batch_doc.db_set("use_batchwise_valuation", 0) - - stock_queue = [] - qty_after_transaction = 0 - balance_value = 0 - for qty, valuation in {10: 100, 20: 200}.items(): - stock_queue.append([qty, valuation]) - qty_after_transaction += qty - balance_value += qty_after_transaction * valuation - - doc = frappe.get_doc( - { - "doctype": "Stock Ledger Entry", - "posting_date": today(), - "posting_time": nowtime(), - "batch_no": batch_id, - "incoming_rate": valuation, - "qty_after_transaction": qty_after_transaction, - "stock_value_difference": valuation * qty, - "balance_value": balance_value, - "valuation_rate": balance_value / qty_after_transaction, - "actual_qty": qty, - "item_code": batch_item_code, - "warehouse": "_Test Warehouse - _TC", - "stock_queue": json.dumps(stock_queue), - } - ) - - doc.flags.ignore_permissions = True - doc.flags.ignore_mandatory = True - doc.flags.ignore_links = True - doc.flags.ignore_validate = True - doc.submit() - doc.reload() - - bundle_doc = make_serial_batch_bundle( - { - "item_code": batch_item_code, - "warehouse": "_Test Warehouse - _TC", - "voucher_type": "Stock Entry", - "posting_date": today(), - "posting_time": nowtime(), - "qty": -10, - "batches": frappe._dict({batch_id: 10}), - "type_of_transaction": "Outward", - "do_not_submit": True, - } - ) - - bundle_doc.reload() - for row in bundle_doc.entries: - self.assertEqual(flt(row.stock_value_difference, 2), -1666.67) - - bundle_doc.flags.ignore_permissions = True - bundle_doc.flags.ignore_mandatory = True - bundle_doc.flags.ignore_links = True - bundle_doc.flags.ignore_validate = True - bundle_doc.submit() - - bundle_doc = make_serial_batch_bundle( - { - "item_code": batch_item_code, - "warehouse": "_Test Warehouse - _TC", - "voucher_type": "Stock Entry", - "posting_date": today(), - "posting_time": nowtime(), - "qty": -20, - "batches": frappe._dict({batch_id: 20}), - "type_of_transaction": "Outward", - "do_not_submit": True, - } - ) - - bundle_doc.reload() - for row in bundle_doc.entries: - self.assertEqual(flt(row.stock_value_difference, 2), -3333.33) - - bundle_doc.flags.ignore_permissions = True - bundle_doc.flags.ignore_mandatory = True - bundle_doc.flags.ignore_links = True - bundle_doc.flags.ignore_validate = True - bundle_doc.submit() - - frappe.flags.ignore_serial_batch_bundle_validation = False - frappe.flags.use_serial_and_batch_fields = False - - def test_old_serial_no_valuation(self): - from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt - - serial_no_item_code = "Old Serial No Item Valuation 1" - make_item( - serial_no_item_code, - { - "has_serial_no": 1, - "serial_no_series": "TEST-SER-VALL-.#####", - "is_stock_item": 1, - }, - ) - - make_purchase_receipt( - item_code=serial_no_item_code, warehouse="_Test Warehouse - _TC", qty=1, rate=500 - ) - - frappe.flags.ignore_serial_batch_bundle_validation = True - frappe.flags.use_serial_and_batch_fields = True - - serial_no_id = "Old Serial No 1" - if not frappe.db.exists("Serial No", serial_no_id): - sn_doc = frappe.get_doc( - { - "doctype": "Serial No", - "serial_no": serial_no_id, - "item_code": serial_no_item_code, - "company": "_Test Company", - } - ).insert(ignore_permissions=True) - - sn_doc.db_set( - { - "warehouse": "_Test Warehouse - _TC", - "purchase_rate": 100, - } - ) - - doc = frappe.get_doc( - { - "doctype": "Stock Ledger Entry", - "posting_date": today(), - "posting_time": nowtime(), - "serial_no": serial_no_id, - "incoming_rate": 100, - "qty_after_transaction": 1, - "stock_value_difference": 100, - "balance_value": 100, - "valuation_rate": 100, - "actual_qty": 1, - "item_code": serial_no_item_code, - "warehouse": "_Test Warehouse - _TC", - "company": "_Test Company", - } - ) - - doc.flags.ignore_permissions = True - doc.flags.ignore_mandatory = True - doc.flags.ignore_links = True - doc.flags.ignore_validate = True - doc.submit() - - bundle_doc = make_serial_batch_bundle( - { - "item_code": serial_no_item_code, - "warehouse": "_Test Warehouse - _TC", - "voucher_type": "Stock Entry", - "posting_date": today(), - "posting_time": nowtime(), - "qty": -1, - "serial_nos": [serial_no_id], - "type_of_transaction": "Outward", - "do_not_submit": True, - } - ) - - bundle_doc.reload() - for row in bundle_doc.entries: - self.assertEqual(flt(row.stock_value_difference, 2), -100.00) - - frappe.flags.ignore_serial_batch_bundle_validation = False - frappe.flags.use_serial_and_batch_fields = False - - def test_batch_not_belong_to_serial_no(self): - from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt - - serial_and_batch_code = "New Serial No Valuation 1" - make_item( - serial_and_batch_code, - { - "has_serial_no": 1, - "serial_no_series": "TEST-SER-VALL-.#####", - "is_stock_item": 1, - "has_batch_no": 1, - "create_new_batch": 1, - "batch_number_series": "TEST-SNBAT-VAL-.#####", - }, - ) - - pr = make_purchase_receipt( - item_code=serial_and_batch_code, warehouse="_Test Warehouse - _TC", qty=1, rate=500 - ) - - serial_no = get_serial_nos_from_bundle(pr.items[0].serial_and_batch_bundle)[0] - - pr = make_purchase_receipt( - item_code=serial_and_batch_code, warehouse="_Test Warehouse - _TC", qty=1, rate=300 - ) - - batch_no = get_batch_from_bundle(pr.items[0].serial_and_batch_bundle) - - doc = frappe.get_doc( - { - "doctype": "Serial and Batch Bundle", - "item_code": serial_and_batch_code, - "warehouse": "_Test Warehouse - _TC", - "voucher_type": "Stock Entry", - "posting_date": today(), - "posting_time": nowtime(), - "qty": -1, - "type_of_transaction": "Outward", - } - ) - - doc.append( - "entries", - { - "batch_no": batch_no, - "serial_no": serial_no, - "qty": -1, - }, - ) - - # Batch does not belong to serial no - self.assertRaises(frappe.exceptions.ValidationError, doc.save) - - def test_auto_delete_draft_serial_and_batch_bundle(self): - serial_and_batch_code = "New Serial No Auto Delete 1" - make_item( - serial_and_batch_code, - { - "has_serial_no": 1, - "serial_no_series": "TEST-SER-VALL-.#####", - "is_stock_item": 1, - }, - ) - - ste = make_stock_entry( - item_code=serial_and_batch_code, - target="_Test Warehouse - _TC", - qty=1, - rate=500, - do_not_submit=True, - ) - - serial_no = "SN-TEST-AUTO-DEL" - if not frappe.db.exists("Serial No", serial_no): - frappe.get_doc( - { - "doctype": "Serial No", - "serial_no": serial_no, - "item_code": serial_and_batch_code, - "company": "_Test Company", - } - ).insert(ignore_permissions=True) - - bundle_doc = make_serial_batch_bundle( - { - "item_code": serial_and_batch_code, - "warehouse": "_Test Warehouse - _TC", - "voucher_type": "Stock Entry", - "posting_date": ste.posting_date, - "posting_time": ste.posting_time, - "qty": 1, - "serial_nos": [serial_no], - "type_of_transaction": "Inward", - "do_not_submit": True, - } - ) - - bundle_doc.reload() - ste.items[0].serial_and_batch_bundle = bundle_doc.name - ste.save() - ste.reload() - - ste.delete() - self.assertFalse(frappe.db.exists("Serial and Batch Bundle", bundle_doc.name)) - - def test_serial_and_batch_bundle_company(self): - from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt - - item = make_item( - "Test Serial and Batch Bundle Company Item", - properties={ - "has_serial_no": 1, - "serial_no_series": "TT-SER-VAL-.#####", - }, - ).name - - pr = make_purchase_receipt( - item_code=item, - warehouse="_Test Warehouse - _TC", - qty=3, - rate=500, - do_not_submit=True, - ) - - entries = [] - for serial_no in ["TT-SER-VAL-00001", "TT-SER-VAL-00002", "TT-SER-VAL-00003"]: - entries.append(frappe._dict({"serial_no": serial_no, "qty": 1})) - - if not frappe.db.exists("Serial No", serial_no): - frappe.get_doc( - { - "doctype": "Serial No", - "serial_no": serial_no, - "item_code": item, - } - ).insert(ignore_permissions=True) - - item_row = pr.items[0] - item_row.type_of_transaction = "Inward" - item_row.is_rejected = 0 - sn_doc = add_serial_batch_ledgers(entries, item_row, pr, "_Test Warehouse - _TC") - self.assertEqual(sn_doc.company, "_Test Company") - - def test_auto_cancel_serial_and_batch(self): - item_code = make_item( - properties={"has_serial_no": 1, "serial_no_series": "ATC-TT-SER-VAL-.#####"} - ).name - - se = make_stock_entry( - item_code=item_code, - target="_Test Warehouse - _TC", - qty=5, - rate=500, - ) - - bundle = se.items[0].serial_and_batch_bundle - docstatus = frappe.db.get_value("Serial and Batch Bundle", bundle, "docstatus") - self.assertEqual(docstatus, 1) - - se.cancel() - docstatus = frappe.db.get_value("Serial and Batch Bundle", bundle, "docstatus") - self.assertEqual(docstatus, 2) - - def test_batch_duplicate_entry(self): - item_code = make_item(properties={"has_batch_no": 1}).name - - batch_id = "TEST-BATTCCH-VAL-00001" - batch_nos = [{"batch_no": batch_id, "qty": 1}] - - make_batch_nos(item_code, batch_nos) - self.assertTrue(frappe.db.exists("Batch", batch_id)) - - batch_id = "TEST-BATTCCH-VAL-00001" - batch_nos = [{"batch_no": batch_id, "qty": 1}] - - # Shouldn't throw duplicate entry error - make_batch_nos(item_code, batch_nos) - self.assertTrue(frappe.db.exists("Batch", batch_id)) - - def test_serial_no_duplicate_entry(self): - item_code = make_item(properties={"has_serial_no": 1}).name - - serial_no_id = "TEST-SNID-VAL-00001" - serial_nos = [{"serial_no": serial_no_id, "qty": 1}] - - make_serial_nos(item_code, serial_nos) - self.assertTrue(frappe.db.exists("Serial No", serial_no_id)) - - serial_no_id = "TEST-SNID-VAL-00001" - serial_nos = [{"batch_no": serial_no_id, "qty": 1}] - - # Shouldn't throw duplicate entry error - make_serial_nos(item_code, serial_nos) - self.assertTrue(frappe.db.exists("Serial No", serial_no_id)) - - @change_settings("Stock Settings", {"auto_create_serial_and_batch_bundle_for_outward": 1}) - def test_duplicate_serial_and_batch_bundle(self): - from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt - - item_code = make_item(properties={"is_stock_item": 1, "has_serial_no": 1}).name - - serial_no = f"{item_code}-001" - serial_nos = [{"serial_no": serial_no, "qty": 1}] - make_serial_nos(item_code, serial_nos) - - pr1 = make_purchase_receipt(item=item_code, qty=1, rate=500, serial_no=[serial_no]) - pr2 = make_purchase_receipt(item=item_code, qty=1, rate=500, do_not_save=True) - - pr1.reload() - pr2.items[0].serial_and_batch_bundle = pr1.items[0].serial_and_batch_bundle - - self.assertRaises(frappe.exceptions.ValidationError, pr2.save) - - -def get_batch_from_bundle(bundle): - from erpnext.stock.serial_batch_bundle import get_batch_nos - - batches = get_batch_nos(bundle) - - return list(batches.keys())[0] - - -def get_serial_nos_from_bundle(bundle): - from erpnext.stock.serial_batch_bundle import get_serial_nos - - serial_nos = get_serial_nos(bundle) - return sorted(serial_nos) if serial_nos else [] - - -def make_serial_batch_bundle(kwargs): - from erpnext.stock.serial_batch_bundle import SerialBatchCreation - - if isinstance(kwargs, dict): - kwargs = frappe._dict(kwargs) - - type_of_transaction = "Inward" if kwargs.qty > 0 else "Outward" - if kwargs.get("type_of_transaction"): - type_of_transaction = kwargs.get("type_of_transaction") - - sb = SerialBatchCreation( - { - "item_code": kwargs.item_code, - "warehouse": kwargs.warehouse, - "voucher_type": kwargs.voucher_type, - "voucher_no": kwargs.voucher_no, - "posting_date": kwargs.posting_date, - "posting_time": kwargs.posting_time, - "qty": kwargs.qty, - "avg_rate": kwargs.rate, - "batches": kwargs.batches, - "serial_nos": kwargs.serial_nos, - "type_of_transaction": type_of_transaction, - "company": kwargs.company or "_Test Company", - "do_not_submit": kwargs.do_not_submit, - } - ) - - if not kwargs.get("do_not_save"): - return sb.make_serial_and_batch_bundle() - - return sb diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index ce1ffa52ede..c09a30d22c9 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -7,8 +7,8 @@ from typing import Optional, Set, Tuple import frappe from frappe import _ from frappe.model.meta import get_field_precision -from frappe.query_builder.functions import CombineDatetime, Sum -from frappe.utils import cint, cstr, flt, get_link_to_form, getdate, now, nowdate, nowtime, parse_json +from frappe.query_builder.functions import Sum +from frappe.utils import cint, cstr, flt, get_link_to_form, getdate, now, nowdate import erpnext from erpnext.stock.doctype.bin.bin import update_qty as update_bin_qty @@ -1640,10 +1640,7 @@ def get_future_sle_with_negative_qty(sle): (SLE.item_code == sle.item_code) & (SLE.warehouse == sle.warehouse) & (SLE.voucher_no != sle.voucher_no) - & ( - SLE.posting_datetime - >= get_combine_datetime(sle.posting_date, sle.posting_time) - ) + & (SLE.posting_datetime >= get_combine_datetime(sle.posting_date, sle.posting_time)) & (SLE.is_cancelled == 0) & (SLE.qty_after_transaction < 0) )