chore: Merge branch 'version-13-hotfix' into 'version-13-pre-release'

This commit is contained in:
Rohit Waghchaure
2022-02-15 15:14:10 +05:30
82 changed files with 1836 additions and 508 deletions

View File

@@ -31,23 +31,9 @@ class Bin(Document):
def update_reserved_qty_for_production(self):
'''Update qty reserved for production from Production Item tables
in open work orders'''
self.reserved_qty_for_production = frappe.db.sql('''
SELECT
SUM(CASE WHEN ifnull(skip_transfer, 0) = 0 THEN
item.required_qty - item.transferred_qty
ELSE
item.required_qty - item.consumed_qty END)
END
FROM `tabWork Order` pro, `tabWork Order Item` item
WHERE
item.item_code = %s
and item.parent = pro.name
and pro.docstatus = 1
and item.source_warehouse = %s
and pro.status not in ("Stopped", "Completed")
and (item.required_qty > item.transferred_qty or item.required_qty > item.consumed_qty)
''', (self.item_code, self.warehouse))[0][0]
from erpnext.manufacturing.doctype.work_order.work_order import get_reserved_qty_for_production
self.reserved_qty_for_production = get_reserved_qty_for_production(self.item_code, self.warehouse)
self.set_projected_qty()
self.db_set('reserved_qty_for_production', flt(self.reserved_qty_for_production))

View File

@@ -339,17 +339,31 @@ class DeliveryNote(SellingController):
frappe.throw(_("Could not create Credit Note automatically, please uncheck 'Issue Credit Note' and submit again"))
def update_billed_amount_based_on_so(so_detail, update_modified=True):
from frappe.query_builder.functions import Sum
# Billed against Sales Order directly
billed_against_so = frappe.db.sql("""select sum(amount) from `tabSales Invoice Item`
where so_detail=%s and (dn_detail is null or dn_detail = '') and docstatus=1""", so_detail)
si_item = frappe.qb.DocType("Sales Invoice Item").as_("si_item")
sum_amount = Sum(si_item.amount).as_("amount")
billed_against_so = frappe.qb.from_(si_item).select(sum_amount).where(
(si_item.so_detail == so_detail) &
((si_item.dn_detail.isnull()) | (si_item.dn_detail == '')) &
(si_item.docstatus == 1)
).run()
billed_against_so = billed_against_so and billed_against_so[0][0] or 0
# Get all Delivery Note Item rows against the Sales Order Item row
dn_details = frappe.db.sql("""select dn_item.name, dn_item.amount, dn_item.si_detail, dn_item.parent
from `tabDelivery Note Item` dn_item, `tabDelivery Note` dn
where dn.name=dn_item.parent and dn_item.so_detail=%s
and dn.docstatus=1 and dn.is_return = 0
order by dn.posting_date asc, dn.posting_time asc, dn.name asc""", so_detail, as_dict=1)
dn = frappe.qb.DocType("Delivery Note").as_("dn")
dn_item = frappe.qb.DocType("Delivery Note Item").as_("dn_item")
dn_details = frappe.qb.from_(dn).from_(dn_item).select(dn_item.name, dn_item.amount, dn_item.si_detail, dn_item.parent).where(
(dn.name == dn_item.parent) &
(dn_item.so_detail == so_detail) &
(dn.docstatus == 1) &
(dn.is_return == 0)
).orderby(
dn.posting_date, dn.posting_time, dn.name
).run(as_dict=True)
updated_dn = []
for dnd in dn_details:

View File

@@ -4,10 +4,11 @@
import frappe
from frappe.utils import flt
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.purchase_receipt.test_purchase_receipt import (
get_gl_entries,
@@ -28,7 +29,8 @@ class TestLandedCostVoucher(ERPNextTestCase):
"voucher_type": pr.doctype,
"voucher_no": pr.name,
"item_code": "_Test Item",
"warehouse": "Stores - TCP1"
"warehouse": "Stores - TCP1",
"is_cancelled": 0,
},
fieldname=["qty_after_transaction", "stock_value"], as_dict=1)
@@ -41,14 +43,39 @@ class TestLandedCostVoucher(ERPNextTestCase):
"voucher_type": pr.doctype,
"voucher_no": pr.name,
"item_code": "_Test Item",
"warehouse": "Stores - TCP1"
"warehouse": "Stores - TCP1",
"is_cancelled": 0,
},
fieldname=["qty_after_transaction", "stock_value"], as_dict=1)
self.assertEqual(last_sle.qty_after_transaction, last_sle_after_landed_cost.qty_after_transaction)
self.assertEqual(last_sle_after_landed_cost.stock_value - last_sle.stock_value, 25.0)
# assert after submit
self.assertPurchaseReceiptLCVGLEntries(pr)
# Mess up cancelled SLE modified timestamp to check
# if they aren't effective in any business logic.
frappe.db.set_value("Stock Ledger Entry",
{
"is_cancelled": 1,
"voucher_type": pr.doctype,
"voucher_no": pr.name
},
"is_cancelled", 1,
modified=add_to_date(now(), hours=1, as_datetime=True, as_string=True)
)
items, warehouses = pr.get_items_and_warehouses()
update_gl_entries_after(pr.posting_date, pr.posting_time,
warehouses, items, company=pr.company)
# reassert after reposting
self.assertPurchaseReceiptLCVGLEntries(pr)
def assertPurchaseReceiptLCVGLEntries(self, pr):
gl_entries = get_gl_entries("Purchase Receipt", pr.name)
self.assertTrue(gl_entries)
@@ -74,8 +101,8 @@ class TestLandedCostVoucher(ERPNextTestCase):
for gle in gl_entries:
if not gle.get('is_cancelled'):
self.assertEqual(expected_values[gle.account][0], gle.debit)
self.assertEqual(expected_values[gle.account][1], gle.credit)
self.assertEqual(expected_values[gle.account][0], gle.debit, msg=f"incorrect debit for {gle.account}")
self.assertEqual(expected_values[gle.account][1], gle.credit, msg=f"incorrect credit for {gle.account}")
def test_landed_cost_voucher_against_purchase_invoice(self):

View File

@@ -1,10 +1,14 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
from frappe.utils import add_to_date, nowdate
from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
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
@@ -12,31 +16,30 @@ class TestPackedItem(ERPNextTestCase):
"Test impact on Packed Items table in various scenarios."
@classmethod
def setUpClass(cls) -> None:
make_item("_Test Product Bundle X", {"is_stock_item": 0})
make_item("_Test Bundle Item 1", {"is_stock_item": 1})
make_item("_Test Bundle Item 2", {"is_stock_item": 1})
super().setUpClass()
cls.bundle = "_Test Product Bundle X"
cls.bundle_items = ["_Test Bundle Item 1", "_Test Bundle Item 2"]
make_item(cls.bundle, {"is_stock_item": 0})
for item in cls.bundle_items:
make_item(item, {"is_stock_item": 1})
make_item("_Test Normal Stock Item", {"is_stock_item": 1})
make_product_bundle(
"_Test Product Bundle X",
["_Test Bundle Item 1", "_Test Bundle Item 2"],
qty=2
)
make_product_bundle(cls.bundle, cls.bundle_items, qty=2)
def test_adding_bundle_item(self):
"Test impact on packed items if bundle item row is added."
so = make_sales_order(item_code = "_Test Product Bundle X", qty=1,
so = make_sales_order(item_code = self.bundle, qty=1,
do_not_submit=True)
self.assertEqual(so.items[0].qty, 1)
self.assertEqual(len(so.packed_items), 2)
self.assertEqual(so.packed_items[0].item_code, "_Test Bundle Item 1")
self.assertEqual(so.packed_items[0].item_code, self.bundle_items[0])
self.assertEqual(so.packed_items[0].qty, 2)
def test_updating_bundle_item(self):
"Test impact on packed items if bundle item row is updated."
so = make_sales_order(item_code = "_Test Product Bundle X", qty=1,
do_not_submit=True)
so = make_sales_order(item_code=self.bundle, qty=1, do_not_submit=True)
so.items[0].qty = 2 # change qty
so.save()
@@ -55,7 +58,7 @@ class TestPackedItem(ERPNextTestCase):
so_items = []
for qty in [2, 4, 6, 8]:
so_items.append({
"item_code": "_Test Product Bundle X",
"item_code": self.bundle,
"qty": qty,
"rate": 400,
"warehouse": "_Test Warehouse - _TC"
@@ -66,7 +69,7 @@ class TestPackedItem(ERPNextTestCase):
# check alternate rows for qty
self.assertEqual(len(so.packed_items), 8)
self.assertEqual(so.packed_items[1].item_code, "_Test Bundle Item 2")
self.assertEqual(so.packed_items[1].item_code, self.bundle_items[1])
self.assertEqual(so.packed_items[1].qty, 4)
self.assertEqual(so.packed_items[3].qty, 8)
self.assertEqual(so.packed_items[5].qty, 12)
@@ -94,8 +97,7 @@ class TestPackedItem(ERPNextTestCase):
@change_settings("Selling Settings", {"editable_bundle_item_rates": 1})
def test_bundle_item_cumulative_price(self):
"Test if Bundle Item rate is cumulative from packed items."
so = make_sales_order(item_code = "_Test Product Bundle X", qty=2,
do_not_submit=True)
so = make_sales_order(item_code=self.bundle, qty=2, do_not_submit=True)
so.packed_items[0].rate = 150
so.packed_items[1].rate = 200
@@ -109,7 +111,7 @@ class TestPackedItem(ERPNextTestCase):
so_items = []
for qty in [2, 4]:
so_items.append({
"item_code": "_Test Product Bundle X",
"item_code": self.bundle,
"qty": qty,
"rate": 400,
"warehouse": "_Test Warehouse - _TC"
@@ -124,4 +126,33 @@ class TestPackedItem(ERPNextTestCase):
self.assertEqual(len(dn.packed_items), 4)
self.assertEqual(dn.packed_items[2].qty, 6)
self.assertEqual(dn.packed_items[3].qty, 6)
self.assertEqual(dn.packed_items[3].qty, 6)
def test_reposting_packed_items(self):
warehouse = "Stores - TCP1"
company = "_Test Company with perpetual inventory"
today = nowdate()
yesterday = add_to_date(today, days=-1, as_string=True)
for item in self.bundle_items:
make_stock_entry(item_code=item, to_warehouse=warehouse, qty=10, rate=100, posting_date=today)
so = make_sales_order(item_code = self.bundle, qty=1, company=company, warehouse=warehouse)
dn = make_delivery_note(so.name)
dn.save()
dn.submit()
gles = get_gl_entries(dn.doctype, dn.name)
credit_before_repost = sum(gle.credit for gle in gles)
# backdated stock entry
for item in self.bundle_items:
make_stock_entry(item_code=item, to_warehouse=warehouse, qty=10, rate=200, posting_date=yesterday)
# assert correct reposting
gles = get_gl_entries(dn.doctype, dn.name)
credit_after_reposting = sum(gle.credit for gle in gles)
self.assertNotEqual(credit_before_repost, credit_after_reposting)
self.assertAlmostEqual(credit_after_reposting, 2 * credit_before_repost)

View File

@@ -281,10 +281,7 @@ class PurchaseReceipt(BuyingController):
if warehouse_account.get(d.warehouse):
stock_value_diff = frappe.db.get_value("Stock Ledger Entry",
{"voucher_type": "Purchase Receipt", "voucher_no": self.name,
"voucher_detail_no": d.name, "warehouse": d.warehouse}, "stock_value_difference")
if not stock_value_diff:
continue
"voucher_detail_no": d.name, "warehouse": d.warehouse, "is_cancelled": 0}, "stock_value_difference")
warehouse_account_name = warehouse_account[d.warehouse]["account"]
warehouse_account_currency = warehouse_account[d.warehouse]["account_currency"]

View File

@@ -4,6 +4,7 @@
import json
import unittest
from collections import defaultdict
import frappe
from frappe.utils import add_days, cint, cstr, flt, today
@@ -17,7 +18,7 @@ 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
from erpnext.tests.utils import ERPNextTestCase, change_settings
class TestPurchaseReceipt(ERPNextTestCase):
@@ -1367,6 +1368,36 @@ class TestPurchaseReceipt(ERPNextTestCase):
automatically_fetch_payment_terms(enable=0)
@change_settings("Stock Settings", {"allow_negative_stock": 1})
def test_neg_to_positive(self):
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
item_code = "_TestNegToPosItem"
warehouse = "Stores - TCP1"
company = "_Test Company with perpetual inventory"
account = "Stock Received But Not Billed - TCP1"
make_item(item_code)
se = make_stock_entry(item_code=item_code, from_warehouse=warehouse, qty=50, do_not_save=True, rate=0)
se.items[0].allow_zero_valuation_rate = 1
se.save()
se.submit()
pr = make_purchase_receipt(
qty=50,
rate=1,
item_code=item_code,
warehouse=warehouse,
get_taxes_and_charges=True,
company=company,
)
gles = get_gl_entries(pr.doctype, pr.name)
for gle in gles:
if gle.account == account:
self.assertEqual(gle.credit, 50)
def get_sl_entries(voucher_type, voucher_no):
return frappe.db.sql(""" select actual_qty, warehouse, stock_value_difference
from `tabStock Ledger Entry` where voucher_type=%s and voucher_no=%s

View File

@@ -13,7 +13,7 @@ from erpnext.accounts.utils import (
check_if_stock_and_account_balance_synced,
update_gl_entries_after,
)
from erpnext.stock.stock_ledger import repost_future_sle
from erpnext.stock.stock_ledger import get_items_to_be_repost, repost_future_sle
class RepostItemValuation(Document):
@@ -138,13 +138,20 @@ def repost_gl_entries(doc):
if doc.based_on == 'Transaction':
ref_doc = frappe.get_doc(doc.voucher_type, doc.voucher_no)
items, warehouses = ref_doc.get_items_and_warehouses()
doc_items, doc_warehouses = ref_doc.get_items_and_warehouses()
sles = get_items_to_be_repost(doc.voucher_type, doc.voucher_no)
sle_items = [sle.item_code for sle in sles]
sle_warehouse = [sle.warehouse for sle in sles]
items = list(set(doc_items).union(set(sle_items)))
warehouses = list(set(doc_warehouses).union(set(sle_warehouse)))
else:
items = [doc.item_code]
warehouses = [doc.warehouse]
update_gl_entries_after(doc.posting_date, doc.posting_time,
warehouses, items, company=doc.company)
for_warehouses=warehouses, for_items=items, company=doc.company)
def notify_error_to_stock_managers(doc, traceback):
recipients = get_users_with_role("Stock Manager")

View File

@@ -8,7 +8,6 @@
"engine": "InnoDB",
"field_order": [
"items_section",
"title",
"naming_series",
"stock_entry_type",
"outgoing_stock_entry",
@@ -83,14 +82,6 @@
"fieldtype": "Section Break",
"oldfieldtype": "Section Break"
},
{
"fieldname": "title",
"fieldtype": "Data",
"hidden": 1,
"label": "Title",
"no_copy": 1,
"print_hide": 1
},
{
"fieldname": "naming_series",
"fieldtype": "Select",
@@ -353,9 +344,9 @@
},
{
"fieldname": "scan_barcode",
"options": "Barcode",
"fieldtype": "Data",
"label": "Scan Barcode"
"label": "Scan Barcode",
"options": "Barcode"
},
{
"allow_bulk_edit": 1,
@@ -628,10 +619,11 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2021-08-20 19:19:31.514846",
"modified": "2022-02-07 12:55:14.614077",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Entry",
"naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{
@@ -698,6 +690,7 @@
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "title",
"states": [],
"title_field": "stock_entry_type",
"track_changes": 1
}

View File

@@ -77,7 +77,6 @@ class StockEntry(StockController):
self.validate_posting_time()
self.validate_purpose()
self.set_title()
self.validate_item()
self.validate_customer_provided_item()
self.validate_qty()
@@ -1117,7 +1116,7 @@ class StockEntry(StockController):
self.set_actual_qty()
self.update_items_for_process_loss()
self.validate_customer_provided_item()
self.calculate_rate_and_amount()
self.calculate_rate_and_amount(raise_error_if_no_rate=False)
def set_scrap_items(self):
if self.purpose != "Send to Subcontractor" and self.purpose in ["Manufacture", "Repack"]:
@@ -1837,14 +1836,6 @@ class StockEntry(StockController):
return sorted(list(set(get_serial_nos(self.pro_doc.serial_no)) - set(used_serial_nos)))
def set_title(self):
if frappe.flags.in_import and self.title:
# Allow updating title during data import/update
return
self.title = self.purpose
@frappe.whitelist()
def move_sample_to_retention_warehouse(company, items):
if isinstance(items, string_types):

View File

@@ -18,7 +18,6 @@
"items",
"section_break_9",
"expense_account",
"reconciliation_json",
"column_break_13",
"difference_amount",
"amended_from",
@@ -111,15 +110,6 @@
"label": "Cost Center",
"options": "Cost Center"
},
{
"fieldname": "reconciliation_json",
"fieldtype": "Long Text",
"hidden": 1,
"label": "Reconciliation JSON",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "column_break_13",
"fieldtype": "Column Break"
@@ -155,7 +145,7 @@
"idx": 1,
"is_submittable": 1,
"links": [],
"modified": "2021-11-30 01:33:51.437194",
"modified": "2022-02-06 14:28:19.043905",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Reconciliation",
@@ -178,5 +168,6 @@
"search_fields": "posting_date",
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "DESC"
"sort_order": "DESC",
"states": []
}

View File

@@ -25,8 +25,8 @@ from erpnext.tests.utils import ERPNextTestCase, change_settings
class TestStockReconciliation(ERPNextTestCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
create_batch_or_serial_no_items()
super().setUpClass()
frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1)
def tearDown(self):

View File

@@ -344,6 +344,7 @@ def get_basic_details(args, item, overwrite_warehouse=True):
args.conversion_factor = out.conversion_factor
out.stock_qty = out.qty * out.conversion_factor
args.stock_qty = out.stock_qty
# calculate last purchase rate
if args.get('doctype') in purchase_doctypes:

View File

@@ -252,6 +252,7 @@ class FIFOSlots:
key, fifo_queue, transferred_item_key = self.__init_key_stores(d)
if d.voucher_type == "Stock Reconciliation":
# get difference in qty shift as actual qty
prev_balance_qty = self.item_details[key].get("qty_after_transaction", 0)
d.actual_qty = flt(d.qty_after_transaction) - flt(prev_balance_qty)
@@ -264,12 +265,16 @@ class FIFOSlots:
self.__update_balances(d, key)
if not self.filters.get("show_warehouse_wise_stock"):
# (Item 1, WH 1), (Item 1, WH 2) => (Item 1)
self.item_details = self.__aggregate_details_by_item(self.item_details)
return self.item_details
def __init_key_stores(self, row: Dict) -> Tuple:
"Initialise keys and FIFO Queue."
key = (row.name, row.warehouse) if self.filters.get('show_warehouse_wise_stock') else row.name
key = (row.name, row.warehouse)
self.item_details.setdefault(key, {"details": row, "fifo_queue": []})
fifo_queue = self.item_details[key]["fifo_queue"]
@@ -338,6 +343,27 @@ class FIFOSlots:
self.item_details[key]["has_serial_no"] = row.has_serial_no
def __aggregate_details_by_item(self, wh_wise_data: Dict) -> Dict:
"Aggregate Item-Wh wise data into single Item entry."
item_aggregated_data = {}
for key,row in wh_wise_data.items():
item = key[0]
if not item_aggregated_data.get(item):
item_aggregated_data.setdefault(item, {
"details": frappe._dict(),
"fifo_queue": [],
"qty_after_transaction": 0.0,
"total_qty": 0.0
})
item_row = item_aggregated_data.get(item)
item_row["details"].update(row["details"])
item_row["fifo_queue"].extend(row["fifo_queue"])
item_row["qty_after_transaction"] += flt(row["qty_after_transaction"])
item_row["total_qty"] += flt(row["total_qty"])
item_row["has_serial_no"] = row["has_serial_no"]
return item_aggregated_data
def __get_stock_ledger_entries(self) -> List[Dict]:
sle = frappe.qb.DocType("Stock Ledger Entry")
item = self.__get_item_query() # used as derived table in sle query

View File

@@ -15,6 +15,7 @@ Here, the balance qty is 70.
50 qty is (today-the 1st) days old
20 qty is (today-the 2nd) days old
> Note: We generate FIFO slots warehouse wise as stock reconciliations from different warehouses can cause incorrect values.
### Calculation of FIFO Slots
#### Case 1: Outward from sufficient balance qty

View File

@@ -15,11 +15,12 @@ class TestStockAgeing(ERPNextTestCase):
)
def test_normal_inward_outward_queue(self):
"Reference: Case 1 in stock_ageing_fifo_logic.md"
"Reference: Case 1 in stock_ageing_fifo_logic.md (same wh)"
sle = [
frappe._dict(
name="Flask Item",
actual_qty=30, qty_after_transaction=30,
warehouse="WH 1",
posting_date="2021-12-01", voucher_type="Stock Entry",
voucher_no="001",
has_serial_no=False, serial_no=None
@@ -27,6 +28,7 @@ class TestStockAgeing(ERPNextTestCase):
frappe._dict(
name="Flask Item",
actual_qty=20, qty_after_transaction=50,
warehouse="WH 1",
posting_date="2021-12-02", voucher_type="Stock Entry",
voucher_no="002",
has_serial_no=False, serial_no=None
@@ -34,6 +36,7 @@ class TestStockAgeing(ERPNextTestCase):
frappe._dict(
name="Flask Item",
actual_qty=(-10), qty_after_transaction=40,
warehouse="WH 1",
posting_date="2021-12-03", voucher_type="Stock Entry",
voucher_no="003",
has_serial_no=False, serial_no=None
@@ -50,11 +53,12 @@ class TestStockAgeing(ERPNextTestCase):
self.assertEqual(queue[0][0], 20.0)
def test_insufficient_balance(self):
"Reference: Case 3 in stock_ageing_fifo_logic.md"
"Reference: Case 3 in stock_ageing_fifo_logic.md (same wh)"
sle = [
frappe._dict(
name="Flask Item",
actual_qty=(-30), qty_after_transaction=(-30),
warehouse="WH 1",
posting_date="2021-12-01", voucher_type="Stock Entry",
voucher_no="001",
has_serial_no=False, serial_no=None
@@ -62,6 +66,7 @@ class TestStockAgeing(ERPNextTestCase):
frappe._dict(
name="Flask Item",
actual_qty=20, qty_after_transaction=(-10),
warehouse="WH 1",
posting_date="2021-12-02", voucher_type="Stock Entry",
voucher_no="002",
has_serial_no=False, serial_no=None
@@ -69,6 +74,7 @@ class TestStockAgeing(ERPNextTestCase):
frappe._dict(
name="Flask Item",
actual_qty=20, qty_after_transaction=10,
warehouse="WH 1",
posting_date="2021-12-03", voucher_type="Stock Entry",
voucher_no="003",
has_serial_no=False, serial_no=None
@@ -76,6 +82,7 @@ class TestStockAgeing(ERPNextTestCase):
frappe._dict(
name="Flask Item",
actual_qty=10, qty_after_transaction=20,
warehouse="WH 1",
posting_date="2021-12-03", voucher_type="Stock Entry",
voucher_no="004",
has_serial_no=False, serial_no=None
@@ -91,11 +98,16 @@ class TestStockAgeing(ERPNextTestCase):
self.assertEqual(queue[0][0], 10.0)
self.assertEqual(queue[1][0], 10.0)
def test_stock_reconciliation(self):
def test_basic_stock_reconciliation(self):
"""
Ledger (same wh): [+30, reco reset >> 50, -10]
Bal: 40
"""
sle = [
frappe._dict(
name="Flask Item",
actual_qty=30, qty_after_transaction=30,
warehouse="WH 1",
posting_date="2021-12-01", voucher_type="Stock Entry",
voucher_no="001",
has_serial_no=False, serial_no=None
@@ -103,6 +115,7 @@ class TestStockAgeing(ERPNextTestCase):
frappe._dict(
name="Flask Item",
actual_qty=0, qty_after_transaction=50,
warehouse="WH 1",
posting_date="2021-12-02", voucher_type="Stock Reconciliation",
voucher_no="002",
has_serial_no=False, serial_no=None
@@ -110,6 +123,7 @@ class TestStockAgeing(ERPNextTestCase):
frappe._dict(
name="Flask Item",
actual_qty=(-10), qty_after_transaction=40,
warehouse="WH 1",
posting_date="2021-12-03", voucher_type="Stock Entry",
voucher_no="003",
has_serial_no=False, serial_no=None
@@ -122,5 +136,112 @@ class TestStockAgeing(ERPNextTestCase):
queue = result["fifo_queue"]
self.assertEqual(result["qty_after_transaction"], result["total_qty"])
self.assertEqual(result["total_qty"], 40.0)
self.assertEqual(queue[0][0], 20.0)
self.assertEqual(queue[1][0], 20.0)
def test_sequential_stock_reco_same_warehouse(self):
"""
Test back to back stock recos (same warehouse).
Ledger: [reco opening >> +1000, reco reset >> 400, -10]
Bal: 390
"""
sle = [
frappe._dict(
name="Flask Item",
actual_qty=0, qty_after_transaction=1000,
warehouse="WH 1",
posting_date="2021-12-01", voucher_type="Stock Reconciliation",
voucher_no="002",
has_serial_no=False, serial_no=None
),
frappe._dict(
name="Flask Item",
actual_qty=0, qty_after_transaction=400,
warehouse="WH 1",
posting_date="2021-12-02", voucher_type="Stock Reconciliation",
voucher_no="003",
has_serial_no=False, serial_no=None
),
frappe._dict(
name="Flask Item",
actual_qty=(-10), qty_after_transaction=390,
warehouse="WH 1",
posting_date="2021-12-03", voucher_type="Stock Entry",
voucher_no="003",
has_serial_no=False, serial_no=None
)
]
slots = FIFOSlots(self.filters, sle).generate()
result = slots["Flask Item"]
queue = result["fifo_queue"]
self.assertEqual(result["qty_after_transaction"], result["total_qty"])
self.assertEqual(result["total_qty"], 390.0)
self.assertEqual(queue[0][0], 390.0)
def test_sequential_stock_reco_different_warehouse(self):
"""
Ledger:
WH | Voucher | Qty
-------------------
WH1 | Reco | 1000
WH2 | Reco | 400
WH1 | SE | -10
Bal: WH1 bal + WH2 bal = 990 + 400 = 1390
"""
sle = [
frappe._dict(
name="Flask Item",
actual_qty=0, qty_after_transaction=1000,
warehouse="WH 1",
posting_date="2021-12-01", voucher_type="Stock Reconciliation",
voucher_no="002",
has_serial_no=False, serial_no=None
),
frappe._dict(
name="Flask Item",
actual_qty=0, qty_after_transaction=400,
warehouse="WH 2",
posting_date="2021-12-02", voucher_type="Stock Reconciliation",
voucher_no="003",
has_serial_no=False, serial_no=None
),
frappe._dict(
name="Flask Item",
actual_qty=(-10), qty_after_transaction=990,
warehouse="WH 1",
posting_date="2021-12-03", voucher_type="Stock Entry",
voucher_no="004",
has_serial_no=False, serial_no=None
)
]
item_wise_slots, item_wh_wise_slots = generate_item_and_item_wh_wise_slots(
filters=self.filters,sle=sle
)
# test without 'show_warehouse_wise_stock'
item_result = item_wise_slots["Flask Item"]
queue = item_result["fifo_queue"]
self.assertEqual(item_result["qty_after_transaction"], item_result["total_qty"])
self.assertEqual(item_result["total_qty"], 1390.0)
self.assertEqual(queue[0][0], 990.0)
self.assertEqual(queue[1][0], 400.0)
# test with 'show_warehouse_wise_stock' checked
item_wh_balances = [item_wh_wise_slots.get(i).get("qty_after_transaction") for i in item_wh_wise_slots]
self.assertEqual(sum(item_wh_balances), item_result["qty_after_transaction"])
def generate_item_and_item_wh_wise_slots(filters, sle):
"Return results with and without 'show_warehouse_wise_stock'"
item_wise_slots = FIFOSlots(filters, sle).generate()
filters.show_warehouse_wise_stock = True
item_wh_wise_slots = FIFOSlots(filters, sle).generate()
filters.show_warehouse_wise_stock = False
return item_wise_slots, item_wh_wise_slots

View File

@@ -104,6 +104,7 @@ def get_columns():
{"label": _("Incoming Rate"), "fieldname": "incoming_rate", "fieldtype": "Currency", "width": 110, "options": "Company:company:default_currency", "convertible": "rate"},
{"label": _("Valuation Rate"), "fieldname": "valuation_rate", "fieldtype": "Currency", "width": 110, "options": "Company:company:default_currency", "convertible": "rate"},
{"label": _("Balance Value"), "fieldname": "stock_value", "fieldtype": "Currency", "width": 110, "options": "Company:company:default_currency"},
{"label": _("Value Change"), "fieldname": "stock_value_difference", "fieldtype": "Currency", "width": 110, "options": "Company:company:default_currency"},
{"label": _("Voucher Type"), "fieldname": "voucher_type", "width": 110},
{"label": _("Voucher #"), "fieldname": "voucher_no", "fieldtype": "Dynamic Link", "options": "voucher_type", "width": 100},
{"label": _("Batch"), "fieldname": "batch_no", "fieldtype": "Link", "options": "Batch", "width": 100},

View File

@@ -3,7 +3,7 @@
import frappe
from frappe.utils import cstr, flt, nowdate, nowtime
from frappe.utils import cstr, flt, now, nowdate, nowtime
from erpnext.controllers.stock_controller import create_repost_item_valuation_entry
from erpnext.stock.utils import update_bin
@@ -175,6 +175,7 @@ def update_bin_qty(item_code, warehouse, qty_dict=None):
bin.set(field, flt(value))
mismatch = True
bin.modified = now()
if mismatch:
bin.set_projected_qty()
bin.db_update()