Repost item valuation (#24031)
* feat: Reposting logic for future finished/transferred item
* feat: added fields to identify needs to recalculate rate while reposting
* refactor: Set rate for outgoing and finished items
* refactor: Arranged fields in Stock Entry item table and added fields to identify finished and scrap item
* refactor: Arranged fields in Stock Entry item table and added fields to identify finished and scrap item
* refactor: Get outgoing rate for purchase return
* refactor: Get incoming rate for sales return
* test: Added tests for reposting valuation of transferred/finished/returned items
* feat: added incoming rate field in DN, SI and Packed Item table
* feat: get incoming rate for returned item
* fix: no error while getting valuation rate in stock entry
* fix: update stock ledger for DN and SI
* feat: update item valuation rate in PR and PI based on supplied items cost
* feat: SLE reposting logic for sales return and subcontracted item with test cases
* feat: update qty in future sle
* feat: repost future sle and gle via Repost Item Valuation
* fix: Skip unwanted function calling while reposting
* fix: repost sle for specific item and warehouse
* test: Modified tests for backdated stock reco
* fix: ignore cancelled sle in few methods
* feat: role allowed to do backdated entry
* feat: Show reposting status on stock valuation related reports
* fix: minor fixes
* fix: fixed sider issues
* fix: serial no fix related to immutable ledger
* fix: Test cases fixes related to perpetual inventory
* fix: Test cases fixed
* fix: Fixed reposting on cancel and test cases
* feat: Restart reposting item valuation
* refactor: Code cleanup using small functions and test case fixes
* fix: minor fixes
* fix: Raise on error while reposting item valuation
* fix: minor fix
* fix: Tests fixed
* fix: skip some validation ig gle made from reposting
* fix: test fixes
* fix: debugging stock and account validation
* fix: debugging stock and account validation
* fix: debugging travis for stock and account sync validation
* fix: debugging travis
* fix: debugging travis
* fix: debugging travis
diff --git a/erpnext/accounts/doctype/account/test_account.py b/erpnext/accounts/doctype/account/test_account.py
index 0605d89..113bea0 100644
--- a/erpnext/accounts/doctype/account/test_account.py
+++ b/erpnext/accounts/doctype/account/test_account.py
@@ -172,7 +172,7 @@
frappe.delete_doc("Account", doc)
-def _make_test_records(verbose):
+def _make_test_records(verbose=None):
from frappe.test_runner import make_test_objects
accounts = [
diff --git a/erpnext/accounts/doctype/coupon_code/test_coupon_code.py b/erpnext/accounts/doctype/coupon_code/test_coupon_code.py
index 340b9dd..622bd33 100644
--- a/erpnext/accounts/doctype/coupon_code/test_coupon_code.py
+++ b/erpnext/accounts/doctype/coupon_code/test_coupon_code.py
@@ -28,22 +28,22 @@
"item_group": "_Test Item Group",
"item_name": "_Test Tesla Car",
"apply_warehouse_wise_reorder_level": 0,
- "warehouse":"Stores - TCP1",
+ "warehouse":"Stores - _TC",
"gst_hsn_code": "999800",
"valuation_rate": 5000,
"standard_rate":5000,
"item_defaults": [{
- "company": "_Test Company with perpetual inventory",
- "default_warehouse": "Stores - TCP1",
+ "company": "_Test Company",
+ "default_warehouse": "Stores - _TC",
"default_price_list":"_Test Price List",
- "expense_account": "Cost of Goods Sold - TCP1",
- "buying_cost_center": "Main - TCP1",
- "selling_cost_center": "Main - TCP1",
- "income_account": "Sales - TCP1"
+ "expense_account": "Cost of Goods Sold - _TC",
+ "buying_cost_center": "Main - _TC",
+ "selling_cost_center": "Main - _TC",
+ "income_account": "Sales - _TC"
}],
"show_in_website": 1,
"route":"-test-tesla-car",
- "website_warehouse": "Stores - TCP1"
+ "website_warehouse": "Stores - _TC"
})
item.insert()
# create test item price
@@ -65,12 +65,12 @@
"items": [{
"item_code": "_Test Tesla Car"
}],
- "warehouse":"Stores - TCP1",
+ "warehouse":"Stores - _TC",
"coupon_code_based":1,
"selling": 1,
"rate_or_discount": "Discount Percentage",
"discount_percentage": 30,
- "company": "_Test Company with perpetual inventory",
+ "company": "_Test Company",
"currency":"INR",
"for_price_list":"_Test Price List"
})
@@ -85,7 +85,7 @@
})
sales_partner.insert()
# create test item coupon code
- if not frappe.db.exists("Coupon Code","SAVE30"):
+ if not frappe.db.exists("Coupon Code", "SAVE30"):
coupon_code = frappe.get_doc({
"doctype": "Coupon Code",
"coupon_name":"SAVE30",
@@ -102,35 +102,27 @@
test_create_test_data()
def tearDown(self):
- frappe.set_user("Administrator")
+ frappe.set_user("Administrator")
- def test_1_check_coupon_code_used_before_so(self):
- coupon_code = frappe.get_doc("Coupon Code", frappe.db.get_value("Coupon Code", {"coupon_name":"SAVE30"}))
- # reset used coupon code count
- coupon_code.used=0
- coupon_code.save()
- # check no coupon code is used before sales order is made
- self.assertEqual(coupon_code.get("used"),0)
+ def test_sales_order_with_coupon_code(self):
+ frappe.db.set_value("Coupon Code", "SAVE30", "used", 0)
- def test_2_sales_order_with_coupon_code(self):
- so = make_sales_order(company='_Test Company with perpetual inventory', warehouse='Stores - TCP1',
- customer="_Test Customer", selling_price_list="_Test Price List", item_code="_Test Tesla Car", rate=5000,qty=1,
+ so = make_sales_order(company='_Test Company', warehouse='Stores - _TC',
+ customer="_Test Customer", selling_price_list="_Test Price List",
+ item_code="_Test Tesla Car", rate=5000, qty=1,
do_not_submit=True)
- so = frappe.get_doc('Sales Order', so.name)
- # check item price before coupon code is applied
self.assertEqual(so.items[0].rate, 5000)
+
so.coupon_code='SAVE30'
so.sales_partner='_Test Coupon Partner'
so.save()
+
# check item price after coupon code is applied
self.assertEqual(so.items[0].rate, 3500)
- so.submit()
- def test_3_check_coupon_code_used_after_so(self):
- doc = frappe.get_doc("Coupon Code", frappe.db.get_value("Coupon Code", {"coupon_name":"SAVE30"}))
- # check no coupon code is used before sales order is made
- self.assertEqual(doc.get("used"),1)
+ so.submit()
+ self.assertEqual(frappe.db.get_value("Coupon Code", "SAVE30", "used"), 1)
diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py
index def9ed6..c441274 100644
--- a/erpnext/accounts/doctype/gl_entry/gl_entry.py
+++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py
@@ -30,20 +30,22 @@
self.pl_must_have_cost_center()
self.validate_cost_center()
- self.check_pl_account()
- self.validate_party()
- self.validate_currency()
+ if not self.flags.from_repost:
+ self.check_pl_account()
+ self.validate_party()
+ self.validate_currency()
- def on_update_with_args(self, adv_adj, update_outstanding = 'Yes'):
- self.validate_account_details(adv_adj)
- self.validate_dimensions_for_pl_and_bs()
+ def on_update_with_args(self, adv_adj, update_outstanding = 'Yes', from_repost=False):
+ if not from_repost:
+ self.validate_account_details(adv_adj)
+ self.validate_dimensions_for_pl_and_bs()
validate_frozen_account(self.account, adv_adj)
validate_balance_type(self.account, adv_adj)
# Update outstanding amt on against voucher
if self.against_voucher_type in ['Journal Entry', 'Sales Invoice', 'Purchase Invoice', 'Fees'] \
- and self.against_voucher and update_outstanding == 'Yes':
+ and self.against_voucher and update_outstanding == 'Yes' and not from_repost:
update_outstanding_amt(self.account, self.party_type, self.party, self.against_voucher_type,
self.against_voucher)
@@ -106,8 +108,8 @@
from tabAccount where name=%s""", self.account, as_dict=1)[0]
if ret.is_group==1:
- frappe.throw(_('''{0} {1}: Account {2} is a Group Account and group accounts cannot be used in
- transactions''').format(self.voucher_type, self.voucher_no, self.account))
+ frappe.throw(_('''{0} {1}: Account {2} is a Group Account and group accounts cannot be used in transactions''')
+ .format(self.voucher_type, self.voucher_no, self.account))
if ret.docstatus==2:
frappe.throw(_("{0} {1}: Account {2} is inactive")
@@ -136,8 +138,8 @@
.format(self.voucher_type, self.voucher_no, self.cost_center, self.company))
if self.cost_center and _check_is_group():
- frappe.throw(_("""{0} {1}: Cost Center {2} is a group cost center and group cost centers cannot
- be used in transactions""").format(self.voucher_type, self.voucher_no, frappe.bold(self.cost_center)))
+ frappe.throw(_("""{0} {1}: Cost Center {2} is a group cost center and group cost centers cannot be used in transactions""")
+ .format(self.voucher_type, self.voucher_no, frappe.bold(self.cost_center)))
def validate_party(self):
validate_party_frozen_disabled(self.party_type, self.party)
diff --git a/erpnext/accounts/doctype/journal_entry/test_journal_entry.py b/erpnext/accounts/doctype/journal_entry/test_journal_entry.py
index 53c0758..1d2eacd 100644
--- a/erpnext/accounts/doctype/journal_entry/test_journal_entry.py
+++ b/erpnext/accounts/doctype/journal_entry/test_journal_entry.py
@@ -75,54 +75,40 @@
elif test_voucher.doctype in ["Sales Order", "Purchase Order"]:
# if test_voucher is a Sales Order/Purchase Order, test error on cancellation of test_voucher
+ frappe.db.set_value("Accounts Settings", "Accounts Settings",
+ "unlink_advance_payment_on_cancelation_of_order", 0)
submitted_voucher = frappe.get_doc(test_voucher.doctype, test_voucher.name)
self.assertRaises(frappe.LinkExistsError, submitted_voucher.cancel)
def test_jv_against_stock_account(self):
- from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory
- set_perpetual_inventory()
+ company = "_Test Company with perpetual inventory"
+ stock_account = get_inventory_account(company)
- jv = frappe.copy_doc({
- "cheque_date": nowdate(),
- "cheque_no": "33",
- "company": "_Test Company with perpetual inventory",
- "doctype": "Journal Entry",
- "accounts": [
- {
- "account": "Debtors - TCP1",
- "party_type": "Customer",
- "party": "_Test Customer",
- "credit_in_account_currency": 400.0,
- "debit_in_account_currency": 0.0,
- "doctype": "Journal Entry Account",
- "parentfield": "accounts",
- "cost_center": "Main - TCP1"
- },
- {
- "account": "_Test Bank - TCP1",
- "credit_in_account_currency": 0.0,
- "debit_in_account_currency": 400.0,
- "doctype": "Journal Entry Account",
- "parentfield": "accounts",
- "cost_center": "Main - TCP1"
- }
- ],
- "naming_series": "_T-Journal Entry-",
- "posting_date": nowdate(),
- "user_remark": "test",
- "voucher_type": "Bank Entry"
- })
-
- jv.get("accounts")[0].update({
- "account": get_inventory_account('_Test Company with perpetual inventory'),
- "company": "_Test Company with perpetual inventory",
- "party_type": None,
- "party": None
+ jv = frappe.new_doc("Journal Entry")
+ jv.company = company
+ jv.posting_date = nowdate()
+ jv.append("accounts", {
+ "account": stock_account,
+ "cost_center": "Main - TCP1",
+ "debit_in_account_currency": 100
})
+
+ jv.append("accounts", {
+ "account": "Stock Adjustment - TCP1",
+ "credit_in_account_currency": 100,
+ "cost_center": "Main - TCP1",
+ })
+ jv.insert()
- self.assertRaises(StockAccountInvalidTransaction, jv.submit)
- jv.cancel()
- set_perpetual_inventory(0)
+ from erpnext.accounts.utils import get_stock_and_account_balance
+ account_bal, stock_bal, warehouse_list = get_stock_and_account_balance(stock_account, nowdate(), company)
+
+ if account_bal == stock_bal:
+ self.assertRaises(StockAccountInvalidTransaction, jv.submit)
+ frappe.db.rollback()
+ else:
+ jv.submit()
+ jv.cancel()
def test_multi_currency(self):
jv = make_journal_entry("_Test Bank USD - _TC",
diff --git a/erpnext/accounts/doctype/loyalty_program/test_loyalty_program.py b/erpnext/accounts/doctype/loyalty_program/test_loyalty_program.py
index 5278d8b..3199488 100644
--- a/erpnext/accounts/doctype/loyalty_program/test_loyalty_program.py
+++ b/erpnext/accounts/doctype/loyalty_program/test_loyalty_program.py
@@ -8,12 +8,10 @@
from frappe.utils import today, cint, flt, getdate
from erpnext.accounts.doctype.loyalty_program.loyalty_program import get_loyalty_program_details_with_points
from erpnext.accounts.party import get_dashboard_info
-from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory
class TestLoyaltyProgram(unittest.TestCase):
@classmethod
def setUpClass(self):
- set_perpetual_inventory(0)
# create relevant item, customer, loyalty program, etc
create_records()
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index d94d261..b52678e 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -410,10 +410,13 @@
# this sequence because outstanding may get -negative
self.make_gl_entries()
+ if self.update_stock == 1:
+ self.repost_future_sle_and_gle()
+
self.update_project()
update_linked_doc(self.doctype, self.name, self.inter_company_invoice_reference)
- def make_gl_entries(self, gl_entries=None):
+ def make_gl_entries(self, gl_entries=None, from_repost=False):
if not gl_entries:
gl_entries = self.get_gl_entries()
@@ -421,7 +424,7 @@
update_outstanding = "No" if (cint(self.is_paid) or self.write_off_account) else "Yes"
if self.docstatus == 1:
- make_gl_entries(gl_entries, update_outstanding=update_outstanding, merge_entries=False)
+ make_gl_entries(gl_entries, update_outstanding=update_outstanding, merge_entries=False, from_repost=from_repost)
elif self.docstatus == 2:
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
@@ -436,9 +439,11 @@
self.auto_accounting_for_stock = erpnext.is_perpetual_inventory_enabled(self.company)
if self.auto_accounting_for_stock:
self.stock_received_but_not_billed = self.get_company_default("stock_received_but_not_billed")
+ self.expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation")
else:
self.stock_received_but_not_billed = None
- self.expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation")
+ self.expenses_included_in_valuation = None
+
self.negative_expense_to_be_booked = 0.0
gl_entries = []
@@ -452,7 +457,7 @@
self.make_internal_transfer_gl_entries(gl_entries)
gl_entries = make_regional_gl_entries(gl_entries, self)
-
+
gl_entries = merge_similar_entries(gl_entries)
self.make_payment_gl_entries(gl_entries)
@@ -994,11 +999,15 @@
self.delete_auto_created_batches()
self.make_gl_entries_on_cancel()
+
+ if self.update_stock == 1:
+ self.repost_future_sle_and_gle()
+
self.update_project()
frappe.db.set(self, 'status', 'Cancelled')
unlink_inter_company_doc(self.doctype, self.name, self.inter_company_invoice_reference)
- self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry')
+ self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation')
def update_project(self):
project_list = []
diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
index f2499d2..c0506ba 100644
--- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
@@ -9,8 +9,7 @@
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
from frappe.utils import cint, flt, today, nowdate, add_days, getdate
import frappe.defaults
-from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory, \
- test_records as pr_test_records, make_purchase_receipt, get_taxes
+from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt, get_taxes
from erpnext.controllers.accounts_controller import get_payment_terms
from erpnext.exceptions import InvalidCurrency
from erpnext.stock.doctype.stock_entry.test_stock_entry import get_qty_after_transaction
@@ -33,13 +32,10 @@
def test_gl_entries_without_perpetual_inventory(self):
frappe.db.set_value("Company", "_Test Company", "round_off_account", "Round Off - _TC")
- wrapper = frappe.copy_doc(test_records[0])
- set_perpetual_inventory(0, wrapper.company)
- self.assertTrue(not cint(erpnext.is_perpetual_inventory_enabled(wrapper.company)))
- wrapper.insert()
- wrapper.submit()
- wrapper.load_from_db()
- dl = wrapper
+ pi = frappe.copy_doc(test_records[0])
+ self.assertTrue(not cint(erpnext.is_perpetual_inventory_enabled(pi.company)))
+ pi.insert()
+ pi.submit()
expected_gl_entries = {
"_Test Payable - _TC": [0, 1512.0],
@@ -54,12 +50,16 @@
"Round Off - _TC": [0, 0.3]
}
gl_entries = frappe.db.sql("""select account, debit, credit from `tabGL Entry`
- where voucher_type = 'Purchase Invoice' and voucher_no = %s""", dl.name, as_dict=1)
+ where voucher_type = 'Purchase Invoice' and voucher_no = %s""", pi.name, as_dict=1)
for d in gl_entries:
self.assertEqual([d.debit, d.credit], expected_gl_entries.get(d.account))
def test_gl_entries_with_perpetual_inventory(self):
- pi = make_purchase_invoice(company="_Test Company with perpetual inventory", supplier_warehouse="Work In Progress - TCP1", warehouse= "Stores - TCP1", cost_center = "Main - TCP1", expense_account ="_Test Account Cost for Goods Sold - TCP1", get_taxes_and_charges=True, qty=10)
+ pi = make_purchase_invoice(company="_Test Company with perpetual inventory",
+ warehouse= "Stores - TCP1", cost_center = "Main - TCP1",
+ expense_account ="_Test Account Cost for Goods Sold - TCP1",
+ get_taxes_and_charges=True, qty=10)
+
self.assertTrue(cint(erpnext.is_perpetual_inventory_enabled(pi.company)), 1)
self.check_gle_for_pi(pi.name)
@@ -198,8 +198,6 @@
pr = make_purchase_receipt(company="_Test Company with perpetual inventory", supplier_warehouse="Work In Progress - TCP1", warehouse= "Stores - TCP1", cost_center = "Main - TCP1", get_taxes_and_charges=True,)
- self.assertTrue(cint(erpnext.is_perpetual_inventory_enabled(pr.company)), 1)
-
pi = make_purchase_invoice(company="_Test Company with perpetual inventory", supplier_warehouse="Work In Progress - TCP1", warehouse= "Stores - TCP1", cost_center = "Main - TCP1", expense_account ="_Test Account Cost for Goods Sold - TCP1", get_taxes_and_charges=True, qty=10,do_not_save= "True")
for d in pi.items:
@@ -247,17 +245,11 @@
self.assertRaises(frappe.CannotChangeConstantError, pi.save)
- def test_gl_entries_with_aia_for_non_stock_items(self):
- pi = frappe.copy_doc(test_records[1])
- set_perpetual_inventory(1, pi.company)
- self.assertTrue(cint(erpnext.is_perpetual_inventory_enabled(pi.company)), 1)
- pi.get("items")[0].item_code = "_Test Non Stock Item"
- pi.get("items")[0].expense_account = "_Test Account Cost for Goods Sold - _TC"
- pi.get("taxes").pop(0)
- pi.get("taxes").pop(1)
- pi.insert()
- pi.submit()
- pi.load_from_db()
+ def test_gl_entries_for_non_stock_items_with_perpetual_inventory(self):
+ pi = make_purchase_invoice(item_code = "_Test Non Stock Item",
+ company = "_Test Company with perpetual inventory", warehouse= "Stores - TCP1",
+ cost_center = "Main - TCP1", expense_account ="_Test Account Cost for Goods Sold - TCP1")
+
self.assertTrue(pi.status, "Unpaid")
gl_entries = frappe.db.sql("""select account, debit, credit
@@ -265,17 +257,15 @@
order by account asc""", pi.name, as_dict=1)
self.assertTrue(gl_entries)
- expected_values = sorted([
- ["_Test Payable - _TC", 0, 620],
- ["_Test Account Cost for Goods Sold - _TC", 500.0, 0],
- ["_Test Account VAT - _TC", 120.0, 0],
- ])
+ expected_values = [
+ ["_Test Account Cost for Goods Sold - TCP1", 250.0, 0],
+ ["Creditors - TCP1", 0, 250]
+ ]
for i, gle in enumerate(gl_entries):
self.assertEqual(expected_values[i][0], gle.account)
self.assertEqual(expected_values[i][1], gle.debit)
self.assertEqual(expected_values[i][2], gle.credit)
- set_perpetual_inventory(0, pi.company)
def test_purchase_invoice_calculation(self):
pi = frappe.copy_doc(test_records[0])
@@ -457,12 +447,13 @@
pi.cancel()
self.assertEqual(frappe.db.get_value("Project", "_Test Project", "total_purchase_cost"), existing_purchase_cost)
- def test_return_purchase_invoice(self):
- set_perpetual_inventory()
+ def test_return_purchase_invoice_with_perpetual_inventory(self):
+ pi = make_purchase_invoice(company = "_Test Company with perpetual inventory", warehouse= "Stores - TCP1",
+ cost_center = "Main - TCP1", expense_account ="_Test Account Cost for Goods Sold - TCP1")
- pi = make_purchase_invoice()
-
- return_pi = make_purchase_invoice(is_return=1, return_against=pi.name, qty=-2)
+ return_pi = make_purchase_invoice(is_return=1, return_against=pi.name, qty=-2,
+ company = "_Test Company with perpetual inventory", warehouse= "Stores - TCP1",
+ cost_center = "Main - TCP1", expense_account ="_Test Account Cost for Goods Sold - TCP1")
# check gl entries for return
@@ -473,19 +464,15 @@
self.assertTrue(gl_entries)
expected_values = {
- "Creditors - _TC": [100.0, 0.0],
- "Stock Received But Not Billed - _TC": [0.0, 100.0],
+ "Creditors - TCP1": [100.0, 0.0],
+ "Stock Received But Not Billed - TCP1": [0.0, 100.0],
}
for gle in gl_entries:
self.assertEqual(expected_values[gle.account][0], gle.debit)
self.assertEqual(expected_values[gle.account][1], gle.credit)
- set_perpetual_inventory(0)
-
def test_multi_currency_gle(self):
- set_perpetual_inventory(0)
-
pi = make_purchase_invoice(supplier="_Test Supplier USD", credit_to="_Test Payable USD - _TC",
currency="USD", conversion_rate=50)
@@ -640,10 +627,9 @@
self.assertEqual(len(pi.get("supplied_items")), 2)
rm_supp_cost = sum([d.amount for d in pi.get("supplied_items")])
- self.assertEqual(pi.get("items")[0].rm_supp_cost, flt(rm_supp_cost, 2))
+ self.assertEqual(flt(pi.get("items")[0].rm_supp_cost, 2), flt(rm_supp_cost, 2))
def test_rejected_serial_no(self):
- set_perpetual_inventory(0)
pi = make_purchase_invoice(item_code="_Test Serialized Item With Series", received_qty=2, qty=1,
rejected_qty=1, rate=500, update_stock=1,
rejected_warehouse = "_Test Rejected Warehouse - _TC")
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index ca6f22c..50734c8 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -179,6 +179,9 @@
# this sequence because outstanding may get -ve
self.make_gl_entries()
+
+ if self.update_stock == 1:
+ self.repost_future_sle_and_gle()
if not self.is_return:
self.update_billing_status_for_zero_amount_refdoc("Delivery Note")
@@ -258,6 +261,10 @@
self.update_stock_ledger()
self.make_gl_entries_on_cancel()
+
+ if self.update_stock == 1:
+ self.repost_future_sle_and_gle()
+
frappe.db.set(self, 'status', 'Cancelled')
if frappe.db.get_single_value('Selling Settings', 'sales_update_frequency') == "Each Transaction":
@@ -279,7 +286,7 @@
if "Healthcare" in active_domains:
manage_invoice_submit_cancel(self, "on_cancel")
- self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry')
+ self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation')
def update_status_updater_args(self):
if cint(self.update_stock):
@@ -722,22 +729,20 @@
if d.delivery_note and frappe.db.get_value("Delivery Note", d.delivery_note, "docstatus") != 1:
throw(_("Delivery Note {0} is not submitted").format(d.delivery_note))
- def make_gl_entries(self, gl_entries=None):
- from erpnext.accounts.general_ledger import make_reverse_gl_entries
+ def make_gl_entries(self, gl_entries=None, from_repost=False):
+ from erpnext.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries
auto_accounting_for_stock = erpnext.is_perpetual_inventory_enabled(self.company)
if not gl_entries:
gl_entries = self.get_gl_entries()
if gl_entries:
- from erpnext.accounts.general_ledger import make_gl_entries
-
# if POS and amount is written off, updating outstanding amt after posting all gl entries
update_outstanding = "No" if (cint(self.is_pos) or self.write_off_account or
cint(self.redeem_loyalty_points)) else "Yes"
if self.docstatus == 1:
- make_gl_entries(gl_entries, update_outstanding=update_outstanding, merge_entries=False)
+ make_gl_entries(gl_entries, update_outstanding=update_outstanding, merge_entries=False, from_repost=from_repost)
elif self.docstatus == 2:
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
diff --git a/erpnext/accounts/doctype/sales_invoice/test_records.json b/erpnext/accounts/doctype/sales_invoice/test_records.json
index 11ebe6a..ee6419d 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_records.json
+++ b/erpnext/accounts/doctype/sales_invoice/test_records.json
@@ -17,7 +17,8 @@
"description": "138-CMS Shoe",
"doctype": "Sales Invoice Item",
"income_account": "Sales - _TC",
- "expense_account": "_Test Account Cost for Goods Sold - _TC",
+ "expense_account": "_Test Account Cost for Goods Sold - _TC",
+ "item_code": "138-CMS Shoe",
"item_name": "138-CMS Shoe",
"parentfield": "items",
"qty": 1.0,
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index 22a4f33..ceb7907 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -10,7 +10,6 @@
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry, get_qty_after_transaction
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import unlink_payment_on_cancel_of_invoice
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
-from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory
from erpnext.exceptions import InvalidAccountCurrency, InvalidCurrency
from erpnext.stock.doctype.serial_no.serial_no import SerialNoWarehouseError
from frappe.model.naming import make_autoname
@@ -659,7 +658,6 @@
def test_sales_invoice_gl_entry_without_perpetual_inventory(self):
si = frappe.copy_doc(test_records[1])
- set_perpetual_inventory(0, si.company)
si.insert()
si.submit()
@@ -815,7 +813,6 @@
frappe.db.sql("delete from `tabPOS Profile`")
def test_pos_si_without_payment(self):
- set_perpetual_inventory()
make_pos_profile()
pos = copy.deepcopy(test_records[1])
@@ -829,9 +826,8 @@
self.assertRaises(frappe.ValidationError, si.submit)
def test_sales_invoice_gl_entry_with_perpetual_inventory_no_item_code(self):
- set_perpetual_inventory()
-
- si = frappe.get_doc(test_records[1])
+ si = create_sales_invoice(company="_Test Company with perpetual inventory", debit_to = "Debtors - TCP1",
+ income_account="Sales - TCP1", cost_center = "Main - TCP1", do_not_save=True)
si.get("items")[0].item_code = None
si.insert()
si.submit()
@@ -842,24 +838,16 @@
self.assertTrue(gl_entries)
expected_values = dict((d[0], d) for d in [
- [si.debit_to, 630.0, 0.0],
- [test_records[1]["items"][0]["income_account"], 0.0, 500.0],
- [test_records[1]["taxes"][0]["account_head"], 0.0, 80.0],
- [test_records[1]["taxes"][1]["account_head"], 0.0, 50.0],
+ ["Debtors - TCP1", 100.0, 0.0],
+ ["Sales - TCP1", 0.0, 100.0]
])
for i, gle in enumerate(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)
- set_perpetual_inventory(0)
-
def test_sales_invoice_gl_entry_with_perpetual_inventory_non_stock_item(self):
- set_perpetual_inventory()
- si = frappe.get_doc(test_records[1])
- si.get("items")[0].item_code = "_Test Non Stock Item"
- si.insert()
- si.submit()
+ si = create_sales_invoice(item="_Test Non Stock Item")
gl_entries = frappe.db.sql("""select account, debit, credit
from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s
@@ -867,17 +855,14 @@
self.assertTrue(gl_entries)
expected_values = dict((d[0], d) for d in [
- [si.debit_to, 630.0, 0.0],
- [test_records[1]["items"][0]["income_account"], 0.0, 500.0],
- [test_records[1]["taxes"][0]["account_head"], 0.0, 80.0],
- [test_records[1]["taxes"][1]["account_head"], 0.0, 50.0],
+ [si.debit_to, 100.0, 0.0],
+ [test_records[1]["items"][0]["income_account"], 0.0, 100.0]
])
for i, gle in enumerate(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)
- set_perpetual_inventory(0)
def _insert_purchase_receipt(self):
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import test_records \
@@ -1106,7 +1091,6 @@
self.assertEqual(si.grand_total, 859.43)
def test_multi_currency_gle(self):
- set_perpetual_inventory(0)
si = create_sales_invoice(customer="_Test Customer USD", debit_to="_Test Receivable USD - _TC",
currency="USD", conversion_rate=50)
@@ -1776,64 +1760,69 @@
si.submit()
target_doc = make_inter_company_transaction("Sales Invoice", si.name)
+ target_doc.items[0].update({
+ "expense_account": "Cost of Goods Sold - _TC1",
+ "cost_center": "Main - _TC1",
+ "warehouse": "Stores - _TC1"
+ })
target_doc.submit()
self.assertEqual(target_doc.company, "_Test Company 1")
self.assertEqual(target_doc.supplier, "_Test Internal Supplier")
- def test_internal_transfer_gl_entry(self):
- ## Create internal transfer account
- account = create_account(account_name="Unrealized Profit",
- parent_account="Current Liabilities - TCP1", company="_Test Company with perpetual inventory")
+ # def test_internal_transfer_gl_entry(self):
+ # ## Create internal transfer account
+ # account = create_account(account_name="Unrealized Profit",
+ # parent_account="Current Liabilities - TCP1", company="_Test Company with perpetual inventory")
- frappe.db.set_value('Company', '_Test Company with perpetual inventory',
- 'unrealized_profit_loss_account', account)
+ # frappe.db.set_value('Company', '_Test Company with perpetual inventory',
+ # 'unrealized_profit_loss_account', account)
- customer = create_internal_customer("_Test Internal Customer 2", "_Test Company with perpetual inventory",
- "_Test Company with perpetual inventory")
+ # customer = create_internal_customer("_Test Internal Customer 2", "_Test Company with perpetual inventory",
+ # "_Test Company with perpetual inventory")
- create_internal_supplier("_Test Internal Supplier 2", "_Test Company with perpetual inventory",
- "_Test Company with perpetual inventory")
+ # create_internal_supplier("_Test Internal Supplier 2", "_Test Company with perpetual inventory",
+ # "_Test Company with perpetual inventory")
- si = create_sales_invoice(
- company = "_Test Company with perpetual inventory",
- customer = customer,
- debit_to = "Debtors - TCP1",
- warehouse = "Stores - TCP1",
- income_account = "Sales - TCP1",
- expense_account = "Cost of Goods Sold - TCP1",
- cost_center = "Main - TCP1",
- currency = "INR",
- do_not_save = 1
- )
+ # si = create_sales_invoice(
+ # company = "_Test Company with perpetual inventory",
+ # customer = customer,
+ # debit_to = "Debtors - TCP1",
+ # warehouse = "Stores - TCP1",
+ # income_account = "Sales - TCP1",
+ # expense_account = "Cost of Goods Sold - TCP1",
+ # cost_center = "Main - TCP1",
+ # currency = "INR",
+ # do_not_save = 1
+ # )
- si.selling_price_list = "_Test Price List Rest of the World"
- si.update_stock = 1
- si.items[0].target_warehouse = 'Work In Progress - TCP1'
- add_taxes(si)
- si.save()
- si.submit()
+ # si.selling_price_list = "_Test Price List Rest of the World"
+ # si.update_stock = 1
+ # si.items[0].target_warehouse = 'Work In Progress - TCP1'
+ # add_taxes(si)
+ # si.save()
+ # si.submit()
- target_doc = make_inter_company_transaction("Sales Invoice", si.name)
- target_doc.company = '_Test Company with perpetual inventory'
- target_doc.items[0].warehouse = 'Finished Goods - TCP1'
- add_taxes(target_doc)
- target_doc.save()
- target_doc.submit()
+ # target_doc = make_inter_company_transaction("Sales Invoice", si.name)
+ # target_doc.company = '_Test Company with perpetual inventory'
+ # target_doc.items[0].warehouse = 'Finished Goods - TCP1'
+ # add_taxes(target_doc)
+ # target_doc.save()
+ # target_doc.submit()
- si_gl_entries = [
- ["_Test Account Excise Duty - TCP1", 0.0, 12.0, nowdate()],
- ["Unrealized Profit - TCP1", 12.0, 0.0, nowdate()]
- ]
+ # si_gl_entries = [
+ # ["_Test Account Excise Duty - TCP1", 0.0, 12.0, nowdate()],
+ # ["Unrealized Profit - TCP1", 12.0, 0.0, nowdate()]
+ # ]
- check_gl_entries(self, si.name, si_gl_entries, add_days(nowdate(), -1))
+ # check_gl_entries(self, si.name, si_gl_entries, add_days(nowdate(), -1))
- pi_gl_entries = [
- ["_Test Account Excise Duty - TCP1", 12.0 , 0.0, nowdate()],
- ["Unrealized Profit - TCP1", 0.0, 12.0, nowdate()]
- ]
+ # pi_gl_entries = [
+ # ["_Test Account Excise Duty - TCP1", 12.0 , 0.0, nowdate()],
+ # ["Unrealized Profit - TCP1", 0.0, 12.0, nowdate()]
+ # ]
- check_gl_entries(self, target_doc.name, pi_gl_entries, add_days(nowdate(), -1))
+ # check_gl_entries(self, target_doc.name, pi_gl_entries, add_days(nowdate(), -1))
def test_eway_bill_json(self):
if not frappe.db.exists('Address', '_Test Address for Eway bill-Billing'):
@@ -1991,14 +1980,19 @@
si.append("items", {
"item_code": args.item or args.item_code or "_Test Item",
+ "item_name": args.item_name or "_Test Item",
+ "description": args.description or "_Test Item",
"gst_hsn_code": "999800",
"warehouse": args.warehouse or "_Test Warehouse - _TC",
"qty": args.qty or 1,
+ "uom": args.uom or "Nos",
+ "stock_uom": args.uom or "Nos",
"rate": args.rate if args.get("rate") is not None else 100,
"income_account": args.income_account or "Sales - _TC",
"expense_account": args.expense_account or "Cost of Goods Sold - _TC",
"cost_center": args.cost_center or "_Test Cost Center - _TC",
- "serial_no": args.serial_no
+ "serial_no": args.serial_no,
+ "conversion_factor": 1
})
if not args.do_not_save:
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 fb3dd6a..3695075 100644
--- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json
+++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"autoname": "hash",
"creation": "2013-06-04 11:02:19",
"doctype": "DocType",
@@ -51,6 +52,7 @@
"column_break_24",
"base_net_rate",
"base_net_amount",
+ "incoming_rate",
"drop_ship",
"delivered_by_supplier",
"accounting",
@@ -792,20 +794,28 @@
"options": "Project"
},
{
- "depends_on": "eval:parent.update_stock == 1",
- "fieldname": "sales_invoice_item",
- "fieldtype": "Data",
- "ignore_user_permissions": 1,
- "label": "Sales Invoice Item",
- "no_copy": 1,
- "print_hide": 1,
- "read_only": 1
- }
+ "depends_on": "eval:parent.update_stock == 1",
+ "fieldname": "sales_invoice_item",
+ "fieldtype": "Data",
+ "ignore_user_permissions": 1,
+ "label": "Sales Invoice Item",
+ "no_copy": 1,
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "incoming_rate",
+ "fieldtype": "Currency",
+ "label": "Incoming Rate",
+ "no_copy": 1,
+ "print_hide": 1,
+ "read_only": 1
+ }
],
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2020-08-20 11:24:41.749986",
+ "modified": "2020-09-23 19:59:04.879322",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice Item",
diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py
index 9a091bf..c7f0c87 100644
--- a/erpnext/accounts/general_ledger.py
+++ b/erpnext/accounts/general_ledger.py
@@ -15,13 +15,13 @@
class StockAccountInvalidTransaction(frappe.ValidationError): pass
class StockValueAndAccountBalanceOutOfSync(frappe.ValidationError): pass
-def make_gl_entries(gl_map, cancel=False, adv_adj=False, merge_entries=True, update_outstanding='Yes'):
+def make_gl_entries(gl_map, cancel=False, adv_adj=False, merge_entries=True, update_outstanding='Yes', from_repost=False):
if gl_map:
if not cancel:
validate_accounting_period(gl_map)
gl_map = process_gl_map(gl_map, merge_entries)
if gl_map and len(gl_map) > 1:
- save_entries(gl_map, adv_adj, update_outstanding)
+ save_entries(gl_map, adv_adj, update_outstanding, from_repost)
else:
frappe.throw(_("Incorrect number of General Ledger Entries found. You might have selected a wrong Account in the transaction."))
else:
@@ -119,8 +119,9 @@
if same_head:
return e
-def save_entries(gl_map, adv_adj, update_outstanding):
- validate_cwip_accounts(gl_map)
+def save_entries(gl_map, adv_adj, update_outstanding, from_repost=False):
+ if not from_repost:
+ validate_cwip_accounts(gl_map)
round_off_debit_credit(gl_map)
@@ -128,24 +129,24 @@
check_freezing_date(gl_map[0]["posting_date"], adv_adj)
for entry in gl_map:
- make_entry(entry, adv_adj, update_outstanding)
+ make_entry(entry, adv_adj, update_outstanding, from_repost)
- # check against budget
- validate_expense_against_budget(entry)
-
- validate_account_for_perpetual_inventory(gl_map)
+ if not from_repost:
+ validate_account_for_perpetual_inventory(gl_map)
-def make_entry(args, adv_adj, update_outstanding):
+def make_entry(args, adv_adj, update_outstanding, from_repost=False):
gle = frappe.new_doc("GL Entry")
gle.update(args)
gle.flags.ignore_permissions = 1
+ gle.flags.from_repost = from_repost
gle.insert()
- gle.run_method("on_update_with_args", adv_adj, update_outstanding)
+ gle.run_method("on_update_with_args", adv_adj, update_outstanding, from_repost)
gle.submit()
# check against budget
- validate_expense_against_budget(args)
+ if not from_repost:
+ validate_expense_against_budget(args)
def validate_account_for_perpetual_inventory(gl_map):
if cint(erpnext.is_perpetual_inventory_enabled(gl_map[0].company)):
@@ -161,7 +162,7 @@
# Always use current date to get stock and account balance as there can future entries for
# other items
account_bal, stock_bal, warehouse_list = get_stock_and_account_balance(account,
- getdate(), gl_map[0].company)
+ gl_map[0].posting_date, gl_map[0].company)
if gl_map[0].voucher_type=="Journal Entry":
# In case of Journal Entry, there are no corresponding SL entries,
@@ -176,8 +177,8 @@
currency=frappe.get_cached_value('Company', gl_map[0].company, "default_currency"))
diff = flt(stock_bal - account_bal, precision)
- error_reason = _("Stock Value ({0}) and Account Balance ({1}) are out of sync for account {2} and it's linked warehouses.").format(
- stock_bal, account_bal, frappe.bold(account))
+ error_reason = _("Stock Value ({0}) and Account Balance ({1}) are out of sync for account {2} and it's linked warehouses on {3}.").format(
+ stock_bal, account_bal, frappe.bold(account), gl_map[0].posting_date)
error_resolution = _("Please create adjustment Journal Entry for amount {0} ").format(frappe.bold(diff))
stock_adjustment_account = frappe.db.get_value("Company",gl_map[0].company,"stock_adjustment_account")
@@ -185,9 +186,10 @@
db_or_cr_stock_adjustment_account = ('debit_in_account_currency' if diff < 0 else 'credit_in_account_currency')
journal_entry_args = {
- 'accounts':[
- {'account': account, db_or_cr_warehouse_account : abs(diff)},
- {'account': stock_adjustment_account, db_or_cr_stock_adjustment_account : abs(diff) }]
+ 'accounts':[
+ {'account': account, db_or_cr_warehouse_account : abs(diff)},
+ {'account': stock_adjustment_account, db_or_cr_stock_adjustment_account : abs(diff)}
+ ]
}
frappe.msgprint(msg="""{0}<br></br>{1}<br></br>""".format(error_reason, error_resolution),
diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py
index 550aaef..540ac84 100644
--- a/erpnext/accounts/utils.py
+++ b/erpnext/accounts/utils.py
@@ -928,7 +928,7 @@
if expected_gle:
if not existing_gle or not compare_existing_and_expected_gle(existing_gle, expected_gle):
_delete_gl_entries(voucher_type, voucher_no)
- voucher_obj.make_gl_entries(gl_entries=expected_gle, repost_future_gle=False, from_repost=True)
+ voucher_obj.make_gl_entries(gl_entries=expected_gle, from_repost=True)
else:
_delete_gl_entries(voucher_type, voucher_no)
@@ -947,7 +947,10 @@
for d in frappe.db.sql("""select distinct sle.voucher_type, sle.voucher_no
from `tabStock Ledger Entry` sle
- where timestamp(sle.posting_date, sle.posting_time) >= timestamp(%s, %s) {condition}
+ where
+ timestamp(sle.posting_date, sle.posting_time) >= timestamp(%s, %s)
+ and is_cancelled = 0
+ {condition}
order by timestamp(sle.posting_date, sle.posting_time) asc, creation asc for update""".format(condition=condition),
tuple([posting_date, posting_time] + values), as_dict=True):
future_stock_vouchers.append([d.voucher_type, d.voucher_no])
@@ -964,3 +967,20 @@
gl_entries.setdefault((d.voucher_type, d.voucher_no), []).append(d)
return gl_entries
+
+def compare_existing_and_expected_gle(existing_gle, expected_gle):
+ matched = True
+ for entry in expected_gle:
+ account_existed = False
+ for e in existing_gle:
+ if entry.account == e.account:
+ account_existed = True
+ if entry.account == e.account and entry.against_account == e.against_account \
+ and (not entry.cost_center or not e.cost_center or entry.cost_center == e.cost_center) \
+ and (entry.debit != e.debit or entry.credit != e.credit):
+ matched = False
+ break
+ if not account_existed:
+ matched = False
+ break
+ return matched
\ No newline at end of file
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 10db240..c691e9f 100644
--- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json
+++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json
@@ -732,7 +732,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2020-10-30 11:59:47.670951",
+ "modified": "2020-12-07 11:59:47.670951",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order Item",
diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py
index 286c4f4..dc61870 100644
--- a/erpnext/controllers/buying_controller.py
+++ b/erpnext/controllers/buying_controller.py
@@ -16,6 +16,8 @@
from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget
from erpnext.controllers.stock_controller import StockController
+from erpnext.controllers.sales_and_purchase_return import get_rate_for_return
+from erpnext.stock.utils import get_incoming_rate
class BuyingController(StockController):
def __setup__(self):
@@ -63,7 +65,7 @@
self.set_landed_cost_voucher_amount()
if self.doctype in ("Purchase Receipt", "Purchase Invoice"):
- self.update_valuation_rate("items")
+ self.update_valuation_rate()
def set_missing_values(self, for_validate=False):
super(BuyingController, self).set_missing_values(for_validate)
@@ -177,7 +179,7 @@
self.in_words = money_in_words(amount, self.currency)
# update valuation rate
- def update_valuation_rate(self, parentfield):
+ def update_valuation_rate(self, reset_outgoing_rate=True):
"""
item_tax_amount is the total tax amount applied on that item
stored for valuation
@@ -188,7 +190,7 @@
stock_and_asset_items_qty, stock_and_asset_items_amount = 0, 0
last_item_idx = 1
- for d in self.get(parentfield):
+ for d in self.get("items"):
if d.item_code and d.item_code in stock_and_asset_items:
stock_and_asset_items_qty += flt(d.qty)
stock_and_asset_items_amount += flt(d.base_net_amount)
@@ -198,7 +200,7 @@
if d.category in ["Valuation", "Valuation and Total"]])
valuation_amount_adjustment = total_valuation_amount
- for i, item in enumerate(self.get(parentfield)):
+ for i, item in enumerate(self.get("items")):
if item.item_code and item.qty and item.item_code in stock_and_asset_items:
item_proportion = flt(item.base_net_amount) / stock_and_asset_items_amount if stock_and_asset_items_amount \
else flt(item.qty) / stock_and_asset_items_qty
@@ -216,16 +218,34 @@
item.conversion_factor = get_conversion_factor(item.item_code, item.uom).get("conversion_factor") or 1.0
qty_in_stock_uom = flt(item.qty * item.conversion_factor)
- rm_supp_cost = flt(item.rm_supp_cost) if self.doctype in ["Purchase Receipt", "Purchase Invoice"] else 0.0
-
- landed_cost_voucher_amount = flt(item.landed_cost_voucher_amount) \
- if self.doctype in ["Purchase Receipt", "Purchase Invoice"] else 0.0
-
- item.valuation_rate = ((item.base_net_amount + item.item_tax_amount + rm_supp_cost
- + landed_cost_voucher_amount) / qty_in_stock_uom)
+ item.rm_supp_cost = self.get_supplied_items_cost(item.name, reset_outgoing_rate)
+ item.valuation_rate = ((item.base_net_amount + item.item_tax_amount + item.rm_supp_cost
+ + flt(item.landed_cost_voucher_amount)) / qty_in_stock_uom)
else:
item.valuation_rate = 0.0
+ def get_supplied_items_cost(self, item_row_id, reset_outgoing_rate=True):
+ supplied_items_cost = 0.0
+ for d in self.get("supplied_items"):
+ if d.reference_name == item_row_id:
+ if reset_outgoing_rate and frappe.db.get_value('Item', d.rm_item_code, 'is_stock_item'):
+ rate = get_incoming_rate({
+ "item_code": d.rm_item_code,
+ "warehouse": self.supplier_warehouse,
+ "posting_date": self.posting_date,
+ "posting_time": self.posting_time,
+ "qty": -1 * d.consumed_qty,
+ "serial_no": d.serial_no
+ })
+
+ if rate > 0:
+ d.rate = rate
+
+ d.amount = flt(d.consumed_qty) * flt(d.rate)
+ supplied_items_cost += flt(d.amount)
+
+ return supplied_items_cost
+
def validate_for_subcontracting(self):
if not self.is_subcontracted and self.sub_contracted_items:
frappe.throw(_("Please enter 'Is Subcontracted' as Yes or No"))
@@ -352,35 +372,17 @@
else:
self.append_raw_material_to_be_backflushed(item, raw_material, qty)
- def append_raw_material_to_be_backflushed(self, fg_item_doc, raw_material_data, qty):
+ def append_raw_material_to_be_backflushed(self, fg_item_row, raw_material_data, qty):
rm = self.append('supplied_items', {})
rm.update(raw_material_data)
if not rm.main_item_code:
- rm.main_item_code = fg_item_doc.item_code
+ rm.main_item_code = fg_item_row.item_code
- rm.reference_name = fg_item_doc.name
+ rm.reference_name = fg_item_row.name
rm.required_qty = qty
rm.consumed_qty = qty
- if not raw_material_data.get('non_stock_item'):
- from erpnext.stock.utils import get_incoming_rate
- rm.rate = get_incoming_rate({
- "item_code": raw_material_data.rm_item_code,
- "warehouse": self.supplier_warehouse,
- "posting_date": self.posting_date,
- "posting_time": self.posting_time,
- "qty": -1 * qty,
- "serial_no": rm.serial_no
- })
-
- if not rm.rate:
- rm.rate = get_valuation_rate(raw_material_data.rm_item_code, self.supplier_warehouse,
- self.doctype, self.name, currency=self.company_currency, company=self.company)
-
- rm.amount = qty * flt(rm.rate)
- fg_item_doc.rm_supp_cost += rm.amount
-
def update_raw_materials_supplied_based_on_bom(self, item, raw_material_table):
exploded_item = 1
if hasattr(item, 'include_exploded_items'):
@@ -389,7 +391,7 @@
bom_items = get_items_from_bom(item.item_code, item.bom, exploded_item)
used_alternative_items = []
- if self.doctype == 'Purchase Receipt' and item.purchase_order:
+ if self.doctype in ["Purchase Receipt", "Purchase Invoice"] and item.purchase_order:
used_alternative_items = get_used_alternative_items(purchase_order = item.purchase_order)
raw_materials_cost = 0
@@ -406,7 +408,7 @@
reserve_warehouse = None
conversion_factor = item.conversion_factor
- if (self.doctype == 'Purchase Receipt' and item.purchase_order and
+ if (self.doctype in ["Purchase Receipt", "Purchase Invoice"] and item.purchase_order and
bom_item.item_code in used_alternative_items):
alternative_item_data = used_alternative_items.get(bom_item.item_code)
bom_item.item_code = alternative_item_data.item_code
@@ -434,9 +436,7 @@
rm.rm_item_code = bom_item.item_code
rm.stock_uom = bom_item.stock_uom
rm.required_qty = required_qty
- if self.doctype == "Purchase Order" and not rm.reserve_warehouse:
- rm.reserve_warehouse = reserve_warehouse
-
+ rm.rate = bom_item.rate
rm.conversion_factor = conversion_factor
if self.doctype in ["Purchase Receipt", "Purchase Invoice"]:
@@ -444,29 +444,8 @@
rm.description = bom_item.description
if item.batch_no and frappe.db.get_value("Item", rm.rm_item_code, "has_batch_no") and not rm.batch_no:
rm.batch_no = item.batch_no
-
- # get raw materials rate
- if self.doctype == "Purchase Receipt":
- from erpnext.stock.utils import get_incoming_rate
- rm.rate = get_incoming_rate({
- "item_code": bom_item.item_code,
- "warehouse": self.supplier_warehouse,
- "posting_date": self.posting_date,
- "posting_time": self.posting_time,
- "qty": -1 * required_qty,
- "serial_no": rm.serial_no
- })
- if not rm.rate:
- rm.rate = get_valuation_rate(bom_item.item_code, self.supplier_warehouse,
- self.doctype, self.name, currency=self.company_currency, company = self.company)
- else:
- rm.rate = bom_item.rate
-
- rm.amount = required_qty * flt(rm.rate)
- raw_materials_cost += flt(rm.amount)
-
- if self.doctype in ("Purchase Receipt", "Purchase Invoice"):
- item.rm_supp_cost = raw_materials_cost
+ elif not rm.reserve_warehouse:
+ rm.reserve_warehouse = reserve_warehouse
def cleanup_raw_materials_supplied(self, parent_items, raw_material_table):
"""Remove all those child items which are no longer present in main item table"""
@@ -579,7 +558,8 @@
or (cint(self.is_return) and self.docstatus==2)):
from_warehouse_sle = self.get_sl_entries(d, {
"actual_qty": -1 * pr_qty,
- "warehouse": d.from_warehouse
+ "warehouse": d.from_warehouse,
+ "dependant_sle_voucher_detail_no": d.name
})
sl_entries.append(from_warehouse_sle)
@@ -589,28 +569,20 @@
"serial_no": cstr(d.serial_no).strip()
})
if self.is_return:
- filters = {
- "voucher_type": self.doctype,
- "voucher_no": self.return_against,
- "item_code": d.item_code
- }
-
- if (self.doctype == "Purchase Invoice" and self.update_stock
- and d.get("purchase_invoice_item")):
- filters["voucher_detail_no"] = d.purchase_invoice_item
- elif self.doctype == "Purchase Receipt" and d.get("purchase_receipt_item"):
- filters["voucher_detail_no"] = d.purchase_receipt_item
-
- original_incoming_rate = frappe.db.get_value("Stock Ledger Entry", filters, "incoming_rate")
+ outgoing_rate = get_rate_for_return(self.doctype, self.name, d.item_code, self.return_against, item_row=d)
sle.update({
- "outgoing_rate": original_incoming_rate
+ "outgoing_rate": outgoing_rate,
+ "recalculate_rate": 1
})
+ if d.from_warehouse:
+ sle.dependant_sle_voucher_detail_no = d.name
else:
val_rate_db_precision = 6 if cint(self.precision("valuation_rate", d)) <= 6 else 9
incoming_rate = flt(d.valuation_rate, val_rate_db_precision)
sle.update({
- "incoming_rate": incoming_rate
+ "incoming_rate": incoming_rate,
+ "recalculate_rate": 1 if (self.is_subcontracted and d.bom) or d.from_warehouse else 0
})
sl_entries.append(sle)
@@ -618,7 +590,8 @@
or (cint(self.is_return) and self.docstatus==1)):
from_warehouse_sle = self.get_sl_entries(d, {
"actual_qty": -1 * pr_qty,
- "warehouse": d.from_warehouse
+ "warehouse": d.from_warehouse,
+ "recalculate_rate": 1
})
sl_entries.append(from_warehouse_sle)
@@ -666,6 +639,7 @@
"item_code": d.rm_item_code,
"warehouse": self.supplier_warehouse,
"actual_qty": -1*flt(d.consumed_qty),
+ "dependant_sle_voucher_detail_no": d.reference_name
}))
def on_submit(self):
@@ -857,6 +831,7 @@
else:
validate_item_type(self, "is_purchase_item", "purchase")
+
def get_items_from_bom(item_code, bom, exploded_item=1):
doctype = "BOM Item" if not exploded_item else "BOM Explosion Item"
diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py
index 5299b25..8f65c31 100644
--- a/erpnext/controllers/sales_and_purchase_return.py
+++ b/erpnext/controllers/sales_and_purchase_return.py
@@ -365,3 +365,45 @@
}, target_doc, set_missing_values)
return doclist
+
+def get_rate_for_return(voucher_type, voucher_no, item_code, return_against=None, item_row=None, voucher_detail_no=None):
+ if not return_against:
+ return_against = frappe.get_cached_value(voucher_type, voucher_no, "return_against")
+
+ return_against_item_field = get_return_against_item_fields(voucher_type)
+
+ filters = get_filters(voucher_type, voucher_no, voucher_detail_no,
+ return_against, item_code, return_against_item_field, item_row)
+
+ if voucher_type in ("Purchase Receipt", "Purchase Invoice"):
+ select_field = "incoming_rate"
+ else:
+ select_field = "abs(stock_value_difference / actual_qty)"
+
+ return flt(frappe.db.get_value("Stock Ledger Entry", filters, select_field))
+
+def get_return_against_item_fields(voucher_type):
+ return_against_item_fields = {
+ "Purchase Receipt": "purchase_receipt_item",
+ "Purchase Invoice": "purchase_invoice_item",
+ "Delivery Note": "dn_detail",
+ "Sales Invoice": "sales_invoice_item"
+ }
+ return return_against_item_fields[voucher_type]
+
+def get_filters(voucher_type, voucher_no, voucher_detail_no, return_against, item_code, return_against_item_field, item_row):
+ filters = {
+ "voucher_type": voucher_type,
+ "voucher_no": return_against,
+ "item_code": item_code
+ }
+
+ if item_row:
+ reference_voucher_detail_no = item_row.get(return_against_item_field)
+ else:
+ reference_voucher_detail_no = frappe.db.get_value(voucher_type + " Item", voucher_detail_no, return_against_item_field)
+
+ if reference_voucher_detail_no:
+ filters["voucher_detail_no"] = reference_voucher_detail_no
+
+ return filters
\ No newline at end of file
diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py
index 4dbd7bf..85cfb95 100644
--- a/erpnext/controllers/selling_controller.py
+++ b/erpnext/controllers/selling_controller.py
@@ -13,6 +13,7 @@
from erpnext.controllers.accounts_controller import get_taxes_and_charges
from erpnext.controllers.stock_controller import StockController
+from erpnext.controllers.sales_and_purchase_return import get_rate_for_return
class SellingController(StockController):
def __setup__(self):
@@ -48,6 +49,7 @@
self.set_customer_address()
self.validate_for_duplicate_items()
self.validate_target_warehouse()
+ self.set_incoming_rate()
def set_missing_values(self, for_validate=False):
@@ -230,7 +232,8 @@
'voucher_type': self.doctype,
'allow_zero_valuation': d.allow_zero_valuation_rate,
'sales_invoice_item': d.get("sales_invoice_item"),
- 'delivery_note_item': d.get("dn_detail")
+ 'dn_detail': d.get("dn_detail"),
+ 'incoming_rate': p.incoming_rate
}))
else:
il.append(frappe._dict({
@@ -248,7 +251,8 @@
'voucher_type': self.doctype,
'allow_zero_valuation': d.allow_zero_valuation_rate,
'sales_invoice_item': d.get("sales_invoice_item"),
- 'delivery_note_item': d.get("dn_detail")
+ 'dn_detail': d.get("dn_detail"),
+ 'incoming_rate': d.incoming_rate
}))
return il
@@ -307,69 +311,89 @@
sales_order.update_reserved_qty(so_item_rows)
+ def set_incoming_rate(self):
+ if self.doctype not in ("Delivery Note", "Sales Invoice"):
+ return
+
+ items = self.get("items") + (self.get("packed_items") or [])
+ for d in items:
+ if not cint(self.get("is_return")):
+ # Get incoming rate based on original item cost based on valuation method
+ d.incoming_rate = get_incoming_rate({
+ "item_code": d.item_code,
+ "warehouse": d.warehouse,
+ "posting_date": self.posting_date,
+ "posting_time": self.posting_time,
+ "qty": -1*flt(d.qty),
+ "serial_no": d.serial_no,
+ "company": self.company,
+ "voucher_type": self.doctype,
+ "voucher_no": self.name,
+ "allow_zero_valuation": d.get("allow_zero_valuation")
+ }, raise_error_if_no_rate=False)
+ elif self.get("return_against"):
+ # Get incoming rate of return entry from reference document
+ # based on original item cost as per valuation method
+ d.incoming_rate = get_rate_for_return(self.doctype, self.name, d.item_code, self.return_against, item_row=d)
+
def update_stock_ledger(self):
self.update_reserved_qty()
sl_entries = []
+ # Loop over items and packed items table
for d in self.get_item_list():
if frappe.get_cached_value("Item", d.item_code, "is_stock_item") == 1 and flt(d.qty):
if flt(d.conversion_factor)==0.0:
d.conversion_factor = get_conversion_factor(d.item_code, d.uom).get("conversion_factor") or 1.0
- return_rate = 0
- if cint(self.is_return) and self.return_against and self.docstatus==1:
- against_document_no = (d.get("sales_invoice_item")
- if self.doctype == "Sales Invoice" else d.get("delivery_note_item"))
- return_rate = self.get_incoming_rate_for_return(d.item_code,
- self.return_against, against_document_no)
-
- # On cancellation or if return entry submission, make stock ledger entry for
+ # On cancellation or return entry submission, make stock ledger entry for
# target warehouse first, to update serial no values properly
if d.warehouse and ((not cint(self.is_return) and self.docstatus==1)
or (cint(self.is_return) and self.docstatus==2)):
- sl_entries.append(self.get_sl_entries(d, {
- "actual_qty": -1*flt(d.qty),
- "incoming_rate": return_rate
- }))
+ sl_entries.append(self.get_sle_for_source_warehouse(d))
if d.target_warehouse:
- target_warehouse_sle = self.get_sl_entries(d, {
- "actual_qty": flt(d.qty),
- "warehouse": d.target_warehouse
- })
-
- if self.docstatus == 1:
- if not cint(self.is_return):
- args = frappe._dict({
- "item_code": d.item_code,
- "warehouse": d.warehouse,
- "posting_date": self.posting_date,
- "posting_time": self.posting_time,
- "qty": -1*flt(d.qty),
- "serial_no": d.serial_no,
- "company": d.company,
- "voucher_type": d.voucher_type,
- "voucher_no": d.name,
- "allow_zero_valuation": d.allow_zero_valuation
- })
- target_warehouse_sle.update({
- "incoming_rate": get_incoming_rate(args)
- })
- else:
- target_warehouse_sle.update({
- "outgoing_rate": return_rate
- })
- sl_entries.append(target_warehouse_sle)
+ sl_entries.append(self.get_sle_for_target_warehouse(d))
if d.warehouse and ((not cint(self.is_return) and self.docstatus==2)
or (cint(self.is_return) and self.docstatus==1)):
- sl_entries.append(self.get_sl_entries(d, {
- "actual_qty": -1*flt(d.qty),
- "incoming_rate": return_rate
- }))
+ sl_entries.append(self.get_sle_for_source_warehouse(d))
+
self.make_sl_entries(sl_entries)
+ def get_sle_for_source_warehouse(self, item_row):
+ sle = self.get_sl_entries(item_row, {
+ "actual_qty": -1*flt(item_row.qty),
+ "incoming_rate": item_row.incoming_rate,
+ "recalculate_rate": cint(self.is_return)
+ })
+ if item_row.target_warehouse and not cint(self.is_return):
+ sle.dependant_sle_voucher_detail_no = item_row.name
+
+ return sle
+
+ def get_sle_for_target_warehouse(self, item_row):
+ sle = self.get_sl_entries(item_row, {
+ "actual_qty": flt(item_row.qty),
+ "warehouse": item_row.target_warehouse
+ })
+
+ if self.docstatus == 1:
+ if not cint(self.is_return):
+ sle.update({
+ "incoming_rate": item_row.incoming_rate,
+ "recalculate_rate": 1
+ })
+ else:
+ sle.update({
+ "outgoing_rate": item_row.incoming_rate
+ })
+ if item_row.warehouse:
+ sle.dependant_sle_voucher_detail_no = item_row.name
+
+ return sle
+
def set_po_nos(self, for_validate=False):
if self.doctype == 'Sales Invoice' and hasattr(self, "items"):
if for_validate and self.po_no:
@@ -463,4 +487,4 @@
for d in obj.get("items"):
if d.item_code:
if getattr(d, "income_account", None):
- set_item_default(d.item_code, obj.company, 'income_account', d.income_account)
+ set_item_default(d.item_code, obj.company, 'income_account', d.income_account)
\ No newline at end of file
diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py
index 683d7f7..51c063c 100644
--- a/erpnext/controllers/stock_controller.py
+++ b/erpnext/controllers/stock_controller.py
@@ -24,7 +24,7 @@
self.validate_serialized_batch()
self.validate_customer_provided_item()
- def make_gl_entries(self, gl_entries=None):
+ def make_gl_entries(self, gl_entries=None, from_repost=False):
if self.docstatus == 2:
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
@@ -34,12 +34,12 @@
if self.docstatus==1:
if not gl_entries:
gl_entries = self.get_gl_entries(warehouse_account)
- make_gl_entries(gl_entries)
+ make_gl_entries(gl_entries, from_repost=from_repost)
elif self.doctype in ['Purchase Receipt', 'Purchase Invoice'] and self.docstatus == 1:
gl_entries = []
gl_entries = self.get_asset_gl_entry(gl_entries)
- make_gl_entries(gl_entries)
+ make_gl_entries(gl_entries, from_repost=from_repost)
def validate_serialized_batch(self):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
@@ -70,7 +70,6 @@
gl_list = []
warehouse_with_no_account = []
-
precision = frappe.get_precision("GL Entry", "debit_in_account_currency")
for item_row in voucher_details:
sle_list = sle_map.get(item_row.name)
@@ -125,7 +124,7 @@
if warehouse_with_no_account:
for wh in warehouse_with_no_account:
if frappe.db.get_value("Warehouse", wh, "company"):
- frappe.throw(_("Warehouse {0} is not linked to any account, please mention the account in the warehouse record or set default inventory account in company {1}.").format(wh, self.company))
+ frappe.throw(_("Warehouse {0} is not linked to any account, please mention the account in the warehouse record or set default inventory account in company {1}.").format(wh, self.company))
return process_gl_map(gl_list)
@@ -309,23 +308,6 @@
return serialized_items
- def get_incoming_rate_for_return(self, item_code, against_document, against_document_no=None):
- incoming_rate = 0.0
- cond = ''
- if against_document and item_code:
- if against_document_no:
- cond = " and voucher_detail_no = %s" %(frappe.db.escape(against_document_no))
-
- incoming_rate = frappe.db.sql("""select abs(stock_value_difference / actual_qty)
- from `tabStock Ledger Entry`
- where voucher_type = %s and voucher_no = %s
- and item_code = %s {0} limit 1""".format(cond),
- (self.doctype, against_document, item_code))
-
- incoming_rate = incoming_rate[0][0] if incoming_rate else 0.0
-
- return incoming_rate
-
def validate_warehouse(self):
from erpnext.stock.utils import validate_warehouse_company
@@ -409,19 +391,64 @@
if frappe.db.get_value('Item', d.item_code, 'is_customer_provided_item'):
d.allow_zero_valuation_rate = 1
-def compare_existing_and_expected_gle(existing_gle, expected_gle):
- matched = True
- for entry in expected_gle:
- account_existed = False
- for e in existing_gle:
- if entry.account == e.account:
- account_existed = True
- if entry.account == e.account and entry.against_account == e.against_account \
- and (not entry.cost_center or not e.cost_center or entry.cost_center == e.cost_center) \
- and (entry.debit != e.debit or entry.credit != e.credit):
- matched = False
- break
- if not account_existed:
- matched = False
+ def repost_future_sle_and_gle(self):
+ args = frappe._dict({
+ "posting_date": self.posting_date,
+ "posting_time": self.posting_time,
+ "voucher_type": self.doctype,
+ "voucher_no": self.name,
+ "company": self.company
+ })
+
+ if check_if_future_sle_exists(args):
+ create_repost_item_valuation_entry(args)
+
+def check_if_future_sle_exists(args):
+ sl_entries = frappe.db.get_all("Stock Ledger Entry",
+ filters={"voucher_type": args.voucher_type, "voucher_no": args.voucher_no},
+ fields=["item_code", "warehouse"],
+ order_by="creation asc")
+
+ distinct_item_warehouses = list(set([(d.item_code, d.warehouse) for d in sl_entries]))
+
+ sle_exists = False
+ for item_code, warehouse in distinct_item_warehouses:
+ args.update({
+ "item_code": item_code,
+ "warehouse": warehouse
+ })
+ if get_sle(args):
+ sle_exists = True
break
- return matched
+ return sle_exists
+
+def get_sle(args):
+ return frappe.db.sql("""
+ select name
+ from `tabStock Ledger Entry`
+ where
+ item_code=%(item_code)s
+ and warehouse=%(warehouse)s
+ and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
+ and voucher_no != %(voucher_no)s
+ and is_cancelled = 0
+ limit 1
+ """, args)
+
+def create_repost_item_valuation_entry(args):
+ args = frappe._dict(args)
+ repost_entry = frappe.new_doc("Repost Item Valuation")
+ repost_entry.based_on = args.based_on
+ if not args.based_on:
+ repost_entry.based_on = 'Transaction' if args.voucher_no else "Item and Warehouse"
+ repost_entry.voucher_type = args.voucher_type
+ repost_entry.voucher_no = args.voucher_no
+ repost_entry.item_code = args.item_code
+ repost_entry.warehouse = args.warehouse
+ repost_entry.posting_date = args.posting_date
+ repost_entry.posting_time = args.posting_time
+ repost_entry.company = args.company
+ repost_entry.allow_zero_rate = args.allow_zero_rate
+ repost_entry.flags.ignore_links = True
+ repost_entry.save()
+ repost_entry.submit()
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py
index 2bf3fbf..ce9699e 100644
--- a/erpnext/manufacturing/doctype/work_order/test_work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py
@@ -6,7 +6,6 @@
import unittest
import frappe
from frappe.utils import flt, time_diff_in_hours, now, add_months, cint, today
-from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory
from erpnext.manufacturing.doctype.work_order.work_order import (make_stock_entry,
ItemHasVariantError, stop_unstop, StockOverProductionError, OverProductionError, CapacityError)
from erpnext.stock.doctype.stock_entry import test_stock_entry
@@ -18,7 +17,6 @@
class TestWorkOrder(unittest.TestCase):
def setUp(self):
- set_perpetual_inventory(0)
self.warehouse = '_Test Warehouse 2 - _TC'
self.item = '_Test Item'
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 eff17f8..159655b 100644
--- a/erpnext/selling/doctype/sales_order_item/sales_order_item.json
+++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.json
@@ -785,7 +785,7 @@
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2020-05-29 20:54:32.309460",
+ "modified": "2020-012-07 20:54:32.309460",
"modified_by": "Administrator",
"module": "Selling",
"name": "Sales Order Item",
diff --git a/erpnext/setup/doctype/company/test_records.json b/erpnext/setup/doctype/company/test_records.json
index 2130241..9e55702 100644
--- a/erpnext/setup/doctype/company/test_records.json
+++ b/erpnext/setup/doctype/company/test_records.json
@@ -7,7 +7,8 @@
"doctype": "Company",
"domain": "Manufacturing",
"chart_of_accounts": "Standard",
- "default_holiday_list": "_Test Holiday List"
+ "default_holiday_list": "_Test Holiday List",
+ "enable_perpetual_inventory": 0
},
{
"abbr": "_TC1",
@@ -17,7 +18,8 @@
"doctype": "Company",
"domain": "Retail",
"chart_of_accounts": "Standard",
- "default_holiday_list": "_Test Holiday List"
+ "default_holiday_list": "_Test Holiday List",
+ "enable_perpetual_inventory": 0
},
{
"abbr": "_TC2",
@@ -27,7 +29,8 @@
"doctype": "Company",
"domain": "Retail",
"chart_of_accounts": "Standard",
- "default_holiday_list": "_Test Holiday List"
+ "default_holiday_list": "_Test Holiday List",
+ "enable_perpetual_inventory": 0
},
{
"abbr": "_TC3",
@@ -38,7 +41,8 @@
"doctype": "Company",
"domain": "Manufacturing",
"chart_of_accounts": "Standard",
- "default_holiday_list": "_Test Holiday List"
+ "default_holiday_list": "_Test Holiday List",
+ "enable_perpetual_inventory": 0
},
{
"abbr": "_TC4",
@@ -50,7 +54,8 @@
"doctype": "Company",
"domain": "Manufacturing",
"chart_of_accounts": "Standard",
- "default_holiday_list": "_Test Holiday List"
+ "default_holiday_list": "_Test Holiday List",
+ "enable_perpetual_inventory": 0
},
{
"abbr": "_TC5",
@@ -61,7 +66,8 @@
"doctype": "Company",
"domain": "Manufacturing",
"chart_of_accounts": "Standard",
- "default_holiday_list": "_Test Holiday List"
+ "default_holiday_list": "_Test Holiday List",
+ "enable_perpetual_inventory": 0
},
{
"abbr": "TCP1",
diff --git a/erpnext/stock/doctype/batch/test_batch.py b/erpnext/stock/doctype/batch/test_batch.py
index c2a3d3c..e41f1a8 100644
--- a/erpnext/stock/doctype/batch/test_batch.py
+++ b/erpnext/stock/doctype/batch/test_batch.py
@@ -8,13 +8,8 @@
from erpnext.stock.doctype.batch.batch import get_batch_qty, UnableToSelectBatchError, get_batch_no
from frappe.utils import cint, flt
-from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory
class TestBatch(unittest.TestCase):
-
- def setUp(self):
- set_perpetual_inventory(0)
-
def test_item_has_batch_enabled(self):
self.assertRaises(ValidationError, frappe.get_doc({
"doctype": "Batch",
diff --git a/erpnext/stock/doctype/bin/bin.py b/erpnext/stock/doctype/bin/bin.py
index 7acdec7..ab19b77 100644
--- a/erpnext/stock/doctype/bin/bin.py
+++ b/erpnext/stock/doctype/bin/bin.py
@@ -16,22 +16,30 @@
def update_stock(self, args, allow_negative_stock=False, via_landed_cost_voucher=False):
'''Called from erpnext.stock.utils.update_bin'''
self.update_qty(args)
-
if args.get("actual_qty") or args.get("voucher_type") == "Stock Reconciliation":
- from erpnext.stock.stock_ledger import update_entries_after
+ from erpnext.stock.stock_ledger import update_entries_after, update_qty_in_future_sle
if not args.get("posting_date"):
args["posting_date"] = nowdate()
+ if args.get("is_cancelled") and via_landed_cost_voucher:
+ return
+
+ # Reposts only current voucher SL Entries
+ # Updates valuation rate, stock value, stock queue for current transaction
update_entries_after({
"item_code": self.item_code,
"warehouse": self.warehouse,
"posting_date": args.get("posting_date"),
"posting_time": args.get("posting_time"),
+ "voucher_type": args.get("voucher_type"),
"voucher_no": args.get("voucher_no"),
- "sle_id": args.sle_id
+ "sle_id": args.name
}, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher)
+ # Update qty_after_transaction in future SLEs of this item and warehouse
+ update_qty_in_future_sle(args)
+
def update_qty(self, args):
# update the stock values (for current quantities)
if args.get("voucher_type")=="Stock Reconciliation":
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py
index 3f3407e..1a6a555 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.py
@@ -217,6 +217,7 @@
# because updating reserved qty in bin depends upon updated delivered qty in SO
self.update_stock_ledger()
self.make_gl_entries()
+ self.repost_future_sle_and_gle()
def on_cancel(self):
super(DeliveryNote, self).on_cancel()
@@ -234,7 +235,8 @@
self.cancel_packing_slips()
self.make_gl_entries_on_cancel()
- self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry')
+ self.repost_future_sle_and_gle()
+ self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation')
def check_credit_limit(self):
from erpnext.selling.doctype.customer.customer import check_credit_limit
diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
index 6b4663a..559f8be 100644
--- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
@@ -10,8 +10,7 @@
from frappe.utils import cint, nowdate, nowtime, cstr, add_days, flt, today
from erpnext.stock.stock_ledger import get_previous_sle
from erpnext.accounts.utils import get_balance_on
-from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt \
- import get_gl_entries, set_perpetual_inventory
+from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import get_gl_entries
from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_invoice, make_delivery_trip
from erpnext.stock.doctype.stock_entry.test_stock_entry \
import make_stock_entry, make_serialized_item, get_qty_after_transaction
@@ -24,9 +23,6 @@
from erpnext.stock.doctype.item.test_item import create_item
class TestDeliveryNote(unittest.TestCase):
- def setUp(self):
- set_perpetual_inventory(0)
-
def test_over_billing_against_dn(self):
frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1)
@@ -43,7 +39,6 @@
def test_delivery_note_no_gl_entry(self):
company = frappe.db.get_value('Warehouse', '_Test Warehouse - _TC', 'company')
- set_perpetual_inventory(0, company)
make_stock_entry(target="_Test Warehouse - _TC", qty=5, basic_rate=100)
stock_queue = json.loads(get_previous_sle({
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 7b47187..4bbf3de 100644
--- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
+++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
@@ -56,6 +56,7 @@
"base_net_rate",
"base_net_amount",
"billed_amt",
+ "incoming_rate",
"item_weight_details",
"weight_per_unit",
"total_weight",
@@ -732,16 +733,22 @@
"depends_on": "returned_qty",
"fieldname": "returned_qty",
"fieldtype": "Float",
- "label": "Returned Qty in Stock UOM",
+ "label": "Returned Qty in Stock UOM"
+ },
+ {
+ "fieldname": "incoming_rate",
+ "fieldtype": "Currency",
+ "label": "Incoming Rate",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
}
],
"idx": 1,
+ "index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2020-07-31 20:12:43.054342",
+ "modified": "2020-12-07 19:59:27.119856",
"modified_by": "Administrator",
"module": "Stock",
"name": "Delivery Note Item",
diff --git a/erpnext/stock/doctype/item/test_records.json b/erpnext/stock/doctype/item/test_records.json
index 9ca887c..8f437b1 100644
--- a/erpnext/stock/doctype/item/test_records.json
+++ b/erpnext/stock/doctype/item/test_records.json
@@ -458,5 +458,15 @@
"item_tax_template": "_Test Item Tax Template 1"
}
]
+ },
+ {
+ "description": "_Test",
+ "doctype": "Item",
+ "is_stock_item": 1,
+ "item_code": "138-CMS Shoe",
+ "item_group": "_Test Item Group",
+ "item_name": "138-CMS Shoe",
+ "stock_uom": "_Test UOM",
+ "gst_hsn_code": "999800"
}
]
diff --git a/erpnext/stock/doctype/item_alternative/test_item_alternative.py b/erpnext/stock/doctype/item_alternative/test_item_alternative.py
index f045e4f..d5700fe 100644
--- a/erpnext/stock/doctype/item_alternative/test_item_alternative.py
+++ b/erpnext/stock/doctype/item_alternative/test_item_alternative.py
@@ -12,11 +12,9 @@
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt, make_rm_stock_entry
import unittest
-from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory
class TestItemAlternative(unittest.TestCase):
def setUp(self):
- set_perpetual_inventory(0)
make_items()
def test_alternative_item_for_subcontract_rm(self):
diff --git a/erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.json b/erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.json
index 0cc243d..64331c7 100644
--- a/erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.json
+++ b/erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"creation": "2014-07-11 11:51:00.453717",
"doctype": "DocType",
"editable_grid": 1,
@@ -31,16 +32,19 @@
"reqd": 1
},
{
+ "depends_on": "eval:cint(erpnext.is_perpetual_inventory_enabled(parent.company))",
"fieldname": "expense_account",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Expense Account",
+ "mandatory_depends_on": "eval:cint(erpnext.is_perpetual_inventory_enabled(parent.company))",
"options": "Account",
- "reqd": 1
+ "print_hide": 1
}
],
"istable": 1,
- "modified": "2019-09-30 18:28:32.070655",
+ "links": [],
+ "modified": "2020-12-04 00:22:14.373312",
"modified_by": "Administrator",
"module": "Stock",
"name": "Landed Cost Taxes and Charges",
diff --git a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py
index bc3d326..9ec6b89 100644
--- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py
+++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py
@@ -77,9 +77,9 @@
company_currency = erpnext.get_company_currency(self.company)
for account in self.taxes:
if get_account_currency(account.expense_account) != company_currency:
- frappe.throw(msg=_(""" Row {0}: Expense account currency should be same as company's default currency.
- Please select expense account with account currency as {1}""")
- .format(account.idx, frappe.bold(company_currency)), title=_("Invalid Account Currency"))
+ frappe.throw(_("Row {}: Expense account currency should be same as company's default currency.").format(account.idx)
+ + _("Please select expense account with account currency as {}.").format(frappe.bold(company_currency)),
+ title=_("Invalid Account Currency"))
def set_total_taxes_and_charges(self):
self.total_taxes_and_charges = sum([flt(d.amount) for d in self.get("taxes")])
@@ -121,7 +121,7 @@
doc.set_landed_cost_voucher_amount()
# set valuation amount in pr item
- doc.update_valuation_rate("items")
+ doc.update_valuation_rate(reset_outgoing_rate=False)
# db_update will update and save landed_cost_voucher_amount and voucher_amount in PR
for item in doc.get("items"):
@@ -143,6 +143,7 @@
doc.docstatus = 1
doc.update_stock_ledger(allow_negative_stock=True, via_landed_cost_voucher=True)
doc.make_gl_entries()
+ doc.repost_future_sle_and_gle()
def validate_asset_qty_and_status(self, receipt_document_type, receipt_document):
for item in self.get('items'):
@@ -152,14 +153,13 @@
docs = frappe.db.get_all('Asset', filters={ receipt_document_type: item.receipt_document,
'item_code': item.item_code }, fields=['name', 'docstatus'])
if not docs or len(docs) != item.qty:
- frappe.throw(_('There are not enough asset created or linked to {0}. \
- Please create or link {1} Assets with respective document.').format(item.receipt_document, item.qty))
+ frappe.throw(_('There are not enough asset created or linked to {0}.').format(item.receipt_document)
+ + _('Please create or link {0} Assets with respective document.').format(item.qty))
if docs:
for d in docs:
if d.docstatus == 1:
- frappe.throw(_('{2} <b>{0}</b> has submitted Assets.\
- Remove Item <b>{1}</b> from table to continue.').format(
- item.receipt_document, item.item_code, item.receipt_document_type))
+ frappe.throw(_('{0} {1} has submitted Assets. Remove Item {2} from table to continue.')
+ .format(item.receipt_document_type, frappe.bold(item.receipt_document), frappe.bold(item.item_code)))
def update_rate_in_serial_no_for_non_asset_items(self, receipt_document):
for item in receipt_document.get("items"):
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 3f2c5da..b97213e 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
@@ -7,7 +7,7 @@
import frappe
from frappe.utils import flt
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt \
- import set_perpetual_inventory, get_gl_entries, test_records as pr_test_records, make_purchase_receipt
+ import get_gl_entries, test_records as pr_test_records, make_purchase_receipt
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.accounts.doctype.account.test_account import get_inventory_account
@@ -27,7 +27,7 @@
},
fieldname=["qty_after_transaction", "stock_value"], as_dict=1)
- submit_landed_cost_voucher("Purchase Receipt", pr.name, pr.company)
+ create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company)
pr_lc_value = frappe.db.get_value("Purchase Receipt Item", {"parent": pr.name}, "landed_cost_voucher_amount")
self.assertEqual(pr_lc_value, 25.0)
@@ -89,7 +89,7 @@
},
fieldname=["qty_after_transaction", "stock_value"], as_dict=1)
- submit_landed_cost_voucher("Purchase Invoice", pi.name, pi.company)
+ create_landed_cost_voucher("Purchase Invoice", pi.name, pi.company)
pi_lc_value = frappe.db.get_value("Purchase Invoice Item", {"parent": pi.name},
"landed_cost_voucher_amount")
@@ -137,7 +137,7 @@
serial_no_rate = frappe.db.get_value("Serial No", "SN001", "purchase_rate")
- submit_landed_cost_voucher("Purchase Receipt", pr.name, pr.company)
+ create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company)
serial_no = frappe.db.get_value("Serial No", "SN001",
["warehouse", "purchase_rate"], as_dict=1)
@@ -160,7 +160,7 @@
})
pr.submit()
- lcv = submit_landed_cost_voucher("Purchase Receipt", pr.name, pr.company, 123.22)
+ lcv = create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company, 123.22)
self.assertEqual(lcv.items[0].applicable_charges, 41.07)
self.assertEqual(lcv.items[2].applicable_charges, 41.08)
@@ -236,7 +236,7 @@
return lcv
-def submit_landed_cost_voucher(receipt_document_type, receipt_document, company, charges=50):
+def create_landed_cost_voucher(receipt_document_type, receipt_document, company, charges=50):
ref_doc = frappe.get_doc(receipt_document_type, receipt_document)
lcv = frappe.new_doc("Landed Cost Voucher")
diff --git a/erpnext/stock/doctype/material_request/test_material_request.py b/erpnext/stock/doctype/material_request/test_material_request.py
index 19924b1..0a29fa0 100644
--- a/erpnext/stock/doctype/material_request/test_material_request.py
+++ b/erpnext/stock/doctype/material_request/test_material_request.py
@@ -12,9 +12,6 @@
from erpnext.stock.doctype.item.test_item import create_item
class TestMaterialRequest(unittest.TestCase):
- def setUp(self):
- erpnext.set_perpetual_inventory(0)
-
def test_make_purchase_order(self):
mr = frappe.copy_doc(test_records[0]).insert()
diff --git a/erpnext/stock/doctype/packed_item/packed_item.json b/erpnext/stock/doctype/packed_item/packed_item.json
index 2ac5c42..f1d7f8c 100644
--- a/erpnext/stock/doctype/packed_item/packed_item.json
+++ b/erpnext/stock/doctype/packed_item/packed_item.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"creation": "2013-02-22 01:28:00",
"doctype": "DocType",
"editable_grid": 1,
@@ -14,6 +15,7 @@
"target_warehouse",
"column_break_9",
"qty",
+ "uom",
"section_break_9",
"serial_no",
"column_break_11",
@@ -23,7 +25,7 @@
"actual_qty",
"projected_qty",
"column_break_16",
- "uom",
+ "incoming_rate",
"page_break",
"prevdoc_doctype",
"parent_detail_docname"
@@ -199,11 +201,21 @@
"no_copy": 1,
"print_hide": 1,
"read_only": 1
+ },
+ {
+ "fieldname": "incoming_rate",
+ "fieldtype": "Currency",
+ "label": "Incoming Rate",
+ "no_copy": 1,
+ "print_hide": 1,
+ "read_only": 1
}
],
"idx": 1,
+ "index_web_pages_for_search": 1,
"istable": 1,
- "modified": "2019-11-26 20:09:59.400960",
+ "links": [],
+ "modified": "2020-09-24 09:25:13.050151",
"modified_by": "Administrator",
"module": "Stock",
"name": "Packed Item",
@@ -212,4 +224,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
-}
+}
\ No newline at end of file
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
index 97e0fa7..226064b 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
@@ -181,6 +181,7 @@
update_serial_nos_after_submit(self, "items")
self.make_gl_entries()
+ self.repost_future_sle_and_gle()
def check_next_docstatus(self):
submit_rv = frappe.db.sql("""select t1.name
@@ -209,7 +210,8 @@
# because updating ordered qty in bin depends upon updated ordered qty in PO
self.update_stock_ledger()
self.make_gl_entries_on_cancel()
- self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry')
+ self.repost_future_sle_and_gle()
+ self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation')
self.delete_auto_created_batches()
def get_current_stock(self):
diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
index 9b8eeed..83012d3 100644
--- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
@@ -9,14 +9,15 @@
from frappe.utils import cint, flt, cstr, today, random_string, add_days
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_invoice
from erpnext.stock.doctype.item.test_item import create_item
-from erpnext import set_perpetual_inventory
from erpnext.stock.doctype.serial_no.serial_no import SerialNoDuplicateError
from erpnext.accounts.doctype.account.test_account import get_inventory_account
from erpnext.stock.doctype.item.test_item import make_item
from six import iteritems
+from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
+
+
class TestPurchaseReceipt(unittest.TestCase):
def setUp(self):
- set_perpetual_inventory(0)
frappe.db.set_value("Buying Settings", None, "allow_multiple_items", 1)
def test_reverse_purchase_receipt_sle(self):
@@ -112,6 +113,8 @@
self.assertFalse(get_gl_entries("Purchase Receipt", pr.name))
+ pr.cancel()
+
def test_batched_serial_no_purchase(self):
item = frappe.db.exists("Item", {'item_name': 'Batched Serialized Item'})
if not item:
@@ -183,22 +186,30 @@
rm_supp_cost = sum([d.amount for d in pr.get("supplied_items")])
self.assertEqual(pr.get("items")[0].rm_supp_cost, flt(rm_supp_cost, 2))
+
+ pr.cancel()
def test_subcontracting_gle_fg_item_rate_zero(self):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
- set_perpetual_inventory()
frappe.db.set_value("Buying Settings", None, "backflush_raw_materials_of_subcontract_based_on", "BOM")
- make_stock_entry(item_code="_Test Item", target="Work In Progress - TCP1", qty=100, basic_rate=100, company="_Test Company with perpetual inventory")
- make_stock_entry(item_code="_Test Item Home Desktop 100", target="Work In Progress - TCP1",
+
+ se1 = make_stock_entry(item_code="_Test Item", target="Work In Progress - TCP1",
qty=100, basic_rate=100, company="_Test Company with perpetual inventory")
+
+ se2 = make_stock_entry(item_code="_Test Item Home Desktop 100", target="Work In Progress - TCP1",
+ qty=100, basic_rate=100, company="_Test Company with perpetual inventory")
+
pr = make_purchase_receipt(item_code="_Test FG Item", qty=10, rate=0, is_subcontracted="Yes",
- company="_Test Company with perpetual inventory", warehouse='Stores - TCP1', supplier_warehouse='Work In Progress - TCP1')
+ company="_Test Company with perpetual inventory", warehouse='Stores - TCP1',
+ supplier_warehouse='Work In Progress - TCP1')
gl_entries = get_gl_entries("Purchase Receipt", pr.name)
self.assertFalse(gl_entries)
- set_perpetual_inventory(0)
+ pr.cancel()
+ se1.cancel()
+ se2.cancel()
def test_subcontracting_over_receipt(self):
"""
@@ -216,13 +227,13 @@
item_code = "_Test Subcontracted FG Item 1"
make_subcontracted_item(item_code=item_code)
- po = create_purchase_order(item_code=item_code, qty=1,
+ po = create_purchase_order(item_code=item_code, qty=1, include_exploded_items=0,
is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC")
#stock raw materials in a warehouse before transfer
- make_stock_entry(target="_Test Warehouse - _TC",
- item_code = "Test Extra Item 1", qty=1, basic_rate=100)
- make_stock_entry(target="_Test Warehouse - _TC",
+ se1 = make_stock_entry(target="_Test Warehouse - _TC",
+ item_code = "Test Extra Item 1", qty=10, basic_rate=100)
+ se2 = make_stock_entry(target="_Test Warehouse - _TC",
item_code = "_Test FG Item", qty=1, basic_rate=100)
rm_items = [
{
@@ -254,6 +265,13 @@
pr1.submit()
self.assertRaises(frappe.ValidationError, pr2.submit)
+ pr1.cancel()
+ se.cancel()
+ se1.cancel()
+ se2.cancel()
+ po.reload()
+ po.cancel()
+
def test_serial_no_supplier(self):
pr = make_purchase_receipt(item_code="_Test Serialized Item With Series", qty=1)
self.assertEqual(frappe.db.get_value("Serial No", pr.get("items")[0].serial_no, "supplier"),
@@ -284,6 +302,8 @@
self.assertEqual(frappe.db.get_value("Serial No", serial_no, "warehouse"),
pr.get("items")[0].rejected_warehouse)
+ pr.cancel()
+
def test_purchase_return_partial(self):
pr = make_purchase_receipt(company="_Test Company with perpetual inventory",
warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1")
@@ -371,6 +391,9 @@
self.assertEqual(pr.per_returned, 100)
self.assertEqual(pr.status, 'Return Issued')
+ return_pr.cancel()
+ pr.cancel()
+
def test_purchase_return_for_rejected_qty(self):
from erpnext.stock.doctype.warehouse.test_warehouse import get_warehouse
@@ -388,6 +411,9 @@
self.assertEqual(actual_qty, -2)
+ return_pr.cancel()
+ pr.cancel()
+
def test_purchase_return_for_serialized_items(self):
def _check_serial_no_values(serial_no, field_values):
@@ -415,6 +441,10 @@
"delivery_document_no": return_pr.name
})
+ return_pr.cancel()
+ pr.reload()
+ pr.cancel()
+
def test_purchase_return_for_multi_uom(self):
item_code = "_Test Purchase Return For Multi-UOM"
if not frappe.db.exists('Item', item_code):
@@ -431,6 +461,9 @@
self.assertEqual(abs(return_pr.items[0].stock_qty), 1.0)
+ return_pr.cancel()
+ pr.cancel()
+
def test_closed_purchase_receipt(self):
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import update_purchase_receipt_status
@@ -440,6 +473,9 @@
update_purchase_receipt_status(pr.name, "Closed")
self.assertEqual(frappe.db.get_value("Purchase Receipt", pr.name, "status"), "Closed")
+ pr.reload()
+ pr.cancel()
+
def test_pr_billing_status(self):
# PO -> PR1 -> PI and PO -> PI and PO -> PR2
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
@@ -482,6 +518,16 @@
self.assertEqual(pr2.per_billed, 80)
self.assertEqual(pr2.status, "To Bill")
+ pr2.cancel()
+ pi2.reload()
+ pi2.cancel()
+ pi1.reload()
+ pi1.cancel()
+ pr1.reload()
+ pr1.cancel()
+ po.reload()
+ po.cancel()
+
def test_serial_no_against_purchase_receipt(self):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
@@ -509,6 +555,8 @@
self.assertEqual(serial_no, frappe.db.get_value("Serial No",
{"purchase_document_type": "Purchase Receipt", "purchase_document_no": new_pr_doc.name}, "name"))
+ new_pr_doc.cancel()
+
def test_not_accept_duplicate_serial_no(self):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
@@ -519,16 +567,19 @@
item_code = item.name
serial_no = random_string(5)
- make_purchase_receipt(item_code=item_code, qty=1, serial_no=serial_no)
- create_delivery_note(item_code=item_code, qty=1, serial_no=serial_no)
+ pr1 = make_purchase_receipt(item_code=item_code, qty=1, serial_no=serial_no)
+ dn = create_delivery_note(item_code=item_code, qty=1, serial_no=serial_no)
- pr = make_purchase_receipt(item_code=item_code, qty=1, serial_no=serial_no, do_not_submit=True)
- self.assertRaises(SerialNoDuplicateError, pr.submit)
+ pr2 = make_purchase_receipt(item_code=item_code, qty=1, serial_no=serial_no, do_not_submit=True)
+ self.assertRaises(SerialNoDuplicateError, pr2.submit)
se = make_stock_entry(item_code=item_code, target="_Test Warehouse - _TC", qty=1,
serial_no=serial_no, basic_rate=100, do_not_submit=True)
self.assertRaises(SerialNoDuplicateError, se.submit)
+ dn.cancel()
+ pr1.cancel()
+
def test_auto_asset_creation(self):
asset_item = "Test Asset Item"
@@ -549,7 +600,7 @@
'company_name': '_Test Company',
'fixed_asset_account': '_Test Fixed Asset - _TC',
'accumulated_depreciation_account': '_Test Accumulated Depreciations - _TC',
- 'depreciation_expense_account': '_Test Depreciation - _TC'
+ 'depreciation_expense_account': '_Test Depreciations - _TC'
}]
}).insert()
@@ -568,6 +619,8 @@
location = frappe.db.get_value('Asset', assets[0].name, 'location')
self.assertEquals(location, "Test Location")
+ pr.cancel()
+
def test_purchase_return_with_submitted_asset(self):
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_return
@@ -594,6 +647,9 @@
pr_return.submit()
+ pr_return.cancel()
+ pr.cancel()
+
def test_purchase_receipt_cost_center(self):
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
cost_center = "_Test Cost Center for BS Account - TCP1"
@@ -605,7 +661,8 @@
'location_name': 'Test Location'
}).insert()
- pr = make_purchase_receipt(cost_center=cost_center, company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1")
+ pr = make_purchase_receipt(cost_center=cost_center, company="_Test Company with perpetual inventory",
+ warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1")
stock_in_hand_account = get_inventory_account(pr.company, pr.get("items")[0].warehouse)
gl_entries = get_gl_entries("Purchase Receipt", pr.name)
@@ -623,6 +680,8 @@
for i, gle in enumerate(gl_entries):
self.assertEqual(expected_values[gle.account]["cost_center"], gle.cost_center)
+ pr.cancel()
+
def test_purchase_receipt_cost_center_with_balance_sheet_account(self):
if not frappe.db.exists('Location', 'Test Location'):
frappe.get_doc({
@@ -648,6 +707,8 @@
for i, gle in enumerate(gl_entries):
self.assertEqual(expected_values[gle.account]["cost_center"], gle.cost_center)
+ pr.cancel()
+
def test_make_purchase_invoice_from_pr_for_returned_qty(self):
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order, create_pr_against_po
@@ -663,6 +724,12 @@
pi = make_purchase_invoice(pr.name)
self.assertEquals(pi.items[0].qty, 3)
+ pr1.cancel()
+ pr.reload()
+ pr.cancel()
+ po.reload()
+ po.cancel()
+
def test_make_purchase_invoice_from_pr_with_returned_qty_duplicate_items(self):
pr1 = make_purchase_receipt(qty=8, do_not_submit=True)
pr1.append("items", {
@@ -689,8 +756,14 @@
self.assertEquals(pi2.items[0].qty, 2)
self.assertEquals(pi2.items[1].qty, 1)
+ pr2.cancel()
+ pi1.cancel()
+ pr1.reload()
+ pr1.cancel()
+
def test_stock_transfer_from_purchase_receipt(self):
- pr1 = make_purchase_receipt(warehouse = 'Work In Progress - TCP1', company="_Test Company with perpetual inventory")
+ pr1 = make_purchase_receipt(warehouse = 'Work In Progress - TCP1',
+ company="_Test Company with perpetual inventory")
pr = make_purchase_receipt(company="_Test Company with perpetual inventory",
warehouse = "Stores - TCP1", do_not_save=1)
@@ -713,18 +786,20 @@
for sle in sl_entries:
self.assertEqual(expected_sle[sle.warehouse], sle.actual_qty)
- def test_stock_transfer_from_purchase_receipt_with_valuation(self):
- warehouse = frappe.get_doc('Warehouse', 'Work In Progress - TCP1')
- warehouse.account = '_Test Account Stock In Hand - TCP1'
- warehouse.save()
+ pr.cancel()
+ pr1.cancel()
- pr1 = make_purchase_receipt(warehouse = 'Work In Progress - TCP1',
+ def test_stock_transfer_from_purchase_receipt_with_valuation(self):
+ create_warehouse("_Test Warehouse for Valuation", company="_Test Company with perpetual inventory",
+ properties={"account": '_Test Account Stock In Hand - TCP1'})
+
+ pr1 = make_purchase_receipt(warehouse = '_Test Warehouse for Valuation - TCP1',
company="_Test Company with perpetual inventory")
pr = make_purchase_receipt(company="_Test Company with perpetual inventory",
warehouse = "Stores - TCP1", do_not_save=1)
- pr.items[0].from_warehouse = 'Work In Progress - TCP1'
+ pr.items[0].from_warehouse = '_Test Warehouse for Valuation - TCP1'
pr.supplier_warehouse = ''
@@ -749,7 +824,7 @@
]
expected_sle = {
- 'Work In Progress - TCP1': -5,
+ '_Test Warehouse for Valuation - TCP1': -5,
'Stores - TCP1': 5
}
@@ -761,60 +836,9 @@
self.assertEqual(gle.debit, expected_gle[i][1])
self.assertEqual(gle.credit, expected_gle[i][2])
- warehouse.account = ''
- warehouse.save()
+ pr.cancel()
+ pr1.cancel()
- def test_backdated_purchase_receipt(self):
- # make purchase receipt for default company
- make_purchase_receipt(company="_Test Company 4", warehouse="Stores - _TC4")
-
- # try to make another backdated PR
- posting_date = add_days(today(), -1)
- pr = make_purchase_receipt(company="_Test Company 4", warehouse="Stores - _TC4",
- do_not_submit=True)
-
- pr.set_posting_time = 1
- pr.posting_date = posting_date
- pr.save()
-
- self.assertRaises(frappe.ValidationError, pr.submit)
-
- # make purchase receipt for other company backdated
- pr = make_purchase_receipt(company="_Test Company 5", warehouse="Stores - _TC5",
- do_not_submit=True)
-
- pr.set_posting_time = 1
- pr.posting_date = posting_date
- pr.submit()
-
- # Allowed to submit for other company's PR
- self.assertEqual(pr.docstatus, 1)
-
- def test_backdated_purchase_receipt_for_same_company_different_warehouse(self):
- # make purchase receipt for default company
- make_purchase_receipt(company="_Test Company 4", warehouse="Stores - _TC4")
-
- # try to make another backdated PR
- posting_date = add_days(today(), -1)
- pr = make_purchase_receipt(company="_Test Company 4", warehouse="Stores - _TC4",
- do_not_submit=True)
-
- pr.set_posting_time = 1
- pr.posting_date = posting_date
- pr.save()
-
- self.assertRaises(frappe.ValidationError, pr.submit)
-
- # make purchase receipt for other company backdated
- pr = make_purchase_receipt(company="_Test Company 4", warehouse="Finished Goods - _TC4",
- do_not_submit=True)
-
- pr.set_posting_time = 1
- pr.posting_date = posting_date
- pr.submit()
-
- # Allowed to submit for other company's PR
- self.assertEqual(pr.docstatus, 1)
def test_subcontracted_pr_for_multi_transfer_batches(self):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
@@ -877,6 +901,12 @@
update_backflush_based_on("BOM")
+ pr.delete()
+ se.cancel()
+ ste2.cancel()
+ ste1.cancel()
+ po.cancel()
+
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
@@ -972,6 +1002,8 @@
pr.posting_date = args.posting_date or today()
if args.posting_time:
pr.posting_time = args.posting_time
+ if args.posting_date or args.posting_time:
+ pr.set_posting_time = 1
pr.company = args.company or "_Test Company"
pr.supplier = args.supplier or "_Test Supplier"
pr.is_subcontracted = args.is_subcontracted or "No"
diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
index 84c64aa..871b255 100644
--- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
+++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
@@ -866,7 +866,7 @@
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2020-11-02 10:00:38.204294",
+ "modified": "2020-12-07 10:00:38.204294",
"modified_by": "Administrator",
"module": "Stock",
"name": "Purchase Receipt Item",
diff --git a/erpnext/stock/doctype/repost_item_valuation/__init__.py b/erpnext/stock/doctype/repost_item_valuation/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/stock/doctype/repost_item_valuation/__init__.py
diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js
new file mode 100644
index 0000000..e429cd5
--- /dev/null
+++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js
@@ -0,0 +1,52 @@
+// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Repost Item Valuation', {
+ setup: function(frm) {
+ frm.set_query("warehouse", () => {
+ let filters = {
+ 'is_group': 0
+ };
+ if (frm.doc.company) filters['company'] = frm.doc.company;
+ return {filters: filters};
+ });
+
+ frm.set_query("voucher_type", () => {
+ return {
+ filters: {
+ name: ['in', ['Purchase Receipt', 'Purchase Invoice', 'Delivery Note',
+ 'Sales Invoice', 'Stock Entry', 'Stock Reconciliation']]
+ }
+ };
+ });
+
+ if (frm.doc.company) {
+ frm.set_query("voucher_no", () => {
+ return {
+ filters: {
+ company: frm.doc.company
+ }
+ };
+ });
+ }
+ },
+ refresh: function(frm) {
+ if (frm.doc.status == "Failed") {
+ frm.add_custom_button(__('Restart'), function () {
+ frm.trigger("restart_reposting");
+ }).addClass("btn-primary");
+ }
+ },
+
+ restart_reposting: function(frm) {
+ frappe.call({
+ method: "restart_reposting",
+ doc: frm.doc,
+ callback: function(r) {
+ if (!r.exc) {
+ frm.refresh();
+ }
+ }
+ });
+ }
+});
diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json
new file mode 100644
index 0000000..071fc86
--- /dev/null
+++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json
@@ -0,0 +1,215 @@
+{
+ "actions": [],
+ "autoname": "REPOST-ITEM-VAL-.######",
+ "creation": "2020-10-22 22:27:07.742161",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "based_on",
+ "voucher_type",
+ "voucher_no",
+ "item_code",
+ "warehouse",
+ "posting_date",
+ "posting_time",
+ "column_break_5",
+ "status",
+ "company",
+ "allow_negative_stock",
+ "via_landed_cost_voucher",
+ "allow_zero_rate",
+ "amended_from",
+ "error_section",
+ "error_log"
+ ],
+ "fields": [
+ {
+ "depends_on": "eval:doc.based_on=='Item and Warehouse'",
+ "fieldname": "item_code",
+ "fieldtype": "Link",
+ "label": "Item Code",
+ "mandatory_depends_on": "eval:doc.based_on=='Item and Warehouse'",
+ "options": "Item"
+ },
+ {
+ "depends_on": "eval:doc.based_on=='Item and Warehouse'",
+ "fieldname": "warehouse",
+ "fieldtype": "Link",
+ "label": "Warehouse",
+ "mandatory_depends_on": "eval:doc.based_on=='Item and Warehouse'",
+ "options": "Warehouse"
+ },
+ {
+ "fetch_from": "voucher_no.posting_date",
+ "fieldname": "posting_date",
+ "fieldtype": "Date",
+ "label": "Posting Date",
+ "reqd": 1
+ },
+ {
+ "fetch_from": "voucher_no.posting_time",
+ "fieldname": "posting_time",
+ "fieldtype": "Time",
+ "label": "Posting Time"
+ },
+ {
+ "default": "Queued",
+ "fieldname": "status",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Status",
+ "no_copy": 1,
+ "options": "Queued\nIn Progress\nCompleted\nFailed",
+ "read_only": 1
+ },
+ {
+ "fieldname": "amended_from",
+ "fieldtype": "Link",
+ "label": "Amended From",
+ "no_copy": 1,
+ "options": "Repost Item Valuation",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_5",
+ "fieldtype": "Column Break"
+ },
+ {
+ "depends_on": "eval:doc.status=='Failed'",
+ "fieldname": "error_section",
+ "fieldtype": "Section Break",
+ "label": "Error"
+ },
+ {
+ "fieldname": "error_log",
+ "fieldtype": "Long Text",
+ "label": "Error Log",
+ "no_copy": 1,
+ "read_only": 1
+ },
+ {
+ "fetch_from": "warehouse.company",
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "label": "Company",
+ "options": "Company"
+ },
+ {
+ "depends_on": "eval:doc.based_on=='Transaction'",
+ "fieldname": "voucher_type",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Voucher Type",
+ "mandatory_depends_on": "eval:doc.based_on=='Transaction'",
+ "options": "DocType"
+ },
+ {
+ "depends_on": "eval:doc.based_on=='Transaction'",
+ "fieldname": "voucher_no",
+ "fieldtype": "Dynamic Link",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Voucher No",
+ "mandatory_depends_on": "eval:doc.based_on=='Transaction'",
+ "options": "voucher_type"
+ },
+ {
+ "default": "Transaction",
+ "fieldname": "based_on",
+ "fieldtype": "Select",
+ "label": "Based On",
+ "options": "Transaction\nItem and Warehouse",
+ "reqd": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "allow_negative_stock",
+ "fieldtype": "Check",
+ "label": "Allow Negative Stock"
+ },
+ {
+ "default": "0",
+ "fieldname": "via_landed_cost_voucher",
+ "fieldtype": "Check",
+ "label": "Via Landed Cost Voucher"
+ },
+ {
+ "default": "0",
+ "fieldname": "allow_zero_rate",
+ "fieldtype": "Check",
+ "label": "Allow Zero Rate"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "is_submittable": 1,
+ "links": [],
+ "modified": "2020-12-10 07:52:12.476589",
+ "modified_by": "Administrator",
+ "module": "Stock",
+ "name": "Repost Item Valuation",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ },
+ {
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Stock User",
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ },
+ {
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Stock Manager",
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ },
+ {
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Accounts User",
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC"
+}
\ No newline at end of file
diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
new file mode 100644
index 0000000..a942f2e
--- /dev/null
+++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
@@ -0,0 +1,89 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe, erpnext
+from frappe.model.document import Document
+from frappe.utils import cint
+from erpnext.stock.stock_ledger import repost_future_sle
+from erpnext.accounts.utils import update_gl_entries_after
+
+
+class RepostItemValuation(Document):
+ def validate(self):
+ self.set_status()
+ self.reset_field_values()
+ self.set_company()
+
+ def reset_field_values(self):
+ if self.based_on == 'Transaction':
+ self.item_code = None
+ self.warehouse = None
+ else:
+ self.voucher_type = None
+ self.voucher_no = None
+
+ def set_company(self):
+ if self.voucher_type and self.voucher_no:
+ self.company = frappe.get_cached_value(self.voucher_type, self.voucher_no, "company")
+ elif self.warehouse:
+ self.company = frappe.get_cached_value("Warehouse", self.warehouse, "company")
+
+ def set_status(self, status=None):
+ if not status:
+ status = 'Queued'
+ self.db_set('status', status)
+
+ def on_submit(self):
+ frappe.enqueue(repost, timeout=1800, queue='long',
+ job_name='repost_sle', now=frappe.flags.in_test, doc=self)
+
+ def restart_reposting(self):
+ self.set_status('Queued')
+ frappe.enqueue(repost, timeout=1800, queue='long',
+ job_name='repost_sle', now=True, doc=self)
+
+def repost(doc):
+ try:
+ doc.set_status('In Progress')
+ frappe.db.commit()
+
+ repost_sl_entries(doc)
+ repost_gl_entries(doc)
+ doc.set_status('Completed')
+ except Exception:
+ frappe.db.rollback()
+ traceback = frappe.get_traceback()
+ frappe.log_error(traceback)
+ frappe.db.set_value(doc.doctype, doc.name, 'error_log', traceback)
+ doc.set_status('Failed')
+ raise
+ finally:
+ frappe.db.commit()
+
+def repost_sl_entries(doc):
+ if doc.based_on == 'Transaction':
+ repost_future_sle(voucher_type=doc.voucher_type, voucher_no=doc.voucher_no,
+ allow_negative_stock=doc.allow_negative_stock, via_landed_cost_voucher=doc.via_landed_cost_voucher)
+ else:
+ repost_future_sle(args=[frappe._dict({
+ "item_code": doc.item_code,
+ "warehouse": doc.warehouse,
+ "posting_date": doc.posting_date,
+ "posting_time": doc.posting_time
+ })], allow_negative_stock=doc.allow_negative_stock, via_landed_cost_voucher=doc.via_landed_cost_voucher)
+
+def repost_gl_entries(doc):
+ if not cint(erpnext.is_perpetual_inventory_enabled(doc.company)):
+ return
+
+ if doc.based_on == 'Transaction':
+ ref_doc = frappe.get_doc(doc.voucher_type, doc.voucher_no)
+ items, warehouses = ref_doc.get_items_and_warehouses()
+ else:
+ items = [doc.item_code]
+ warehouses = [doc.warehouse]
+
+ update_gl_entries_after(doc.posting_date, doc.posting_time,
+ warehouses, items, company=doc.company)
\ No newline at end of file
diff --git a/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py
new file mode 100644
index 0000000..13ceb68
--- /dev/null
+++ b/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+# import frappe
+import unittest
+
+class TestRepostItemValuation(unittest.TestCase):
+ pass
diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py
index 295149e..39ccf49 100644
--- a/erpnext/stock/doctype/serial_no/serial_no.py
+++ b/erpnext/stock/doctype/serial_no/serial_no.py
@@ -134,17 +134,13 @@
sle_dict = self.get_stock_ledger_entries(serial_no)
if sle_dict:
if sle_dict.get("incoming", []):
- sle_list = [sle for sle in sle_dict["incoming"] if sle.is_cancelled == 0]
- if sle_list:
- entries["purchase_sle"] = sle_list[0]
+ entries["purchase_sle"] = sle_dict["incoming"][0]
if len(sle_dict.get("incoming", [])) - len(sle_dict.get("outgoing", [])) > 0:
entries["last_sle"] = sle_dict["incoming"][0]
else:
entries["last_sle"] = sle_dict["outgoing"][0]
- sle_list = [sle for sle in sle_dict["outgoing"] if sle.is_cancelled == 0]
- if sle_list:
- entries["delivery_sle"] = sle_list[0]
+ entries["delivery_sle"] = sle_dict["outgoing"][0]
return entries
@@ -155,11 +151,12 @@
for sle in frappe.db.sql("""
SELECT voucher_type, voucher_no,
- posting_date, posting_time, incoming_rate, actual_qty, serial_no, is_cancelled
+ posting_date, posting_time, incoming_rate, actual_qty, serial_no
FROM
`tabStock Ledger Entry`
WHERE
item_code=%s AND company = %s
+ AND is_cancelled = 0
AND (serial_no = %s
OR serial_no like %s
OR serial_no like %s
@@ -179,7 +176,7 @@
def on_trash(self):
sl_entries = frappe.db.sql("""select serial_no from `tabStock Ledger Entry`
- where serial_no like %s and item_code=%s""",
+ where serial_no like %s and item_code=%s and is_cancelled=0""",
("%%%s%%" % self.name, self.item_code), as_dict=True)
# Find the exact match
@@ -229,7 +226,7 @@
if serial_nos:
frappe.throw(_("Item {0} is not setup for Serial Nos. Column must be blank").format(sle.item_code),
SerialNoNotRequiredError)
- else:
+ elif not sle.is_cancelled:
if serial_nos:
if cint(sle.actual_qty) != flt(sle.actual_qty):
frappe.throw(_("Serial No {0} quantity {1} cannot be a fraction").format(sle.item_code, sle.actual_qty))
@@ -247,10 +244,6 @@
"delivery_document_no", "delivery_document_type", "warehouse",
"purchase_document_no", "company"], as_dict=1)
- if sr and cint(sle.actual_qty) < 0 and sr.warehouse != sle.warehouse:
- frappe.throw(_("Cannot cancel {0} {1} because Serial No {2} does not belong to the warehouse {3}")
- .format(sle.voucher_type, sle.voucher_no, serial_no, sle.warehouse), SerialNoWarehouseError)
-
if sr.item_code!=sle.item_code:
if not allow_serial_nos_with_different_item(serial_no, sle):
frappe.throw(_("Serial No {0} does not belong to Item {1}").format(serial_no,
@@ -277,7 +270,7 @@
frappe.throw(_("Serial No {0} does not belong to Batch {1}").format(serial_no,
sle.batch_no), SerialNoBatchError)
- if not sr.warehouse:
+ if not sle.is_cancelled and not sr.warehouse:
frappe.throw(_("Serial No {0} does not belong to any Warehouse")
.format(serial_no), SerialNoWarehouseError)
@@ -327,6 +320,12 @@
elif cint(sle.actual_qty) < 0 or not item_det.serial_no_series:
frappe.throw(_("Serial Nos Required for Serialized Item {0}").format(sle.item_code),
SerialNoRequiredError)
+ elif serial_nos:
+ for serial_no in serial_nos:
+ sr = frappe.db.get_value("Serial No", serial_no, ["name", "warehouse"], as_dict=1)
+ if sr and cint(sle.actual_qty) < 0 and sr.warehouse != sle.warehouse:
+ frappe.throw(_("Cannot cancel {0} {1} because Serial No {2} does not belong to the warehouse {3}")
+ .format(sle.voucher_type, sle.voucher_no, serial_no, sle.warehouse))
def validate_material_transfer_entry(sle_doc):
sle_doc.update({
@@ -334,7 +333,7 @@
"skip_serial_no_validaiton": False
})
- if (sle_doc.voucher_type == "Stock Entry" and
+ if (sle_doc.voucher_type == "Stock Entry" and not sle_doc.is_cancelled and
frappe.get_cached_value("Stock Entry", sle_doc.voucher_no, "purpose") == "Material Transfer"):
if sle_doc.actual_qty < 0:
sle_doc.skip_update_serial_no = True
@@ -379,7 +378,7 @@
stock_entry = frappe.get_cached_doc("Stock Entry", sle.voucher_no)
if stock_entry.purpose in ("Repack", "Manufacture"):
for d in stock_entry.get("items"):
- if d.serial_no and (d.s_warehouse or d.t_warehouse):
+ if d.serial_no and (d.s_warehouse if not sle.is_cancelled else d.t_warehouse):
serial_nos = get_serial_nos(d.serial_no)
if sle_serial_no in serial_nos:
allow_serial_nos = True
@@ -388,7 +387,7 @@
def update_serial_nos(sle, item_det):
if sle.skip_update_serial_no: return
- if not sle.serial_no and cint(sle.actual_qty) > 0 \
+ if not sle.is_cancelled and not sle.serial_no and cint(sle.actual_qty) > 0 \
and item_det.has_serial_no == 1 and item_det.serial_no_series:
serial_nos = get_auto_serial_nos(item_det.serial_no_series, sle.actual_qty)
frappe.db.set(sle, "serial_no", serial_nos)
diff --git a/erpnext/stock/doctype/serial_no/test_serial_no.py b/erpnext/stock/doctype/serial_no/test_serial_no.py
index ab06107..ed70790 100644
--- a/erpnext/stock/doctype/serial_no/test_serial_no.py
+++ b/erpnext/stock/doctype/serial_no/test_serial_no.py
@@ -12,7 +12,6 @@
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
-from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory
test_dependencies = ["Item"]
test_records = frappe.get_test_records('Serial No')
@@ -38,8 +37,6 @@
self.assertTrue(SerialNoCannotCannotChangeError, sr.save)
def test_inter_company_transfer(self):
- set_perpetual_inventory(0, "_Test Company 1")
- set_perpetual_inventory(0)
se = make_serialized_item(target_warehouse="_Test Warehouse - _TC")
serial_nos = get_serial_nos(se.get("items")[0].serial_no)
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js
index 27fcbb7..98116ec 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.js
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.js
@@ -510,22 +510,31 @@
calculate_amount: function(frm) {
frm.events.calculate_total_additional_costs(frm);
-
- const total_basic_amount = frappe.utils.sum(
- (frm.doc.items || []).map(function(i) { return i.t_warehouse ? flt(i.basic_amount) : 0; })
- );
-
+ let total_basic_amount = 0;
+ if (in_list(["Repack", "Manufacture"], frm.doc.purpose)) {
+ total_basic_amount = frappe.utils.sum(
+ (frm.doc.items || []).map(function(i) {
+ return i.is_finished_item ? flt(i.basic_amount) : 0;
+ })
+ );
+ } else {
+ total_basic_amount = frappe.utils.sum(
+ (frm.doc.items || []).map(function(i) {
+ return i.t_warehouse ? flt(i.basic_amount) : 0;
+ })
+ );
+ }
+
for (let i in frm.doc.items) {
let item = frm.doc.items[i];
- if (item.t_warehouse && total_basic_amount) {
+ if (((in_list(["Repack", "Manufacture"], frm.doc.purpose) && item.is_finished_item) || item.t_warehouse) && total_basic_amount) {
item.additional_cost = (flt(item.basic_amount) / total_basic_amount) * frm.doc.total_additional_costs;
} else {
item.additional_cost = 0;
}
- item.amount = flt(item.basic_amount + flt(item.additional_cost),
- precision("amount", item));
+ item.amount = flt(item.basic_amount + flt(item.additional_cost), precision("amount", item));
if (flt(item.transfer_qty)) {
item.valuation_rate = flt(flt(item.basic_rate) + (flt(item.additional_cost) / flt(item.transfer_qty)),
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.json b/erpnext/stock/doctype/stock_entry/stock_entry.json
index 61e0df6..5aed081 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.json
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.json
@@ -644,9 +644,10 @@
],
"icon": "fa fa-file-text",
"idx": 1,
+ "index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2020-08-11 19:10:07.954981",
+ "modified": "2020-09-09 12:59:02.508943",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Entry",
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index 32d7e6e..afdb54c 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -18,7 +18,7 @@
from frappe.model.mapper import get_mapped_doc
from erpnext.stock.doctype.serial_no.serial_no import update_serial_nos_after_submit, get_serial_nos
from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import OpeningEntryAccountError
-
+from erpnext.accounts.general_ledger import process_gl_map
import json
from six import string_types, itervalues, iteritems
@@ -58,6 +58,7 @@
self.validate_warehouse()
self.validate_work_order()
self.validate_bom()
+ self.mark_finished_and_scrap_items()
self.validate_finished_goods()
self.validate_with_material_request()
self.validate_batch()
@@ -75,13 +76,11 @@
else:
set_batch_nos(self, 's_warehouse')
- self.set_incoming_rate()
self.validate_serialized_batch()
self.set_actual_qty()
- self.calculate_rate_and_amount(update_finished_item_rate=False)
+ self.calculate_rate_and_amount()
def on_submit(self):
-
self.update_stock_ledger()
update_serial_nos_after_submit(self, "items")
@@ -89,11 +88,15 @@
self.validate_purchase_order()
if self.purchase_order and self.purpose == "Send to Subcontractor":
self.update_purchase_order_supplied_items()
+
self.make_gl_entries()
+
+ self.repost_future_sle_and_gle()
self.update_cost_in_project()
self.validate_reserved_serial_no_consumption()
self.update_transferred_qty()
self.update_quality_inspection()
+
if self.work_order and self.purpose == "Manufacture":
self.update_so_in_serial_number()
@@ -113,9 +116,10 @@
self.update_work_order()
self.update_stock_ledger()
- self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry')
+ self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation')
self.make_gl_entries_on_cancel()
+ self.repost_future_sle_and_gle()
self.update_cost_in_project()
self.update_transferred_qty()
self.update_quality_inspection()
@@ -256,11 +260,10 @@
def validate_fg_completed_qty(self):
if self.purpose == "Manufacture" and self.work_order:
- production_item = frappe.get_value('Work Order', self.work_order, 'production_item')
- for item in self.items:
- if item.item_code == production_item and item.t_warehouse and item.qty != self.fg_completed_qty:
+ for d in self.items:
+ if d.is_finished_item and d.qty != self.fg_completed_qty:
frappe.throw(_("Finished product quantity <b>{0}</b> and For Quantity <b>{1}</b> cannot be different")
- .format(item.qty, self.fg_completed_qty))
+ .format(d.qty, self.fg_completed_qty))
def validate_difference_account(self):
if not cint(erpnext.is_perpetual_inventory_enabled(self.company)):
@@ -382,21 +385,6 @@
frappe.throw(_("Stock Entries already created for Work Order ")
+ self.work_order + ":" + ", ".join(other_ste), DuplicateEntryForWorkOrderError)
- def set_incoming_rate(self):
- if self.purpose == "Repack":
- self.set_basic_rate_for_finished_goods()
-
- for d in self.items:
- if d.s_warehouse:
- args = self.get_args_for_incoming_rate(d)
- d.basic_rate = get_incoming_rate(args)
- elif d.allow_zero_valuation_rate and not d.s_warehouse:
- d.basic_rate = 0.0
- elif d.t_warehouse and not d.basic_rate:
- d.basic_rate = get_valuation_rate(d.item_code, d.t_warehouse,
- self.doctype, self.name, d.allow_zero_valuation_rate,
- currency=erpnext.get_company_currency(self.company), company=self.company)
-
def set_actual_qty(self):
allow_negative_stock = cint(frappe.db.get_value("Stock Settings", None, "allow_negative_stock"))
@@ -432,57 +420,64 @@
d.serial_no = transferred_serial_no
def get_stock_and_rate(self):
+ """
+ Updates rate and availability of all the items.
+ Called from Update Rate and Availability button.
+ """
self.set_work_order_details()
self.set_transfer_qty()
self.set_actual_qty()
self.calculate_rate_and_amount()
- def calculate_rate_and_amount(self, force=False,
- update_finished_item_rate=True, raise_error_if_no_rate=True):
- self.set_basic_rate(force, update_finished_item_rate, raise_error_if_no_rate)
+ def calculate_rate_and_amount(self, reset_outgoing_rate=True, raise_error_if_no_rate=True):
+ self.set_basic_rate(reset_outgoing_rate, raise_error_if_no_rate)
self.distribute_additional_costs()
self.update_valuation_rate()
self.set_total_incoming_outgoing_value()
self.set_total_amount()
- def set_basic_rate(self, force=False, update_finished_item_rate=True, raise_error_if_no_rate=True):
- """get stock and incoming rate on posting date"""
- raw_material_cost = 0.0
- scrap_material_cost = 0.0
- fg_basic_rate = 0.0
+ def set_basic_rate(self, reset_outgoing_rate=True, raise_error_if_no_rate=True):
+ """
+ Set rate for outgoing, scrapped and finished items
+ """
+ # Set rate for outgoing items
+ outgoing_items_cost = self.set_rate_for_outgoing_items(reset_outgoing_rate)
+ # Set basic rate for incoming items
for d in self.get('items'):
- if d.t_warehouse: fg_basic_rate = flt(d.basic_rate)
- args = self.get_args_for_incoming_rate(d)
+ if d.s_warehouse or d.set_basic_rate_manually: continue
- # get basic rate
- if not d.bom_no:
- if (not flt(d.basic_rate) and not d.allow_zero_valuation_rate) or d.s_warehouse or force:
- basic_rate = flt(get_incoming_rate(args, raise_error_if_no_rate), self.precision("basic_rate", d))
- if basic_rate > 0:
- d.basic_rate = basic_rate
+ if d.allow_zero_valuation_rate:
+ d.basic_rate = 0.0
+ elif d.is_finished_item:
+ if self.purpose == "Manufacture":
+ d.basic_rate = self.get_basic_rate_for_manufactured_item(d.transfer_qty, outgoing_items_cost)
+ elif self.purpose == "Repack":
+ d.basic_rate = self.get_basic_rate_for_repacked_items(d.transfer_qty, outgoing_items_cost)
+
+ if not d.basic_rate and not d.allow_zero_valuation_rate:
+ d.basic_rate = get_valuation_rate(d.item_code, d.t_warehouse,
+ self.doctype, self.name, d.allow_zero_valuation_rate,
+ currency=erpnext.get_company_currency(self.company), company=self.company,
+ raise_error_if_no_rate=raise_error_if_no_rate)
+
+ d.basic_rate = flt(d.basic_rate, d.precision("basic_rate"))
+ d.basic_amount = flt(flt(d.transfer_qty) * flt(d.basic_rate), d.precision("basic_amount"))
+
+ def set_rate_for_outgoing_items(self, reset_outgoing_rate=True):
+ outgoing_items_cost = 0.0
+ for d in self.get('items'):
+ if d.s_warehouse:
+ if reset_outgoing_rate:
+ args = self.get_args_for_incoming_rate(d)
+ rate = get_incoming_rate(args)
+ if rate > 0:
+ d.basic_rate = rate
d.basic_amount = flt(flt(d.transfer_qty) * flt(d.basic_rate), d.precision("basic_amount"))
if not d.t_warehouse:
- raw_material_cost += flt(d.basic_amount)
-
- # get scrap items basic rate
- if d.bom_no:
- if not flt(d.basic_rate) and not d.allow_zero_valuation_rate and \
- getattr(self, "pro_doc", frappe._dict()).scrap_warehouse == d.t_warehouse:
- basic_rate = flt(get_incoming_rate(args, raise_error_if_no_rate),
- self.precision("basic_rate", d))
- if basic_rate > 0:
- d.basic_rate = basic_rate
- d.basic_amount = flt(flt(d.transfer_qty) * flt(d.basic_rate), d.precision("basic_amount"))
-
- if getattr(self, "pro_doc", frappe._dict()).scrap_warehouse == d.t_warehouse:
-
- scrap_material_cost += flt(d.basic_amount)
-
- number_of_fg_items = len([t.t_warehouse for t in self.get("items") if t.t_warehouse])
- if (fg_basic_rate == 0.0 and number_of_fg_items == 1) or update_finished_item_rate:
- self.set_basic_rate_for_finished_goods(raw_material_cost, scrap_material_cost)
+ outgoing_items_cost += flt(d.basic_amount)
+ return outgoing_items_cost
def get_args_for_incoming_rate(self, item):
return frappe._dict({
@@ -498,44 +493,44 @@
"allow_zero_valuation": item.allow_zero_valuation_rate,
})
- def set_basic_rate_for_finished_goods(self, raw_material_cost=0, scrap_material_cost=0):
- total_fg_qty = 0
- if not raw_material_cost and self.get("items"):
- raw_material_cost = sum([flt(row.basic_amount) for row in self.items
- if row.s_warehouse and not row.t_warehouse])
+ def get_basic_rate_for_repacked_items(self, finished_item_qty, outgoing_items_cost):
+ finished_items = [d.item_code for d in self.get("items") if d.is_finished_item]
+ if len(finished_items) == 1:
+ return flt(outgoing_items_cost / finished_item_qty)
+ else:
+ unique_finished_items = set(finished_items)
+ if len(unique_finished_items) == 1:
+ total_fg_qty = sum([flt(d.transfer_qty) for d in self.items if d.is_finished_item])
+ return flt(outgoing_items_cost / total_fg_qty)
- total_fg_qty = sum([flt(row.qty) for row in self.items
- if row.t_warehouse and not row.s_warehouse])
+ def get_basic_rate_for_manufactured_item(self, finished_item_qty, outgoing_items_cost=0):
+ scrap_items_cost = sum([flt(d.basic_amount) for d in self.get("items") if d.is_scrap_item])
- if self.purpose in ["Manufacture", "Repack"]:
- for d in self.get("items"):
- if (d.transfer_qty and (d.bom_no or d.t_warehouse)
- and (getattr(self, "pro_doc", frappe._dict()).scrap_warehouse != d.t_warehouse)):
+ # Get raw materials cost from BOM if multiple material consumption entries
+ if frappe.db.get_single_value("Manufacturing Settings", "material_consumption"):
+ bom_items = self.get_bom_raw_materials(finished_item_qty)
+ outgoing_items_cost = sum([flt(row.qty)*flt(row.rate) for row in bom_items.values()])
- if (self.work_order and self.purpose == "Manufacture"
- and frappe.db.get_single_value("Manufacturing Settings", "material_consumption")):
- bom_items = self.get_bom_raw_materials(d.transfer_qty)
- raw_material_cost = sum([flt(row.qty)*flt(row.rate) for row in bom_items.values()])
-
- if raw_material_cost and self.purpose == "Manufacture":
- d.basic_rate = flt((raw_material_cost - scrap_material_cost) / flt(d.transfer_qty), d.precision("basic_rate"))
- d.basic_amount = flt((raw_material_cost - scrap_material_cost), d.precision("basic_amount"))
- elif self.purpose == "Repack" and total_fg_qty and not d.set_basic_rate_manually:
- d.basic_rate = flt(raw_material_cost) / flt(total_fg_qty)
- d.basic_amount = d.basic_rate * flt(d.qty)
+ return flt(outgoing_items_cost - scrap_items_cost)
def distribute_additional_costs(self):
- if self.purpose == "Material Issue":
+ # If no incoming items, set additional costs blank
+ if not any([d.item_code for d in self.items if d.t_warehouse]):
self.additional_costs = []
self.total_additional_costs = sum([flt(t.amount) for t in self.get("additional_costs")])
- total_basic_amount = sum([flt(t.basic_amount) for t in self.get("items") if t.t_warehouse])
- for d in self.get("items"):
- if d.t_warehouse and total_basic_amount:
- d.additional_cost = (flt(d.basic_amount) / total_basic_amount) * self.total_additional_costs
- else:
- d.additional_cost = 0
+ if self.purpose in ("Repack", "Manufacture"):
+ incoming_items_cost = sum([flt(t.basic_amount) for t in self.get("items") if t.is_finished_item])
+ else:
+ incoming_items_cost = sum([flt(t.basic_amount) for t in self.get("items") if t.t_warehouse])
+
+ if incoming_items_cost:
+ for d in self.get("items"):
+ if (self.purpose in ("Repack", "Manufacture") and d.is_finished_item) or d.t_warehouse:
+ d.additional_cost = (flt(d.basic_amount) / incoming_items_cost) * self.total_additional_costs
+ else:
+ d.additional_cost = 0
def update_valuation_rate(self):
for d in self.get("items"):
@@ -638,71 +633,115 @@
item_code = d.original_item or d.item_code
validate_bom_no(item_code, d.bom_no)
+ def mark_finished_and_scrap_items(self):
+ if self.purpose in ("Repack", "Manufacture"):
+ if any([d.item_code for d in self.items if (d.is_finished_item and d.t_warehouse)]):
+ return
+
+ finished_item = self.get_finished_item()
+
+ for d in self.items:
+ if d.t_warehouse and not d.s_warehouse:
+ if self.purpose=="Repack" or d.item_code == finished_item:
+ d.is_finished_item = 1
+ else:
+ d.is_scrap_item = 1
+ else:
+ d.is_finished_item = 0
+ d.is_scrap_item = 0
+
+ def get_finished_item(self):
+ finished_item = None
+ if self.work_order:
+ finished_item = frappe.db.get_value("Work Order", self.work_order, "production_item")
+ elif self.bom_no:
+ finished_item = frappe.db.get_value("BOM", self.bom_no, "item")
+
+ return finished_item
+
def validate_finished_goods(self):
"""validation: finished good quantity should be same as manufacturing quantity"""
if not self.work_order: return
- items_with_target_warehouse = []
- allowance_percentage = flt(frappe.db.get_single_value("Manufacturing Settings",
- "overproduction_percentage_for_work_order"))
-
production_item, wo_qty = frappe.db.get_value("Work Order",
self.work_order, ["production_item", "qty"])
+ number_of_finished_items = 0
for d in self.get('items'):
- if (self.purpose != "Send to Subcontractor" and d.bom_no
- and flt(d.transfer_qty) > flt(self.fg_completed_qty) and d.item_code == production_item):
- frappe.throw(_("Quantity in row {0} ({1}) must be same as manufactured quantity {2}"). \
- format(d.idx, d.transfer_qty, self.fg_completed_qty))
+ if d.is_finished_item:
+ if d.item_code != production_item:
+ frappe.throw(_("Finished Item {0} does not match with Work Order {1}")
+ .format(d.item_code, self.work_order))
+ elif flt(d.transfer_qty) > flt(self.fg_completed_qty):
+ frappe.throw(_("Quantity in row {0} ({1}) must be same as manufactured quantity {2}"). \
+ format(d.idx, d.transfer_qty, self.fg_completed_qty))
+ number_of_finished_items += 1
- if self.work_order and self.purpose == "Manufacture" and d.t_warehouse:
- items_with_target_warehouse.append(d.item_code)
+ if number_of_finished_items > 1:
+ frappe.throw(_("Multiple items cannot be marked as finished item"))
- if self.work_order and self.purpose == "Manufacture":
+ if self.purpose == "Manufacture":
+ allowance_percentage = flt(frappe.db.get_single_value("Manufacturing Settings",
+ "overproduction_percentage_for_work_order"))
+
allowed_qty = wo_qty + (allowance_percentage/100 * wo_qty)
if self.fg_completed_qty > allowed_qty:
frappe.throw(_("For quantity {0} should not be greater than work order quantity {1}")
.format(flt(self.fg_completed_qty), wo_qty))
- if production_item not in items_with_target_warehouse:
- frappe.throw(_("Finished Item {0} must be entered for Manufacture type entry")
- .format(production_item))
-
def update_stock_ledger(self):
sl_entries = []
+ finished_item_row = self.get_finished_item_row()
- # make sl entries for source warehouse first, then do for target warehouse
- for d in self.get('items'):
- if cstr(d.s_warehouse):
- sl_entries.append(self.get_sl_entries(d, {
- "warehouse": cstr(d.s_warehouse),
- "actual_qty": -flt(d.transfer_qty),
- "incoming_rate": 0
- }))
+ # make sl entries for source warehouse first
+ self.get_sle_for_source_warehouse(sl_entries, finished_item_row)
- for d in self.get('items'):
- if cstr(d.t_warehouse):
- sl_entries.append(self.get_sl_entries(d, {
- "warehouse": cstr(d.t_warehouse),
- "actual_qty": flt(d.transfer_qty),
- "incoming_rate": flt(d.valuation_rate)
- }))
-
- # On cancellation, make stock ledger entry for
- # target warehouse first, to update serial no values properly
-
- # if cstr(d.s_warehouse) and self.docstatus == 2:
- # sl_entries.append(self.get_sl_entries(d, {
- # "warehouse": cstr(d.s_warehouse),
- # "actual_qty": -flt(d.transfer_qty),
- # "incoming_rate": 0
- # }))
-
+ # SLE for target warehouse
+ self.get_sle_for_target_warehouse(sl_entries, finished_item_row)
+
+ # reverse sl entries if cancel
if self.docstatus == 2:
sl_entries.reverse()
self.make_sl_entries(sl_entries)
+ def get_finished_item_row(self):
+ finished_item_row = None
+ if self.purpose in ("Manufacture", "Repack"):
+ for d in self.get('items'):
+ if d.is_finished_item:
+ finished_item_row = d
+
+ return finished_item_row
+
+ def get_sle_for_source_warehouse(self, sl_entries, finished_item_row):
+ for d in self.get('items'):
+ if cstr(d.s_warehouse):
+ sle = self.get_sl_entries(d, {
+ "warehouse": cstr(d.s_warehouse),
+ "actual_qty": -flt(d.transfer_qty),
+ "incoming_rate": 0
+ })
+ if cstr(d.t_warehouse):
+ sle.dependant_sle_voucher_detail_no = d.name
+ elif finished_item_row and (finished_item_row.item_code != d.item_code or finished_item_row.t_warehouse != d.s_warehouse):
+ sle.dependant_sle_voucher_detail_no = finished_item_row.name
+
+ sl_entries.append(sle)
+
+ def get_sle_for_target_warehouse(self, sl_entries, finished_item_row):
+ for d in self.get('items'):
+ if cstr(d.t_warehouse):
+ sle = self.get_sl_entries(d, {
+ "warehouse": cstr(d.t_warehouse),
+ "actual_qty": flt(d.transfer_qty),
+ "incoming_rate": flt(d.valuation_rate)
+ })
+ if cstr(d.s_warehouse) or (finished_item_row and d.name == finished_item_row.name):
+ sle.recalculate_rate = 1
+
+ sl_entries.append(sle)
+
def get_gl_entries(self, warehouse_account):
gl_entries = super(StockEntry, self).get_gl_entries(warehouse_account)
@@ -747,7 +786,7 @@
"credit": -1 * amount # put it as negative credit instead of debit purposefully
}, item=d))
- return gl_entries
+ return process_gl_map(gl_entries)
def update_work_order(self):
def _validate_work_order(pro_doc):
@@ -996,6 +1035,7 @@
"stock_uom": item.stock_uom,
"expense_account": item.get("expense_account"),
"cost_center": item.get("buying_cost_center"),
+ "is_finished_item": 1
}
}, bom_no = self.bom_no)
@@ -1034,6 +1074,7 @@
for item in itervalues(item_dict):
item.from_warehouse = ""
+ item.is_scrap_item = 1
return item_dict
def get_unconsumed_raw_materials(self):
@@ -1246,6 +1287,8 @@
se_child.subcontracted_item = item_dict[d].get("main_item_code")
se_child.cost_center = (item_dict[d].get("cost_center") or
get_default_cost_center(item_dict[d], company = self.company))
+ se_child.is_finished_item = item_dict[d].get("is_finished_item", 0)
+ se_child.is_scrap_item = item_dict[d].get("is_scrap_item", 0)
for field in ["idx", "po_detail", "original_item",
"expense_account", "description", "item_name"]:
diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
index 9b6744c..1a64185 100644
--- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
@@ -6,7 +6,6 @@
import frappe.defaults
from frappe.utils import flt, nowdate, nowtime
from erpnext.stock.doctype.serial_no.serial_no import *
-from erpnext import set_perpetual_inventory
from erpnext.stock.doctype.stock_ledger_entry.stock_ledger_entry import StockFreezeError
from erpnext.stock.stock_ledger import get_previous_sle
from frappe.permissions import add_user_permission, remove_user_permission
@@ -32,7 +31,6 @@
class TestStockEntry(unittest.TestCase):
def tearDown(self):
frappe.set_user("Administrator")
- set_perpetual_inventory(0)
def test_fifo(self):
frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1)
@@ -213,7 +211,6 @@
def test_repack_no_change_in_valuation(self):
company = frappe.db.get_value('Warehouse', '_Test Warehouse - _TC', 'company')
- set_perpetual_inventory(0, company)
make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", qty=50, basic_rate=100)
make_stock_entry(item_code="_Test Item Home Desktop 100", target="_Test Warehouse - _TC",
@@ -235,8 +232,6 @@
order by account desc""", repack.name, as_dict=1)
self.assertFalse(gl_entries)
- set_perpetual_inventory(0, repack.company)
-
def test_repack_with_additional_costs(self):
company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company')
@@ -474,7 +469,6 @@
def test_warehouse_company_validation(self):
company = frappe.db.get_value('Warehouse', '_Test Warehouse 2 - _TC1', 'company')
- set_perpetual_inventory(0, company)
frappe.get_doc("User", "test2@example.com")\
.add_roles("Sales User", "Sales Manager", "Stock User", "Stock Manager")
frappe.set_user("test2@example.com")
@@ -500,7 +494,7 @@
st1 = frappe.copy_doc(test_records[0])
st1.company = "_Test Company 1"
- set_perpetual_inventory(0, st1.company)
+
frappe.set_user("test@example.com")
st1.get("items")[0].t_warehouse="_Test Warehouse 2 - _TC1"
self.assertRaises(frappe.PermissionError, st1.insert)
@@ -698,47 +692,54 @@
repack.insert()
self.assertRaises(frappe.ValidationError, repack.submit)
- def test_material_consumption(self):
- from erpnext.manufacturing.doctype.work_order.work_order \
- import make_stock_entry as _make_stock_entry
- bom_no = frappe.db.get_value("BOM", {"item": "_Test FG Item 2",
- "is_default": 1, "docstatus": 1})
+ # def test_material_consumption(self):
+ # frappe.db.set_value("Manufacturing Settings", None, "backflush_raw_materials_based_on", "BOM")
+ # frappe.db.set_value("Manufacturing Settings", None, "material_consumption", "0")
- work_order = frappe.new_doc("Work Order")
- work_order.update({
- "company": "_Test Company",
- "fg_warehouse": "_Test Warehouse 1 - _TC",
- "production_item": "_Test FG Item 2",
- "bom_no": bom_no,
- "qty": 4.0,
- "stock_uom": "_Test UOM",
- "wip_warehouse": "_Test Warehouse - _TC",
- "additional_operating_cost": 1000
- })
- work_order.insert()
- work_order.submit()
+ # from erpnext.manufacturing.doctype.work_order.work_order \
+ # import make_stock_entry as _make_stock_entry
+ # bom_no = frappe.db.get_value("BOM", {"item": "_Test FG Item 2",
+ # "is_default": 1, "docstatus": 1})
- make_stock_entry(item_code="_Test Serialized Item With Series", target="_Test Warehouse - _TC", qty=50, basic_rate=100)
- make_stock_entry(item_code="_Test Item 2", target="_Test Warehouse - _TC", qty=50, basic_rate=20)
+ # work_order = frappe.new_doc("Work Order")
+ # work_order.update({
+ # "company": "_Test Company",
+ # "fg_warehouse": "_Test Warehouse 1 - _TC",
+ # "production_item": "_Test FG Item 2",
+ # "bom_no": bom_no,
+ # "qty": 4.0,
+ # "stock_uom": "_Test UOM",
+ # "wip_warehouse": "_Test Warehouse - _TC",
+ # "additional_operating_cost": 1000,
+ # "use_multi_level_bom": 1
+ # })
+ # work_order.insert()
+ # work_order.submit()
- item_quantity = {
- '_Test Item': 10.0,
- '_Test Item 2': 12.0,
- '_Test Serialized Item With Series': 6.0
- }
+ # make_stock_entry(item_code="_Test Serialized Item With Series", target="_Test Warehouse - _TC", qty=50, basic_rate=100)
+ # make_stock_entry(item_code="_Test Item 2", target="_Test Warehouse - _TC", qty=50, basic_rate=20)
- stock_entry = frappe.get_doc(_make_stock_entry(work_order.name, "Material Consumption for Manufacture", 2))
- for d in stock_entry.get('items'):
- self.assertEqual(item_quantity.get(d.item_code), d.qty)
+ # item_quantity = {
+ # '_Test Item': 2.0,
+ # '_Test Item 2': 12.0,
+ # '_Test Serialized Item With Series': 6.0
+ # }
+
+ # stock_entry = frappe.get_doc(_make_stock_entry(work_order.name, "Material Consumption for Manufacture", 2))
+ # for d in stock_entry.get('items'):
+ # self.assertEqual(item_quantity.get(d.item_code), d.qty)
def test_customer_provided_parts_se(self):
create_item('CUST-0987', is_customer_provided_item = 1, customer = '_Test Customer', is_purchase_item = 0)
- se = make_stock_entry(item_code='CUST-0987', purpose = 'Material Receipt', qty=4, to_warehouse = "_Test Warehouse - _TC")
+ se = make_stock_entry(item_code='CUST-0987', purpose = 'Material Receipt',
+ qty=4, to_warehouse = "_Test Warehouse - _TC")
self.assertEqual(se.get("items")[0].allow_zero_valuation_rate, 1)
self.assertEqual(se.get("items")[0].amount, 0)
def test_gle_for_opening_stock_entry(self):
- mr = make_stock_entry(item_code="_Test Item", target="Stores - TCP1", company="_Test Company with perpetual inventory",qty=50, basic_rate=100, expense_account="Stock Adjustment - TCP1", is_opening="Yes", do_not_save=True)
+ mr = make_stock_entry(item_code="_Test Item", target="Stores - TCP1",
+ company="_Test Company with perpetual inventory", qty=50, basic_rate=100,
+ expense_account="Stock Adjustment - TCP1", is_opening="Yes", do_not_save=True)
self.assertRaises(OpeningEntryAccountError, mr.save)
@@ -759,8 +760,8 @@
"company":"_Test Company with perpetual inventory",
"items":[
{
- "item_code":"Basil Leaves",
- "description":"Basil Leaves",
+ "item_code":"_Test Item",
+ "description":"_Test Item",
"qty": 1,
"basic_rate": 0,
"uom":"Nos",
@@ -769,8 +770,8 @@
"cost_center": "Main - TCP1"
},
{
- "item_code":"Basil Leaves",
- "description":"Basil Leaves",
+ "item_code":"_Test Item",
+ "description":"_Test Item",
"qty": 2,
"basic_rate": 0,
"uom":"Nos",
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 79e8f9a..6fe6029 100644
--- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json
+++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json
@@ -13,8 +13,10 @@
"t_warehouse",
"sec_break1",
"item_code",
- "col_break2",
"item_name",
+ "col_break2",
+ "is_finished_item",
+ "is_scrap_item",
"subcontracted_item",
"section_break_8",
"description",
@@ -22,35 +24,37 @@
"item_group",
"image",
"image_view",
- "quantity_and_rate",
- "set_basic_rate_manually",
+ "quantity_section",
"qty",
- "basic_rate",
- "basic_amount",
- "additional_cost",
- "amount",
- "valuation_rate",
- "col_break3",
- "uom",
- "conversion_factor",
- "stock_uom",
"transfer_qty",
"retain_sample",
+ "column_break_20",
+ "uom",
+ "stock_uom",
+ "conversion_factor",
"sample_quantity",
+ "rates_section",
+ "basic_rate",
+ "additional_cost",
+ "valuation_rate",
+ "allow_zero_valuation_rate",
+ "col_break3",
+ "set_basic_rate_manually",
+ "basic_amount",
+ "amount",
"serial_no_batch",
"serial_no",
"col_break4",
"batch_no",
- "quality_inspection",
"accounting",
"expense_account",
- "col_break5",
"accounting_dimensions_section",
"cost_center",
+ "project",
"dimension_col_break",
"more_info",
- "allow_zero_valuation_rate",
"actual_qty",
+ "transferred_qty",
"bom_no",
"allow_alternative_item",
"col_break6",
@@ -62,9 +66,8 @@
"ste_detail",
"po_detail",
"column_break_51",
- "transferred_qty",
"reference_purchase_receipt",
- "project"
+ "quality_inspection"
],
"fields": [
{
@@ -160,11 +163,6 @@
"print_hide": 1
},
{
- "fieldname": "quantity_and_rate",
- "fieldtype": "Section Break",
- "label": "Quantity and Rate"
- },
- {
"bold": 1,
"fieldname": "qty",
"fieldtype": "Float",
@@ -322,10 +320,6 @@
"print_hide": 1
},
{
- "fieldname": "col_break5",
- "fieldtype": "Column Break"
- },
- {
"default": ":Company",
"depends_on": "eval:cint(erpnext.is_perpetual_inventory_enabled(parent.company))",
"fieldname": "cost_center",
@@ -335,6 +329,7 @@
"print_hide": 1
},
{
+ "collapsible": 1,
"fieldname": "more_info",
"fieldtype": "Section Break",
"label": "More Information"
@@ -456,6 +451,7 @@
"read_only": 1
},
{
+ "collapsible": 1,
"fieldname": "accounting_dimensions_section",
"fieldtype": "Section Break",
"label": "Accounting Dimensions"
@@ -498,6 +494,32 @@
"fieldname": "set_basic_rate_manually",
"fieldtype": "Check",
"label": "Set Basic Rate Manually"
+ },
+ {
+ "fieldname": "quantity_section",
+ "fieldtype": "Section Break",
+ "label": "Quantity"
+ },
+ {
+ "fieldname": "column_break_20",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "rates_section",
+ "fieldtype": "Section Break",
+ "label": "Rates"
+ },
+ {
+ "default": "0",
+ "fieldname": "is_scrap_item",
+ "fieldtype": "Check",
+ "label": "Is Scrap Item"
+ },
+ {
+ "default": "0",
+ "fieldname": "is_finished_item",
+ "fieldtype": "Check",
+ "label": "Is Finished Item"
}
],
"idx": 1,
diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json
index fda17e0..2463a21 100644
--- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json
+++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json
@@ -8,26 +8,33 @@
"engine": "InnoDB",
"field_order": [
"item_code",
- "serial_no",
- "batch_no",
"warehouse",
"posting_date",
"posting_time",
+ "column_break_6",
"voucher_type",
"voucher_no",
"voucher_detail_no",
+ "dependant_sle_voucher_detail_no",
+ "recalculate_rate",
+ "section_break_11",
"actual_qty",
+ "qty_after_transaction",
"incoming_rate",
"outgoing_rate",
- "stock_uom",
- "qty_after_transaction",
+ "column_break_17",
"valuation_rate",
"stock_value",
"stock_value_difference",
"stock_queue",
- "project",
+ "section_break_21",
"company",
+ "stock_uom",
+ "project",
+ "batch_no",
+ "column_break_26",
"fiscal_year",
+ "serial_no",
"is_cancelled",
"to_rename"
],
@@ -50,7 +57,6 @@
{
"fieldname": "serial_no",
"fieldtype": "Long Text",
- "in_list_view": 1,
"label": "Serial No",
"print_width": "100px",
"read_only": 1,
@@ -59,7 +65,6 @@
{
"fieldname": "batch_no",
"fieldtype": "Data",
- "in_list_view": 1,
"label": "Batch No",
"oldfieldname": "batch_no",
"oldfieldtype": "Data",
@@ -119,6 +124,7 @@
"fieldname": "voucher_no",
"fieldtype": "Dynamic Link",
"in_filter": 1,
+ "in_list_view": 1,
"in_standard_filter": 1,
"label": "Voucher No",
"oldfieldname": "voucher_no",
@@ -142,6 +148,7 @@
"fieldname": "actual_qty",
"fieldtype": "Float",
"in_filter": 1,
+ "in_list_view": 1,
"label": "Actual Quantity",
"oldfieldname": "actual_qty",
"oldfieldtype": "Currency",
@@ -152,6 +159,7 @@
{
"fieldname": "incoming_rate",
"fieldtype": "Currency",
+ "in_list_view": 1,
"label": "Incoming Rate",
"oldfieldname": "incoming_rate",
"oldfieldtype": "Currency",
@@ -217,13 +225,11 @@
{
"fieldname": "stock_queue",
"fieldtype": "Text",
- "hidden": 1,
"label": "Stock Queue (FIFO)",
"oldfieldname": "fcfs_stack",
"oldfieldtype": "Text",
"print_hide": 1,
- "read_only": 1,
- "report_hide": 1
+ "read_only": 1
},
{
"fieldname": "project",
@@ -269,14 +275,48 @@
"hidden": 1,
"label": "To Rename",
"search_index": 1
+ },
+ {
+ "fieldname": "dependant_sle_voucher_detail_no",
+ "fieldtype": "Data",
+ "label": "Dependant SLE Voucher Detail No"
+ },
+ {
+ "fieldname": "column_break_6",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "section_break_11",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "column_break_17",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "section_break_21",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "column_break_26",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "fieldname": "recalculate_rate",
+ "fieldtype": "Check",
+ "label": "Recalculate Incoming/Outgoing Rate",
+ "no_copy": 1,
+ "read_only": 1
}
],
"hide_toolbar": 1,
"icon": "fa fa-list",
"idx": 1,
"in_create": 1,
+ "index_web_pages_for_search": 1,
"links": [],
- "modified": "2020-04-23 05:57:03.985520",
+ "modified": "2020-09-07 11:10:35.318872",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Ledger Entry",
diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
index bb356f6..a5c303c 100644
--- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
+++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
@@ -10,8 +10,10 @@
from datetime import date
from erpnext.controllers.item_variant import ItemTemplateCannotHaveStock
from erpnext.accounts.utils import get_fiscal_year
+from frappe.core.doctype.role.role import get_users
class StockFreezeError(frappe.ValidationError): pass
+class BackDatedStockTransaction(frappe.ValidationError): pass
exclude_from_linked_with = True
@@ -34,7 +36,6 @@
self.validate_and_set_fiscal_year()
self.block_transactions_against_group_warehouse()
self.validate_with_last_transaction_posting_time()
- self.validate_future_posting()
def on_submit(self):
self.check_stock_frozen_date()
@@ -48,7 +49,7 @@
def calculate_batch_qty(self):
if self.batch_no:
batch_qty = frappe.db.get_value("Stock Ledger Entry",
- {"docstatus": 1, "batch_no": self.batch_no},
+ {"docstatus": 1, "batch_no": self.batch_no, "is_cancelled": 0},
"sum(actual_qty)") or 0
frappe.db.set_value("Batch", self.batch_no, "batch_qty", batch_qty)
@@ -88,14 +89,14 @@
# check if batch number is required
if self.voucher_type != 'Stock Reconciliation':
- if item_det.has_batch_no ==1:
+ if item_det.has_batch_no == 1:
batch_item = self.item_code if self.item_code == item_det.item_name else self.item_code + ":" + item_det.item_name
if not self.batch_no:
frappe.throw(_("Batch number is mandatory for Item {0}").format(batch_item))
elif not frappe.db.get_value("Batch",{"item": self.item_code, "name": self.batch_no}):
frappe.throw(_("{0} is not a valid Batch Number for Item {1}").format(self.batch_no, batch_item))
- elif item_det.has_batch_no ==0 and self.batch_no:
+ elif item_det.has_batch_no == 0 and self.batch_no and self.is_cancelled == 0:
frappe.throw(_("The Item {0} cannot have Batch").format(self.item_code))
if item_det.has_variants:
@@ -142,28 +143,28 @@
is_group_warehouse(self.warehouse)
def validate_with_last_transaction_posting_time(self):
- last_transaction_time = frappe.db.sql("""
- select MAX(timestamp(posting_date, posting_time)) as posting_time
- from `tabStock Ledger Entry`
- where docstatus = 1 and item_code = %s
- and warehouse = %s""", (self.item_code, self.warehouse))[0][0]
+ authorized_role = frappe.db.get_single_value("Stock Settings", "role_allowed_to_create_edit_back_dated_transactions")
+ if authorized_role:
+ authorized_users = get_users(authorized_role)
+ if authorized_users and frappe.session.user not in authorized_users:
+ last_transaction_time = frappe.db.sql("""
+ select MAX(timestamp(posting_date, posting_time)) as posting_time
+ from `tabStock Ledger Entry`
+ where docstatus = 1 and item_code = %s
+ and warehouse = %s""", (self.item_code, self.warehouse))[0][0]
- cur_doc_posting_datetime = "%s %s" % (self.posting_date, self.get("posting_time") or "00:00:00")
+ cur_doc_posting_datetime = "%s %s" % (self.posting_date, self.get("posting_time") or "00:00:00")
- if last_transaction_time and get_datetime(cur_doc_posting_datetime) < get_datetime(last_transaction_time):
- msg = _("Last Stock Transaction for item {0} under warehouse {1} was on {2}.").format(frappe.bold(self.item_code),
- frappe.bold(self.warehouse), frappe.bold(last_transaction_time))
+ if last_transaction_time and get_datetime(cur_doc_posting_datetime) < get_datetime(last_transaction_time):
+ msg = _("Last Stock Transaction for item {0} under warehouse {1} was on {2}.").format(frappe.bold(self.item_code),
+ frappe.bold(self.warehouse), frappe.bold(last_transaction_time))
- msg += "<br><br>" + _("Stock Transactions for Item {0} under warehouse {1} cannot be posted before this time.").format(
- frappe.bold(self.item_code), frappe.bold(self.warehouse))
+ msg += "<br><br>" + _("You are not authorized to make/edit Stock Transactions for Item {0} under warehouse {1} before this time.").format(
+ frappe.bold(self.item_code), frappe.bold(self.warehouse))
- msg += "<br><br>" + _("Please remove this item and try to submit again or update the posting time.")
- frappe.throw(msg, title=_("Backdated Stock Entry"))
-
- def validate_future_posting(self):
- if date_diff(self.posting_date, getdate()) > 0:
- msg = _("Posting future stock transactions are not allowed due to Immutable Ledger")
- frappe.throw(msg, title=_("Future Posting Not Allowed"))
+ msg += "<br><br>" + _("Please contact any of the following users to {} this transaction.")
+ msg += "<br>" + "<br>".join(authorized_users)
+ frappe.throw(msg, BackDatedStockTransaction, title=_("Backdated Stock Entry"))
def on_doctype_update():
if not frappe.db.has_index('tabStock Ledger Entry', 'posting_sort_index'):
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 04dae83..59f1f39 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
@@ -5,8 +5,397 @@
import frappe
import unittest
-
-# test_records = frappe.get_test_records('Stock Ledger Entry')
+from frappe.utils import today, add_days
+from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
+from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation \
+ import create_stock_reconciliation
+from erpnext.stock.doctype.item.test_item import make_item
+from erpnext.stock.stock_ledger import get_previous_sle
+from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
+from erpnext.stock.doctype.landed_cost_voucher.test_landed_cost_voucher import create_landed_cost_voucher
+from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
+from erpnext.stock.doctype.stock_ledger_entry.stock_ledger_entry import BackDatedStockTransaction
class TestStockLedgerEntry(unittest.TestCase):
- pass
+ def setUp(self):
+ items = create_items()
+
+ # delete SLE and BINs for all items
+ frappe.db.sql("delete from `tabStock Ledger Entry` where item_code in (%s)" % (', '.join(['%s']*len(items))), items)
+ frappe.db.sql("delete from `tabBin` where item_code in (%s)" % (', '.join(['%s']*len(items))), items)
+
+ def test_item_cost_reposting(self):
+ company = "_Test Company"
+
+ # _Test Item for Reposting at Stores warehouse on 10-04-2020: Qty = 50, Rate = 100
+ create_stock_reconciliation(
+ item_code="_Test Item for Reposting",
+ warehouse="Stores - _TC",
+ qty=50,
+ rate=100,
+ company=company,
+ expense_account = "Stock Adjustment - _TC",
+ posting_date='2020-04-10',
+ posting_time='14:00'
+ )
+
+ # _Test Item for Reposting at FG warehouse on 20-04-2020: Qty = 10, Rate = 200
+ create_stock_reconciliation(
+ item_code="_Test Item for Reposting",
+ warehouse="Finished Goods - _TC",
+ qty=10,
+ rate=200,
+ company=company,
+ expense_account = "Stock Adjustment - _TC",
+ posting_date='2020-04-20',
+ posting_time='14:00'
+ )
+
+ # _Test Item for Reposting transferred from Stores to FG warehouse on 30-04-2020
+ make_stock_entry(
+ item_code="_Test Item for Reposting",
+ source="Stores - _TC",
+ target="Finished Goods - _TC",
+ company=company,
+ qty=10,
+ expense_account="Stock Adjustment - _TC",
+ posting_date='2020-04-30',
+ posting_time='14:00'
+ )
+ target_wh_sle = get_previous_sle({
+ "item_code": "_Test Item for Reposting",
+ "warehouse": "Finished Goods - _TC",
+ "posting_date": '2020-04-30',
+ "posting_time": '14:00'
+ })
+
+ self.assertEqual(target_wh_sle.get("valuation_rate"), 150)
+
+ # Repack entry on 5-5-2020
+ repack = create_repack_entry(company=company, posting_date='2020-05-05', posting_time='14:00')
+
+ finished_item_sle = get_previous_sle({
+ "item_code": "_Test Finished Item for Reposting",
+ "warehouse": "Finished Goods - _TC",
+ "posting_date": '2020-05-05',
+ "posting_time": '14:00'
+ })
+ self.assertEqual(finished_item_sle.get("incoming_rate"), 540)
+ self.assertEqual(finished_item_sle.get("valuation_rate"), 540)
+
+ # Reconciliation for _Test Item for Reposting at Stores on 12-04-2020: Qty = 50, Rate = 150
+ create_stock_reconciliation(
+ item_code="_Test Item for Reposting",
+ warehouse="Stores - _TC",
+ qty=50,
+ rate=150,
+ company=company,
+ expense_account = "Stock Adjustment - _TC",
+ posting_date='2020-04-12',
+ posting_time='14:00'
+ )
+
+
+ # Check valuation rate of finished goods warehouse after back-dated entry at Stores
+ target_wh_sle = get_previous_sle({
+ "item_code": "_Test Item for Reposting",
+ "warehouse": "Finished Goods - _TC",
+ "posting_date": '2020-04-30',
+ "posting_time": '14:00'
+ })
+ self.assertEqual(target_wh_sle.get("incoming_rate"), 150)
+ self.assertEqual(target_wh_sle.get("valuation_rate"), 175)
+
+ # Check valuation rate of repacked item after back-dated entry at Stores
+ finished_item_sle = get_previous_sle({
+ "item_code": "_Test Finished Item for Reposting",
+ "warehouse": "Finished Goods - _TC",
+ "posting_date": '2020-05-05',
+ "posting_time": '14:00'
+ })
+ self.assertEqual(finished_item_sle.get("incoming_rate"), 790)
+ self.assertEqual(finished_item_sle.get("valuation_rate"), 790)
+
+ # Check updated rate in Repack entry
+ repack.reload()
+ self.assertEqual(repack.items[0].get("basic_rate"), 150)
+ self.assertEqual(repack.items[1].get("basic_rate"), 750)
+
+ def test_purchase_return_valuation_reposting(self):
+ pr = make_purchase_receipt(company="_Test Company", posting_date='2020-04-10',
+ warehouse="Stores - _TC", item_code="_Test Item for Reposting", qty=5, rate=100)
+
+ return_pr = make_purchase_receipt(company="_Test Company", posting_date='2020-04-15',
+ warehouse="Stores - _TC", item_code="_Test Item for Reposting", is_return=1, return_against=pr.name, qty=-2)
+
+ # check sle
+ outgoing_rate, stock_value_difference = frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Purchase Receipt",
+ "voucher_no": return_pr.name}, ["outgoing_rate", "stock_value_difference"])
+
+ self.assertEqual(outgoing_rate, 100)
+ self.assertEqual(stock_value_difference, -200)
+
+ create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company)
+
+ outgoing_rate, stock_value_difference = frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Purchase Receipt",
+ "voucher_no": return_pr.name}, ["outgoing_rate", "stock_value_difference"])
+
+ self.assertEqual(outgoing_rate, 110)
+ self.assertEqual(stock_value_difference, -220)
+
+ def test_sales_return_valuation_reposting(self):
+ company = "_Test Company"
+ item_code="_Test Item for Reposting"
+
+ # Purchase Return: Qty = 5, Rate = 100
+ pr = make_purchase_receipt(company=company, posting_date='2020-04-10',
+ warehouse="Stores - _TC", item_code=item_code, qty=5, rate=100)
+
+ #Delivery Note: Qty = 5, Rate = 150
+ dn = create_delivery_note(item_code=item_code, qty=5, rate=150, warehouse="Stores - _TC",
+ company=company, expense_account="Cost of Goods Sold - _TC", cost_center="Main - _TC")
+
+ # check outgoing_rate for DN
+ outgoing_rate = abs(frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Delivery Note",
+ "voucher_no": dn.name}, "stock_value_difference") / 5)
+
+ self.assertEqual(dn.items[0].incoming_rate, 100)
+ self.assertEqual(outgoing_rate, 100)
+
+ # Return Entry: Qty = -2, Rate = 150
+ return_dn = create_delivery_note(is_return=1, return_against=dn.name, item_code=item_code, qty=-2, rate=150,
+ company=company, warehouse="Stores - _TC", expense_account="Cost of Goods Sold - _TC", cost_center="Main - _TC")
+
+ # check incoming rate for Return entry
+ incoming_rate, stock_value_difference = frappe.db.get_value("Stock Ledger Entry",
+ {"voucher_type": "Delivery Note", "voucher_no": return_dn.name},
+ ["incoming_rate", "stock_value_difference"])
+
+ self.assertEqual(return_dn.items[0].incoming_rate, 100)
+ self.assertEqual(incoming_rate, 100)
+ self.assertEqual(stock_value_difference, 200)
+
+ #-------------------------------
+
+ # Landed Cost Voucher to update the rate of incoming Purchase Return: Additional cost = 50
+ lcv = create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company)
+
+ # check outgoing_rate for DN after reposting
+ outgoing_rate = abs(frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Delivery Note",
+ "voucher_no": dn.name}, "stock_value_difference") / 5)
+ self.assertEqual(outgoing_rate, 110)
+
+ dn.reload()
+ self.assertEqual(dn.items[0].incoming_rate, 110)
+
+ # check incoming rate for Return entry after reposting
+ incoming_rate, stock_value_difference = frappe.db.get_value("Stock Ledger Entry",
+ {"voucher_type": "Delivery Note", "voucher_no": return_dn.name},
+ ["incoming_rate", "stock_value_difference"])
+
+ self.assertEqual(incoming_rate, 110)
+ self.assertEqual(stock_value_difference, 220)
+
+ return_dn.reload()
+ self.assertEqual(return_dn.items[0].incoming_rate, 110)
+
+ # Cleanup data
+ return_dn.cancel()
+ dn.cancel()
+ lcv.cancel()
+ pr.cancel()
+
+ def test_reposting_of_sales_return_for_packed_item(self):
+ company = "_Test Company"
+ packed_item_code="_Test Item for Reposting"
+ bundled_item = "_Test Bundled Item for Reposting"
+ create_product_bundle_item(bundled_item, [[packed_item_code, 4]])
+
+ # Purchase Return: Qty = 50, Rate = 100
+ pr = make_purchase_receipt(company=company, posting_date='2020-04-10',
+ warehouse="Stores - _TC", item_code=packed_item_code, qty=50, rate=100)
+
+ #Delivery Note: Qty = 5, Rate = 150
+ dn = create_delivery_note(item_code=bundled_item, qty=5, rate=150, warehouse="Stores - _TC",
+ company=company, expense_account="Cost of Goods Sold - _TC", cost_center="Main - _TC")
+
+ # check outgoing_rate for DN
+ outgoing_rate = abs(frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Delivery Note",
+ "voucher_no": dn.name}, "stock_value_difference") / 20)
+
+ self.assertEqual(dn.packed_items[0].incoming_rate, 100)
+ self.assertEqual(outgoing_rate, 100)
+
+ # Return Entry: Qty = -2, Rate = 150
+ return_dn = create_delivery_note(is_return=1, return_against=dn.name, item_code=bundled_item, qty=-2, rate=150,
+ company=company, warehouse="Stores - _TC", expense_account="Cost of Goods Sold - _TC", cost_center="Main - _TC")
+
+ # check incoming rate for Return entry
+ incoming_rate, stock_value_difference = frappe.db.get_value("Stock Ledger Entry",
+ {"voucher_type": "Delivery Note", "voucher_no": return_dn.name},
+ ["incoming_rate", "stock_value_difference"])
+
+ self.assertEqual(return_dn.packed_items[0].incoming_rate, 100)
+ self.assertEqual(incoming_rate, 100)
+ self.assertEqual(stock_value_difference, 800)
+
+ #-------------------------------
+
+ # Landed Cost Voucher to update the rate of incoming Purchase Return: Additional cost = 50
+ lcv = create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company)
+
+ # check outgoing_rate for DN after reposting
+ outgoing_rate = abs(frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Delivery Note",
+ "voucher_no": dn.name}, "stock_value_difference") / 20)
+ self.assertEqual(outgoing_rate, 101)
+
+ dn.reload()
+ self.assertEqual(dn.packed_items[0].incoming_rate, 101)
+
+ # check incoming rate for Return entry after reposting
+ incoming_rate, stock_value_difference = frappe.db.get_value("Stock Ledger Entry",
+ {"voucher_type": "Delivery Note", "voucher_no": return_dn.name},
+ ["incoming_rate", "stock_value_difference"])
+
+ self.assertEqual(incoming_rate, 101)
+ self.assertEqual(stock_value_difference, 808)
+
+ return_dn.reload()
+ self.assertEqual(return_dn.packed_items[0].incoming_rate, 101)
+
+ # Cleanup data
+ return_dn.cancel()
+ dn.cancel()
+ lcv.cancel()
+ pr.cancel()
+
+ def test_sub_contracted_item_costing(self):
+ from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
+
+ company = "_Test Company"
+ rm_item_code="_Test Item for Reposting"
+ subcontracted_item = "_Test Subcontracted Item for Reposting"
+
+ frappe.db.set_value("Buying Settings", None, "backflush_raw_materials_of_subcontract_based_on", "BOM")
+ make_bom(item = subcontracted_item, raw_materials =[rm_item_code], currency="INR")
+
+ # Purchase raw materials on supplier warehouse: Qty = 50, Rate = 100
+ pr = make_purchase_receipt(company=company, posting_date='2020-04-10',
+ warehouse="Stores - _TC", item_code=rm_item_code, qty=10, rate=100)
+
+ # Purchase Receipt for subcontracted item
+ pr1 = make_purchase_receipt(company=company, posting_date='2020-04-20',
+ warehouse="Finished Goods - _TC", supplier_warehouse="Stores - _TC",
+ item_code=subcontracted_item, qty=10, rate=20, is_subcontracted="Yes")
+
+ self.assertEqual(pr1.items[0].valuation_rate, 120)
+
+ # Update raw material's valuation via LCV, Additional cost = 50
+ lcv = create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company)
+
+ pr1.reload()
+ self.assertEqual(pr1.items[0].valuation_rate, 125)
+
+ # check outgoing_rate for DN after reposting
+ incoming_rate = frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Purchase Receipt",
+ "voucher_no": pr1.name, "item_code": subcontracted_item}, "incoming_rate")
+ self.assertEqual(incoming_rate, 125)
+
+ # cleanup data
+ pr1.cancel()
+ lcv.cancel()
+ pr.cancel()
+
+ def test_back_dated_entry_not_allowed(self):
+ # Back dated stock transactions are only allowed to stock managers
+ frappe.db.set_value("Stock Settings", None,
+ "role_allowed_to_create_edit_back_dated_transactions", "Stock Manager")
+
+ # Set User with Stock User role but not Stock Manager
+ frappe.set_user("test@example.com")
+ user = frappe.get_doc("User", "test@example.com")
+ user.add_roles("Stock User")
+ user.remove_roles("Stock Manager")
+
+ stock_entry_on_today = make_stock_entry(target="_Test Warehouse - _TC", qty=10, basic_rate=100)
+ back_dated_se_1 = make_stock_entry(target="_Test Warehouse - _TC", qty=10, basic_rate=100,
+ posting_date=add_days(today(), -1), do_not_submit=True)
+
+ # Block back-dated entry
+ self.assertRaises(BackDatedStockTransaction, back_dated_se_1.submit)
+
+ user.add_roles("Stock Manager")
+
+ # Back dated entry allowed to Stock Manager
+ back_dated_se_2 = make_stock_entry(target="_Test Warehouse - _TC", qty=10, basic_rate=100,
+ posting_date=add_days(today(), -1))
+
+ back_dated_se_2.cancel()
+ stock_entry_on_today.cancel()
+
+ frappe.db.set_value("Stock Settings", None, "role_allowed_to_create_edit_back_dated_transactions", None)
+ frappe.set_user("Administrator")
+
+
+def create_repack_entry(**args):
+ args = frappe._dict(args)
+ repack = frappe.new_doc("Stock Entry")
+ repack.stock_entry_type = "Repack"
+ repack.company = args.company or "_Test Company"
+ repack.posting_date = args.posting_date
+ repack.set_posting_time = 1
+ repack.append("items", {
+ "item_code": "_Test Item for Reposting",
+ "s_warehouse": "Stores - _TC",
+ "qty": 5,
+ "conversion_factor": 1,
+ "expense_account": "Stock Adjustment - _TC",
+ "cost_center": "Main - _TC"
+ })
+
+ repack.append("items", {
+ "item_code": "_Test Finished Item for Reposting",
+ "t_warehouse": "Finished Goods - _TC",
+ "qty": 1,
+ "conversion_factor": 1,
+ "expense_account": "Stock Adjustment - _TC",
+ "cost_center": "Main - _TC"
+ })
+
+ repack.append("additional_costs", {
+ "expense_account": "Freight and Forwarding Charges - _TC",
+ "description": "transport cost",
+ "amount": 40
+ })
+
+ repack.save()
+ repack.submit()
+
+ return repack
+
+def create_product_bundle_item(new_item_code, packed_items):
+ if not frappe.db.exists("Product Bundle", new_item_code):
+ item = frappe.new_doc("Product Bundle")
+ item.new_item_code = new_item_code
+
+ for d in packed_items:
+ item.append("items", {
+ "item_code": d[0],
+ "qty": d[1]
+ })
+
+ item.save()
+
+def create_items():
+ items = ["_Test Item for Reposting", "_Test Finished Item for Reposting",
+ "_Test Subcontracted Item for Reposting", "_Test Bundled Item for Reposting"]
+ for d in items:
+ properties = {"valuation_method": "FIFO"}
+ if d == "_Test Bundled Item for Reposting":
+ properties.update({"is_stock_item": 0})
+ elif d == "_Test Subcontracted Item for Reposting":
+ properties.update({"is_sub_contracted_item": 1})
+
+ make_item(d, properties=properties)
+
+ return items
\ No newline at end of file
diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
index 00b8f69..5b40292 100644
--- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
@@ -37,14 +37,16 @@
def on_submit(self):
self.update_stock_ledger()
self.make_gl_entries()
+ self.repost_future_sle_and_gle()
from erpnext.stock.doctype.serial_no.serial_no import update_serial_nos_after_submit
update_serial_nos_after_submit(self, "items")
def on_cancel(self):
- self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry')
+ self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation')
self.make_sle_on_cancel()
self.make_gl_entries_on_cancel()
+ self.repost_future_sle_and_gle()
def remove_items_with_no_change(self):
"""Remove items if qty or rate is not changed"""
diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
index 23d48d4..088456f 100644
--- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
@@ -8,12 +8,11 @@
import frappe, unittest
from frappe.utils import flt, nowdate, nowtime
from erpnext.accounts.utils import get_stock_and_account_balance
-from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory
from erpnext.stock.stock_ledger import get_previous_sle, update_entries_after
from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import EmptyStockReconciliationItemsError, get_items
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
from erpnext.stock.doctype.item.test_item import create_item
-from erpnext.stock.utils import get_stock_balance, get_incoming_rate, get_available_serial_nos, get_stock_value_on
+from erpnext.stock.utils import get_incoming_rate, get_stock_value_on, get_valuation_method
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
class TestStockReconciliation(unittest.TestCase):
@@ -29,16 +28,17 @@
self._test_reco_sle_gle("Moving Average")
def _test_reco_sle_gle(self, valuation_method):
- insert_existing_sle(warehouse='Stores - TCP1')
+ se1, se2, se3 = insert_existing_sle(warehouse='Stores - TCP1')
company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company')
# [[qty, valuation_rate, posting_date,
# posting_time, expected_stock_value, bin_qty, bin_valuation]]
+
input_data = [
- [50, 1000],
- [25, 900],
- ["", 1000],
- [20, ""],
- [0, ""]
+ [50, 1000, "2012-12-26", "12:00"],
+ [25, 900, "2012-12-26", "12:00"],
+ ["", 1000, "2012-12-20", "12:05"],
+ [20, "", "2012-12-26", "12:05"],
+ [0, "", "2012-12-31", "12:10"]
]
for d in input_data:
@@ -47,13 +47,13 @@
last_sle = get_previous_sle({
"item_code": "_Test Item",
"warehouse": "Stores - TCP1",
- "posting_date": nowdate(),
- "posting_time": nowtime()
+ "posting_date": d[2],
+ "posting_time": d[3]
})
# submit stock reconciliation
stock_reco = create_stock_reconciliation(qty=d[0], rate=d[1],
- posting_date=nowdate(), posting_time=nowtime(), warehouse="Stores - TCP1",
+ posting_date=d[2], posting_time=d[3], warehouse="Stores - TCP1",
company=company, expense_account = "Stock Adjustment - TCP1")
# check stock value
@@ -81,10 +81,15 @@
stock_reco.cancel()
+ se3.cancel()
+ se2.cancel()
+ se1.cancel()
+
def test_get_items(self):
- create_warehouse("_Test Warehouse Group 1", {"is_group": 1})
+ create_warehouse("_Test Warehouse Group 1",
+ {"is_group": 1, "company": "_Test Company", "parent_warehouse": "All Warehouses - _TC"})
create_warehouse("_Test Warehouse Ledger 1",
- {"is_group": 0, "parent_warehouse": "_Test Warehouse Group 1 - _TC"})
+ {"is_group": 0, "parent_warehouse": "_Test Warehouse Group 1 - _TC", "company": "_Test Company"})
create_item("_Test Stock Reco Item", is_stock_item=1, valuation_rate=100,
warehouse="_Test Warehouse Ledger 1 - _TC", opening_stock=100)
@@ -95,8 +100,6 @@
[items[0]["item_code"], items[0]["warehouse"], items[0]["qty"]])
def test_stock_reco_for_serialized_item(self):
- set_perpetual_inventory()
-
to_delete_records = []
to_delete_serial_nos = []
@@ -148,8 +151,6 @@
stock_doc.cancel()
def test_stock_reco_for_batch_item(self):
- set_perpetual_inventory()
-
to_delete_records = []
to_delete_serial_nos = []
@@ -196,15 +197,17 @@
def insert_existing_sle(warehouse):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
- make_stock_entry(posting_date=nowdate(), posting_time=nowtime(), item_code="_Test Item",
+ se1 = make_stock_entry(posting_date="2012-12-15", posting_time="02:00", item_code="_Test Item",
target=warehouse, qty=10, basic_rate=700)
- make_stock_entry(posting_date=nowdate(), posting_time=nowtime(), item_code="_Test Item",
+ se2 = make_stock_entry(posting_date="2012-12-25", posting_time="03:00", item_code="_Test Item",
source=warehouse, qty=15)
- make_stock_entry(posting_date=nowdate(), posting_time=nowtime(), item_code="_Test Item",
+ se3 = make_stock_entry(posting_date="2013-01-05", posting_time="07:00", item_code="_Test Item",
target=warehouse, qty=15, basic_rate=1200)
+ return se1, se2, se3
+
def create_batch_or_serial_no_items():
create_warehouse("_Test Warehouse for Stock Reco1",
{"is_group": 0, "parent_warehouse": "_Test Warehouse Group - _TC"})
@@ -256,6 +259,10 @@
return sr
def set_valuation_method(item_code, valuation_method):
+ existing_valuation_method = get_valuation_method(item_code)
+ if valuation_method == existing_valuation_method:
+ return
+
frappe.db.set_value("Item", item_code, "valuation_method", valuation_method)
for warehouse in frappe.get_all("Warehouse", filters={"company": "_Test Company"}, fields=["name", "is_group"]):
diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.json b/erpnext/stock/doctype/stock_settings/stock_settings.json
index a166657..859aea2 100644
--- a/erpnext/stock/doctype/stock_settings/stock_settings.json
+++ b/erpnext/stock/doctype/stock_settings/stock_settings.json
@@ -28,7 +28,9 @@
"inter_warehouse_transfer_settings_section",
"allow_from_dn",
"allow_from_pr",
- "freeze_stock_entries",
+ "control_historical_stock_transactions_section",
+ "role_allowed_to_create_edit_back_dated_transactions",
+ "column_break_26",
"stock_frozen_upto",
"stock_frozen_upto_days",
"stock_auth_role",
@@ -156,21 +158,20 @@
"label": "Notify by Email on Creation of Automatic Material Request"
},
{
- "fieldname": "freeze_stock_entries",
- "fieldtype": "Section Break",
- "label": "Freeze Stock Entries"
- },
- {
+ "description": "No stock transactions can be created or modified before this date.",
"fieldname": "stock_frozen_upto",
"fieldtype": "Date",
"label": "Stock Frozen Upto"
},
{
+ "description": "Stock transactions that are older than the mentioned days cannot be modified.",
"fieldname": "stock_frozen_upto_days",
"fieldtype": "Int",
"label": "Freeze Stocks Older Than (Days)"
},
{
+ "depends_on": "eval:(doc.stock_frozen_upto || doc.stock_frozen_upto_days)",
+ "description": "The users with this Role are allowed to create/modify a stock transaction, even though the transaction is frozen.",
"fieldname": "stock_auth_role",
"fieldtype": "Link",
"label": "Role Allowed to Edit Frozen Stock",
@@ -210,6 +211,22 @@
"fieldname": "allow_from_pr",
"fieldtype": "Check",
"label": "Allow Material Transfer from Purchase Receipt to Purchase Invoice"
+ },
+ {
+ "description": "If mentioned, the system will allow only the users with this Role to create or modify any stock transaction earlier than the latest stock transaction for a specific item and warehouse. If set as blank, it allows all users to create/edit back-dated transactions.",
+ "fieldname": "role_allowed_to_create_edit_back_dated_transactions",
+ "fieldtype": "Link",
+ "label": "Role Allowed to Create/Edit Back-dated Transactions",
+ "options": "User"
+ },
+ {
+ "fieldname": "column_break_26",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "control_historical_stock_transactions_section",
+ "fieldtype": "Section Break",
+ "label": "Control Historical Stock Transactions"
}
],
"icon": "icon-cog",
@@ -217,7 +234,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2020-11-23 15:26:54.225608",
+ "modified": "2020-11-23 22:26:54.225608",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Settings",
diff --git a/erpnext/stock/doctype/warehouse/test_warehouse.py b/erpnext/stock/doctype/warehouse/test_warehouse.py
index 3101e8a..95478f6 100644
--- a/erpnext/stock/doctype/warehouse/test_warehouse.py
+++ b/erpnext/stock/doctype/warehouse/test_warehouse.py
@@ -10,13 +10,10 @@
import erpnext
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
-from erpnext import set_perpetual_inventory
from erpnext.accounts.doctype.account.test_account import get_inventory_account, create_account
-
test_records = frappe.get_test_records('Warehouse')
-
class TestWarehouse(unittest.TestCase):
def setUp(self):
if not frappe.get_value('Item', '_Test Item'):
@@ -37,63 +34,63 @@
self.assertEqual(child_warehouse.is_group, 0)
def test_warehouse_renaming(self):
- set_perpetual_inventory(1)
- create_warehouse("Test Warehouse for Renaming 1")
- account = get_inventory_account("_Test Company", "Test Warehouse for Renaming 1 - _TC")
+ create_warehouse("Test Warehouse for Renaming 1", company="_Test Company with perpetual inventory")
+ account = get_inventory_account("_Test Company with perpetual inventory", "Test Warehouse for Renaming 1 - TCP1")
self.assertTrue(frappe.db.get_value("Warehouse", filters={"account": account}))
# Rename with abbr
- if frappe.db.exists("Warehouse", "Test Warehouse for Renaming 2 - _TC"):
- frappe.delete_doc("Warehouse", "Test Warehouse for Renaming 2 - _TC")
- frappe.rename_doc("Warehouse", "Test Warehouse for Renaming 1 - _TC", "Test Warehouse for Renaming 2 - _TC")
+ if frappe.db.exists("Warehouse", "Test Warehouse for Renaming 2 - TCP1"):
+ frappe.delete_doc("Warehouse", "Test Warehouse for Renaming 2 - TCP1")
+ frappe.rename_doc("Warehouse", "Test Warehouse for Renaming 1 - TCP1", "Test Warehouse for Renaming 2 - TCP1")
self.assertTrue(frappe.db.get_value("Warehouse",
- filters={"account": "Test Warehouse for Renaming 1 - _TC"}))
+ filters={"account": "Test Warehouse for Renaming 1 - TCP1"}))
# Rename without abbr
- if frappe.db.exists("Warehouse", "Test Warehouse for Renaming 3 - _TC"):
- frappe.delete_doc("Warehouse", "Test Warehouse for Renaming 3 - _TC")
+ if frappe.db.exists("Warehouse", "Test Warehouse for Renaming 3 - TCP1"):
+ frappe.delete_doc("Warehouse", "Test Warehouse for Renaming 3 - TCP1")
- frappe.rename_doc("Warehouse", "Test Warehouse for Renaming 2 - _TC", "Test Warehouse for Renaming 3")
+ frappe.rename_doc("Warehouse", "Test Warehouse for Renaming 2 - TCP1", "Test Warehouse for Renaming 3")
self.assertTrue(frappe.db.get_value("Warehouse",
- filters={"account": "Test Warehouse for Renaming 1 - _TC"}))
+ filters={"account": "Test Warehouse for Renaming 1 - TCP1"}))
# Another rename with multiple dashes
- if frappe.db.exists("Warehouse", "Test - Warehouse - Company - _TC"):
- frappe.delete_doc("Warehouse", "Test - Warehouse - Company - _TC")
- frappe.rename_doc("Warehouse", "Test Warehouse for Renaming 3 - _TC", "Test - Warehouse - Company")
+ if frappe.db.exists("Warehouse", "Test - Warehouse - Company - TCP1"):
+ frappe.delete_doc("Warehouse", "Test - Warehouse - Company - TCP1")
+ frappe.rename_doc("Warehouse", "Test Warehouse for Renaming 3 - TCP1", "Test - Warehouse - Company")
def test_warehouse_merging(self):
- set_perpetual_inventory(1)
+ company = "_Test Company with perpetual inventory"
+ create_warehouse("Test Warehouse for Merging 1", company=company,
+ properties={"parent_warehouse": "All Warehouses - TCP1"})
+ create_warehouse("Test Warehouse for Merging 2", company=company,
+ properties={"parent_warehouse": "All Warehouses - TCP1"})
- create_warehouse("Test Warehouse for Merging 1")
- create_warehouse("Test Warehouse for Merging 2")
-
- make_stock_entry(item_code="_Test Item", target="Test Warehouse for Merging 1 - _TC",
- qty=1, rate=100)
- make_stock_entry(item_code="_Test Item", target="Test Warehouse for Merging 2 - _TC",
- qty=1, rate=100)
+ make_stock_entry(item_code="_Test Item", target="Test Warehouse for Merging 1 - TCP1",
+ qty=1, rate=100, company=company)
+ make_stock_entry(item_code="_Test Item", target="Test Warehouse for Merging 2 - TCP1",
+ qty=1, rate=100, company=company)
existing_bin_qty = (
cint(frappe.db.get_value("Bin",
- {"item_code": "_Test Item", "warehouse": "Test Warehouse for Merging 1 - _TC"}, "actual_qty"))
+ {"item_code": "_Test Item", "warehouse": "Test Warehouse for Merging 1 - TCP1"}, "actual_qty"))
+ cint(frappe.db.get_value("Bin",
- {"item_code": "_Test Item", "warehouse": "Test Warehouse for Merging 2 - _TC"}, "actual_qty"))
+ {"item_code": "_Test Item", "warehouse": "Test Warehouse for Merging 2 - TCP1"}, "actual_qty"))
)
- frappe.rename_doc("Warehouse", "Test Warehouse for Merging 1 - _TC",
- "Test Warehouse for Merging 2 - _TC", merge=True)
+ frappe.rename_doc("Warehouse", "Test Warehouse for Merging 1 - TCP1",
+ "Test Warehouse for Merging 2 - TCP1", merge=True)
- self.assertFalse(frappe.db.exists("Warehouse", "Test Warehouse for Merging 1 - _TC"))
+ self.assertFalse(frappe.db.exists("Warehouse", "Test Warehouse for Merging 1 - TCP1"))
bin_qty = frappe.db.get_value("Bin",
- {"item_code": "_Test Item", "warehouse": "Test Warehouse for Merging 2 - _TC"}, "actual_qty")
+ {"item_code": "_Test Item", "warehouse": "Test Warehouse for Merging 2 - TCP1"}, "actual_qty")
self.assertEqual(bin_qty, existing_bin_qty)
self.assertTrue(frappe.db.get_value("Warehouse",
- filters={"account": "Test Warehouse for Merging 2 - _TC"}))
+ filters={"account": "Test Warehouse for Merging 2 - TCP1"}))
def create_warehouse(warehouse_name, properties=None, company=None):
if not company:
diff --git a/erpnext/stock/doctype/warehouse/warehouse.py b/erpnext/stock/doctype/warehouse/warehouse.py
index cd86be3..6c84f16 100644
--- a/erpnext/stock/doctype/warehouse/warehouse.py
+++ b/erpnext/stock/doctype/warehouse/warehouse.py
@@ -29,7 +29,6 @@
self.set_onload('account', account)
load_address_and_contact(self)
-
def on_update(self):
self.update_nsm_model()
diff --git a/erpnext/stock/report/stock_analytics/stock_analytics.py b/erpnext/stock/report/stock_analytics/stock_analytics.py
index 54eefdf..0cc8ca4 100644
--- a/erpnext/stock/report/stock_analytics/stock_analytics.py
+++ b/erpnext/stock/report/stock_analytics/stock_analytics.py
@@ -7,9 +7,11 @@
from frappe.utils import getdate, flt
from erpnext.stock.report.stock_balance.stock_balance import (get_items, get_stock_ledger_entries, get_item_details)
from erpnext.accounts.utils import get_fiscal_year
+from erpnext.stock.utils import is_reposting_item_valuation_in_progress
from six import iteritems
def execute(filters=None):
+ is_reposting_item_valuation_in_progress()
filters = frappe._dict(filters or {})
columns = get_columns(filters)
data = get_data(filters)
diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py
index ccd0100..e5d4d62 100644
--- a/erpnext/stock/report/stock_balance/stock_balance.py
+++ b/erpnext/stock/report/stock_balance/stock_balance.py
@@ -7,12 +7,13 @@
from frappe.utils import flt, cint, getdate, now, date_diff
from erpnext.stock.utils import add_additional_uom_columns
from erpnext.stock.report.stock_ledger.stock_ledger import get_item_group_condition
-
+from erpnext.stock.utils import is_reposting_item_valuation_in_progress
from erpnext.stock.report.stock_ageing.stock_ageing import get_fifo_queue, get_average_age
from six import iteritems
def execute(filters=None):
+ is_reposting_item_valuation_in_progress()
if not filters: filters = {}
validate_filters(filters)
diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.py b/erpnext/stock/report/stock_ledger/stock_ledger.py
index 86af5e0..7b5701a 100644
--- a/erpnext/stock/report/stock_ledger/stock_ledger.py
+++ b/erpnext/stock/report/stock_ledger/stock_ledger.py
@@ -5,11 +5,12 @@
import frappe
from frappe.utils import cint, flt
-from erpnext.stock.utils import update_included_uom_in_report
+from erpnext.stock.utils import update_included_uom_in_report, is_reposting_item_valuation_in_progress
from frappe import _
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
def execute(filters=None):
+ is_reposting_item_valuation_in_progress()
include_uom = filters.get("include_uom")
columns = get_columns()
items = get_items(filters)
diff --git a/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py b/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py
index c8efb16..1183e41 100644
--- a/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py
+++ b/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py
@@ -5,9 +5,10 @@
import frappe
from frappe import _
from frappe.utils import flt, today
-from erpnext.stock.utils import update_included_uom_in_report
+from erpnext.stock.utils import update_included_uom_in_report, is_reposting_item_valuation_in_progress
def execute(filters=None):
+ is_reposting_item_valuation_in_progress()
filters = frappe._dict(filters or {})
include_uom = filters.get("include_uom")
columns = get_columns()
diff --git a/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py b/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py
index ebcb106..04f7d34 100644
--- a/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py
+++ b/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py
@@ -11,9 +11,11 @@
from erpnext.stock.report.stock_balance.stock_balance import (get_item_details,
get_item_reorder_details, get_item_warehouse_map, get_items, get_stock_ledger_entries)
from erpnext.stock.report.stock_ageing.stock_ageing import get_fifo_queue, get_average_age
+from erpnext.stock.utils import is_reposting_item_valuation_in_progress
from six import iteritems
def execute(filters=None):
+ is_reposting_item_valuation_in_progress()
if not filters: filters = {}
validate_filters(filters)
diff --git a/erpnext/stock/stock_balance.py b/erpnext/stock/stock_balance.py
index b5ae1b7..8ba1f1c 100644
--- a/erpnext/stock/stock_balance.py
+++ b/erpnext/stock/stock_balance.py
@@ -6,6 +6,7 @@
from frappe.utils import flt, cstr, nowdate, nowtime
from erpnext.stock.utils import update_bin
from erpnext.stock.stock_ledger import update_entries_after
+from erpnext.controllers.stock_controller import create_repost_item_valuation_entry
def repost(only_actual=False, allow_negative_stock=False, allow_zero_rate=False, only_bin=False):
"""
@@ -56,12 +57,18 @@
update_bin_qty(item_code, warehouse, qty_dict)
def repost_actual_qty(item_code, warehouse, allow_zero_rate=False, allow_negative_stock=False):
- update_entries_after({ "item_code": item_code, "warehouse": warehouse },
- allow_zero_rate=allow_zero_rate, allow_negative_stock=allow_negative_stock)
+ create_repost_item_valuation_entry({
+ "item_code": item_code,
+ "warehouse": warehouse,
+ "posting_date": "1900-01-01",
+ "posting_time": "00:01",
+ "allow_negative_stock": allow_negative_stock,
+ "allow_zero_rate": allow_zero_rate
+ })
def get_balance_qty_from_sle(item_code, warehouse):
balance_qty = frappe.db.sql("""select qty_after_transaction from `tabStock Ledger Entry`
- where item_code=%s and warehouse=%s
+ where item_code=%s and warehouse=%s and is_cancelled=0
order by posting_date desc, posting_time desc, creation desc
limit 1""", (item_code, warehouse))
@@ -191,7 +198,7 @@
print(d[0], d[1], d[2], serial_nos[0][0])
sle = frappe.db.sql("""select valuation_rate, company from `tabStock Ledger Entry`
- where item_code = %s and warehouse = %s
+ where item_code = %s and warehouse = %s and is_cancelled = 0
order by posting_date desc limit 1""", (d[0], d[1]))
sle_dict = {
@@ -223,7 +230,8 @@
})
update_bin(args)
- update_entries_after({
+
+ create_repost_item_valuation_entry({
"item_code": d[0],
"warehouse": d[1],
"posting_date": posting_date,
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index f4490f1..5b9ada0 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -5,9 +5,10 @@
import frappe, erpnext
from frappe import _
from frappe.utils import cint, flt, cstr, now, now_datetime
+from frappe.model.meta import get_field_precision
from erpnext.stock.utils import get_valuation_method, get_incoming_outgoing_rate_for_cancel
+from erpnext.stock.utils import get_bin
import json
-
from six import iteritems
# future reposting
@@ -25,32 +26,23 @@
set_as_cancel(sl_entries[0].get('voucher_type'), sl_entries[0].get('voucher_no'))
for sle in sl_entries:
- sle_id = None
- if via_landed_cost_voucher or cancel:
- sle['posting_date'] = now_datetime().strftime('%Y-%m-%d')
- sle['posting_time'] = now_datetime().strftime('%H:%M:%S.%f')
+ if cancel:
+ sle['actual_qty'] = -flt(sle.get('actual_qty'))
- if cancel:
- sle['actual_qty'] = -flt(sle.get('actual_qty'))
+ if sle['actual_qty'] < 0 and not sle.get('outgoing_rate'):
+ sle['outgoing_rate'] = get_incoming_outgoing_rate_for_cancel(sle.item_code,
+ sle.voucher_type, sle.voucher_no, sle.voucher_detail_no)
+ sle['incoming_rate'] = 0.0
- if sle['actual_qty'] < 0 and not sle.get('outgoing_rate'):
- sle['outgoing_rate'] = get_incoming_outgoing_rate_for_cancel(sle.item_code,
- sle.voucher_type, sle.voucher_no, sle.voucher_detail_no)
- sle['incoming_rate'] = 0.0
-
- if sle['actual_qty'] > 0 and not sle.get('incoming_rate'):
- sle['incoming_rate'] = get_incoming_outgoing_rate_for_cancel(sle.item_code,
- sle.voucher_type, sle.voucher_no, sle.voucher_detail_no)
- sle['outgoing_rate'] = 0.0
-
+ if sle['actual_qty'] > 0 and not sle.get('incoming_rate'):
+ sle['incoming_rate'] = get_incoming_outgoing_rate_for_cancel(sle.item_code,
+ sle.voucher_type, sle.voucher_no, sle.voucher_detail_no)
+ sle['outgoing_rate'] = 0.0
if sle.get("actual_qty") or sle.get("voucher_type")=="Stock Reconciliation":
- sle_id = make_entry(sle, allow_negative_stock, via_landed_cost_voucher)
-
- args = sle.copy()
- args.update({
- "sle_id": sle_id
- })
+ sle_doc = make_entry(sle, allow_negative_stock, via_landed_cost_voucher)
+
+ args = sle_doc.as_dict()
update_bin(args, allow_negative_stock, via_landed_cost_voucher)
@@ -68,8 +60,36 @@
sle.via_landed_cost_voucher = via_landed_cost_voucher
sle.insert()
sle.submit()
- return sle.name
+ return sle
+def repost_future_sle(args=None, voucher_type=None, voucher_no=None, allow_negative_stock=False, via_landed_cost_voucher=False):
+ if not args and voucher_type and voucher_no:
+ args = get_args_for_voucher(voucher_type, voucher_no)
+
+ distinct_item_warehouses = [(d.item_code, d.warehouse) for d in args]
+
+ i = 0
+ while i < len(args):
+ obj = update_entries_after({
+ "item_code": args[i].item_code,
+ "warehouse": args[i].warehouse,
+ "posting_date": args[i].posting_date,
+ "posting_time": args[i].posting_time
+ }, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher)
+
+ for item_wh, new_sle in iteritems(obj.new_items):
+ if item_wh not in distinct_item_warehouses:
+ args.append(new_sle)
+
+ i += 1
+
+def get_args_for_voucher(voucher_type, voucher_no):
+ return frappe.db.get_all("Stock Ledger Entry",
+ filters={"voucher_type": voucher_type, "voucher_no": voucher_no},
+ fields=["item_code", "warehouse", "posting_date", "posting_time"],
+ order_by="creation asc",
+ group_by="item_code, warehouse"
+ )
class update_entries_after(object):
"""
@@ -86,141 +106,299 @@
}
"""
def __init__(self, args, allow_zero_rate=False, allow_negative_stock=None, via_landed_cost_voucher=False, verbose=1):
- from frappe.model.meta import get_field_precision
-
- self.exceptions = []
+ self.exceptions = {}
self.verbose = verbose
self.allow_zero_rate = allow_zero_rate
- self.allow_negative_stock = allow_negative_stock
self.via_landed_cost_voucher = via_landed_cost_voucher
- if not self.allow_negative_stock:
- self.allow_negative_stock = cint(frappe.db.get_single_value("Stock Settings",
- "allow_negative_stock"))
+ self.allow_negative_stock = allow_negative_stock \
+ or cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock"))
- self.args = args
- for key, value in iteritems(args):
- setattr(self, key, value)
+ self.args = frappe._dict(args)
+ self.item_code = args.get("item_code")
+ if self.args.sle_id:
+ self.args['name'] = self.args.sle_id
- self.previous_sle = self.get_sle_before_datetime()
- self.previous_sle = self.previous_sle[0] if self.previous_sle else frappe._dict()
+ self.company = frappe.get_cached_value("Warehouse", self.args.warehouse, "company")
+ self.get_precision()
+ self.valuation_method = get_valuation_method(self.item_code)
+ self.new_items = {}
+
+ self.data = frappe._dict()
+ self.initialize_previous_data(self.args)
+
+ self.build()
+
+ def get_precision(self):
+ company_base_currency = frappe.get_cached_value('Company', self.company, "default_currency")
+ self.precision = get_field_precision(frappe.get_meta("Stock Ledger Entry").get_field("stock_value"),
+ currency=company_base_currency)
+
+ def initialize_previous_data(self, args):
+ """
+ Get previous sl entries for current item for each related warehouse
+ and assigns into self.data dict
+
+ :Data Structure:
+
+ self.data = {
+ warehouse1: {
+ 'previus_sle': {},
+ 'qty_after_transaction': 10,
+ 'valuation_rate': 100,
+ 'stock_value': 1000,
+ 'prev_stock_value': 1000,
+ 'stock_queue': '[[10, 100]]',
+ 'stock_value_difference': 1000
+ }
+ }
+
+ """
+ self.data.setdefault(args.warehouse, frappe._dict())
+ warehouse_dict = self.data[args.warehouse]
+ previous_sle = self.get_sle_before_datetime(args)
+ warehouse_dict.previous_sle = previous_sle
for key in ("qty_after_transaction", "valuation_rate", "stock_value"):
- setattr(self, key, flt(self.previous_sle.get(key)))
+ setattr(warehouse_dict, key, flt(previous_sle.get(key)))
- self.company = frappe.db.get_value("Warehouse", self.warehouse, "company")
- self.precision = get_field_precision(frappe.get_meta("Stock Ledger Entry").get_field("stock_value"),
- currency=frappe.get_cached_value('Company', self.company, "default_currency"))
+ warehouse_dict.update({
+ "prev_stock_value": previous_sle.stock_value or 0.0,
+ "stock_queue": json.loads(previous_sle.stock_queue or "[]"),
+ "stock_value_difference": 0.0
+ })
- self.prev_stock_value = self.previous_sle.stock_value or 0.0
- self.stock_queue = json.loads(self.previous_sle.stock_queue or "[]")
- self.valuation_method = get_valuation_method(self.item_code)
- self.stock_value_difference = 0.0
- self.build(args.get('sle_id'))
-
- def build(self, sle_id):
- if sle_id:
- sle = get_sle_by_id(sle_id)
- self.process_sle(sle)
+ def build(self):
+ if self.args.get("sle_id"):
+ self.process_sle_against_current_voucher()
else:
- # includes current entry!
- entries_to_fix = self.get_sle_after_datetime()
- for sle in entries_to_fix:
+ entries_to_fix = self.get_future_entries_to_fix()
+
+ i = 0
+ while i < len(entries_to_fix):
+ sle = entries_to_fix[i]
+ i += 1
+
self.process_sle(sle)
+ if sle.dependant_sle_voucher_detail_no:
+ self.get_dependent_entries_to_fix(entries_to_fix, sle)
+
if self.exceptions:
self.raise_exceptions()
self.update_bin()
- def update_bin(self):
- # update bin
- bin_name = frappe.db.get_value("Bin", {
- "item_code": self.item_code,
- "warehouse": self.warehouse
- })
+ def process_sle_against_current_voucher(self):
+ sl_entries = self.get_sle_against_current_voucher()
+ for sle in sl_entries:
+ self.process_sle(sle)
- if not bin_name:
- bin_doc = frappe.get_doc({
- "doctype": "Bin",
- "item_code": self.item_code,
- "warehouse": self.warehouse
- })
- bin_doc.insert(ignore_permissions=True)
- else:
- bin_doc = frappe.get_doc("Bin", bin_name)
+ def get_sle_against_current_voucher(self):
+ return frappe.db.sql("""
+ select
+ *, timestamp(posting_date, posting_time) as "timestamp"
+ from
+ `tabStock Ledger Entry`
+ where
+ item_code = %(item_code)s
+ and warehouse = %(warehouse)s
+ and voucher_type = %(voucher_type)s
+ and voucher_no = %(voucher_no)s
+ order by
+ creation ASC
+ for update
+ """, self.args, as_dict=1)
- bin_doc.update({
- "valuation_rate": self.valuation_rate,
- "actual_qty": self.qty_after_transaction,
- "stock_value": self.stock_value
- })
- bin_doc.flags.via_stock_ledger_entry = True
+ def get_future_entries_to_fix(self):
+ # includes current entry!
+ args = self.data[self.args.warehouse].previous_sle \
+ or frappe._dict({"item_code": self.item_code, "warehouse": self.args.warehouse})
+
+ return list(self.get_sle_after_datetime(args))
- bin_doc.save(ignore_permissions=True)
+ def get_dependent_entries_to_fix(self, entries_to_fix, sle):
+ dependant_sle = get_sle_by_voucher_detail_no(sle.dependant_sle_voucher_detail_no,
+ excluded_sle=sle.name)
+
+ if not dependant_sle:
+ return
+ elif dependant_sle.item_code == self.item_code and dependant_sle.warehouse == self.args.warehouse:
+ return
+ elif dependant_sle.item_code != self.item_code \
+ and (dependant_sle.item_code, dependant_sle.warehouse) not in self.new_items:
+ self.new_items[(dependant_sle.item_code, dependant_sle.warehouse)] = dependant_sle
+ return
+
+ self.initialize_previous_data(dependant_sle)
+
+ args = self.data[dependant_sle.warehouse].previous_sle \
+ or frappe._dict({"item_code": self.item_code, "warehouse": dependant_sle.warehouse})
+ future_sle_for_dependant = list(self.get_sle_after_datetime(args))
+
+ entries_to_fix.extend(future_sle_for_dependant)
+ entries_to_fix = sorted(entries_to_fix, key=lambda k: k['timestamp'])
def process_sle(self, sle):
+ # previous sle data for this warehouse
+ self.wh_data = self.data[sle.warehouse]
+
if (sle.serial_no and not self.via_landed_cost_voucher) or not cint(self.allow_negative_stock):
# validate negative stock for serialized items, fifo valuation
# or when negative stock is not allowed for moving average
if not self.validate_negative_stock(sle):
- self.qty_after_transaction += flt(sle.actual_qty)
+ self.wh_data.qty_after_transaction += flt(sle.actual_qty)
return
+ # Get dynamic incoming/outgoing rate
+ self.get_dynamic_incoming_outgoing_rate(sle)
+
if sle.serial_no:
self.get_serialized_values(sle)
- self.qty_after_transaction += flt(sle.actual_qty)
+ self.wh_data.qty_after_transaction += flt(sle.actual_qty)
if sle.voucher_type == "Stock Reconciliation":
- self.qty_after_transaction = sle.qty_after_transaction
+ self.wh_data.qty_after_transaction = sle.qty_after_transaction
- self.stock_value = flt(self.qty_after_transaction) * flt(self.valuation_rate)
+ self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(self.wh_data.valuation_rate)
else:
if sle.voucher_type=="Stock Reconciliation" and not sle.batch_no:
# assert
- self.valuation_rate = sle.valuation_rate
- self.qty_after_transaction = sle.qty_after_transaction
- self.stock_queue = [[self.qty_after_transaction, self.valuation_rate]]
- self.stock_value = flt(self.qty_after_transaction) * flt(self.valuation_rate)
+ self.wh_data.valuation_rate = sle.valuation_rate
+ self.wh_data.qty_after_transaction = sle.qty_after_transaction
+ self.wh_data.stock_queue = [[self.wh_data.qty_after_transaction, self.wh_data.valuation_rate]]
+ self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(self.wh_data.valuation_rate)
else:
if self.valuation_method == "Moving Average":
self.get_moving_average_values(sle)
- self.qty_after_transaction += flt(sle.actual_qty)
- self.stock_value = flt(self.qty_after_transaction) * flt(self.valuation_rate)
+ self.wh_data.qty_after_transaction += flt(sle.actual_qty)
+ self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(self.wh_data.valuation_rate)
else:
self.get_fifo_values(sle)
- self.qty_after_transaction += flt(sle.actual_qty)
- self.stock_value = sum((flt(batch[0]) * flt(batch[1]) for batch in self.stock_queue))
+ self.wh_data.qty_after_transaction += flt(sle.actual_qty)
+ self.wh_data.stock_value = sum((flt(batch[0]) * flt(batch[1]) for batch in self.wh_data.stock_queue))
# rounding as per precision
- self.stock_value = flt(self.stock_value, self.precision)
-
- stock_value_difference = self.stock_value - self.prev_stock_value
-
- self.prev_stock_value = self.stock_value
+ self.wh_data.stock_value = flt(self.wh_data.stock_value, self.precision)
+ stock_value_difference = self.wh_data.stock_value - self.wh_data.prev_stock_value
+ self.wh_data.prev_stock_value = self.wh_data.stock_value
# update current sle
- sle.qty_after_transaction = self.qty_after_transaction
- sle.valuation_rate = self.valuation_rate
- sle.stock_value = self.stock_value
- sle.stock_queue = json.dumps(self.stock_queue)
+ sle.qty_after_transaction = self.wh_data.qty_after_transaction
+ sle.valuation_rate = self.wh_data.valuation_rate
+ sle.stock_value = self.wh_data.stock_value
+ sle.stock_queue = json.dumps(self.wh_data.stock_queue)
sle.stock_value_difference = stock_value_difference
sle.doctype="Stock Ledger Entry"
frappe.get_doc(sle).db_update()
+ self.update_outgoing_rate_on_transaction(sle)
+
def validate_negative_stock(self, sle):
"""
validate negative stock for entries current datetime onwards
will not consider cancelled entries
"""
- diff = self.qty_after_transaction + flt(sle.actual_qty)
+ diff = self.wh_data.qty_after_transaction + flt(sle.actual_qty)
if diff < 0 and abs(diff) > 0.0001:
# negative stock!
exc = sle.copy().update({"diff": diff})
- self.exceptions.append(exc)
+ self.exceptions.setdefault(sle.warehouse, []).append(exc)
return False
else:
return True
+ def get_dynamic_incoming_outgoing_rate(self, sle):
+ # Get updated incoming/outgoing rate from transaction
+ if sle.recalculate_rate:
+ rate = self.get_incoming_outgoing_rate_from_transaction(sle)
+
+ if flt(sle.actual_qty) >= 0:
+ sle.incoming_rate = rate
+ else:
+ sle.outgoing_rate = rate
+
+ def get_incoming_outgoing_rate_from_transaction(self, sle):
+ rate = 0
+ # Material Transfer, Repack, Manufacturing
+ if sle.voucher_type == "Stock Entry":
+ rate = frappe.db.get_value("Stock Entry Detail", sle.voucher_detail_no, "valuation_rate")
+ # Sales and Purchase Return
+ elif sle.voucher_type in ("Purchase Receipt", "Purchase Invoice", "Delivery Note", "Sales Invoice"):
+ if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_return"):
+ from erpnext.controllers.sales_and_purchase_return import get_rate_for_return # don't move this import to top
+ rate = get_rate_for_return(sle.voucher_type, sle.voucher_no, sle.item_code, voucher_detail_no=sle.voucher_detail_no)
+ else:
+ if sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
+ rate_field = "valuation_rate"
+ else:
+ rate_field = "incoming_rate"
+
+ # check in item table
+ item_code, incoming_rate = frappe.db.get_value(sle.voucher_type + " Item",
+ sle.voucher_detail_no, ["item_code", rate_field])
+
+ if item_code == sle.item_code:
+ rate = incoming_rate
+ else:
+ if sle.voucher_type in ("Delivery Note", "Sales Invoice"):
+ ref_doctype = "Packed Item"
+ else:
+ ref_doctype = "Purchase Receipt Item Supplied"
+
+ rate = frappe.db.get_value(ref_doctype, {"parent_detail_docname": sle.voucher_detail_no,
+ "item_code": sle.item_code}, rate_field)
+
+ return rate
+
+ def update_outgoing_rate_on_transaction(self, sle):
+ """
+ Update outgoing rate in Stock Entry, Delivery Note, Sales Invoice and Sales Return
+ In case of Stock Entry, also calculate FG Item rate and total incoming/outgoing amount
+ """
+ if sle.actual_qty and sle.voucher_detail_no:
+ outgoing_rate = abs(flt(sle.stock_value_difference)) / abs(sle.actual_qty)
+
+ if flt(sle.actual_qty) < 0 and sle.voucher_type == "Stock Entry":
+ self.update_rate_on_stock_entry(sle, outgoing_rate)
+ elif sle.voucher_type in ("Delivery Note", "Sales Invoice"):
+ self.update_rate_on_delivery_and_sales_return(sle, outgoing_rate)
+ elif flt(sle.actual_qty) < 0 and sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
+ self.update_rate_on_purchase_receipt(sle, outgoing_rate)
+
+ def update_rate_on_stock_entry(self, sle, outgoing_rate):
+ frappe.db.set_value("Stock Entry Detail", sle.voucher_detail_no, "basic_rate", outgoing_rate)
+
+ # Update outgoing item's rate, recalculate FG Item's rate and total incoming/outgoing amount
+ stock_entry = frappe.get_doc("Stock Entry", sle.voucher_no)
+ stock_entry.calculate_rate_and_amount(reset_outgoing_rate=False, raise_error_if_no_rate=False)
+ stock_entry.db_update()
+ for d in stock_entry.items:
+ d.db_update()
+
+ def update_rate_on_delivery_and_sales_return(self, sle, outgoing_rate):
+ # Update item's incoming rate on transaction
+ item_code = frappe.db.get_value(sle.voucher_type + " Item", sle.voucher_detail_no, "item_code")
+ if item_code == sle.item_code:
+ frappe.db.set_value(sle.voucher_type + " Item", sle.voucher_detail_no, "incoming_rate", outgoing_rate)
+ else:
+ # packed item
+ frappe.db.set_value("Packed Item",
+ {"parent_detail_docname": sle.voucher_detail_no, "item_code": sle.item_code},
+ "incoming_rate", outgoing_rate)
+
+ def update_rate_on_purchase_receipt(self, sle, outgoing_rate):
+ if frappe.db.exists(sle.voucher_type + " Item", sle.voucher_detail_no):
+ frappe.db.set_value(sle.voucher_type + " Item", sle.voucher_detail_no, "base_net_rate", outgoing_rate)
+ else:
+ frappe.db.set_value("Purchase Receipt Item Supplied", sle.voucher_detail_no, "rate", outgoing_rate)
+
+ # Recalculate subcontracted item's rate in case of subcontracted purchase receipt/invoice
+ if frappe.db.get_value(sle.voucher_type, sle.voucher_no, "is_subcontracted"):
+ doc = frappe.get_cached_doc(sle.voucher_type, sle.voucher_no)
+ doc.update_valuation_rate(reset_outgoing_rate=False)
+ for d in (doc.items + doc.supplied_items):
+ d.db_update()
+
def get_serialized_values(self, sle):
incoming_rate = flt(sle.incoming_rate)
actual_qty = flt(sle.actual_qty)
@@ -228,7 +406,7 @@
if incoming_rate < 0:
# wrong incoming rate
- incoming_rate = self.valuation_rate
+ incoming_rate = self.wh_data.valuation_rate
stock_value_change = 0
if incoming_rate:
@@ -236,22 +414,25 @@
elif actual_qty < 0:
# In case of delivery/stock issue, get average purchase rate
# of serial nos of current entry
- outgoing_value = self.get_incoming_value_for_serial_nos(sle, serial_nos)
- stock_value_change = -1 * outgoing_value
+ if not sle.is_cancelled:
+ outgoing_value = self.get_incoming_value_for_serial_nos(sle, serial_nos)
+ stock_value_change = -1 * outgoing_value
+ else:
+ stock_value_change = actual_qty * sle.outgoing_rate
- new_stock_qty = self.qty_after_transaction + actual_qty
+ new_stock_qty = self.wh_data.qty_after_transaction + actual_qty
if new_stock_qty > 0:
- new_stock_value = (self.qty_after_transaction * self.valuation_rate) + stock_value_change
+ new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + stock_value_change
if new_stock_value >= 0:
# calculate new valuation rate only if stock value is positive
# else it remains the same as that of previous entry
- self.valuation_rate = new_stock_value / new_stock_qty
+ self.wh_data.valuation_rate = new_stock_value / new_stock_qty
- if not self.valuation_rate and sle.voucher_detail_no:
+ if not self.wh_data.valuation_rate and sle.voucher_detail_no:
allow_zero_rate = self.check_if_allow_zero_valuation_rate(sle.voucher_type, sle.voucher_detail_no)
if not allow_zero_rate:
- self.valuation_rate = get_valuation_rate(sle.item_code, sle.warehouse,
+ self.wh_data.valuation_rate = get_valuation_rate(sle.item_code, sle.warehouse,
sle.voucher_type, sle.voucher_no, self.allow_zero_rate,
currency=erpnext.get_company_currency(sle.company))
@@ -287,39 +468,39 @@
def get_moving_average_values(self, sle):
actual_qty = flt(sle.actual_qty)
- new_stock_qty = flt(self.qty_after_transaction) + actual_qty
+ new_stock_qty = flt(self.wh_data.qty_after_transaction) + actual_qty
if new_stock_qty >= 0:
if actual_qty > 0:
- if flt(self.qty_after_transaction) <= 0:
- self.valuation_rate = sle.incoming_rate
+ if flt(self.wh_data.qty_after_transaction) <= 0:
+ self.wh_data.valuation_rate = sle.incoming_rate
else:
- new_stock_value = (self.qty_after_transaction * self.valuation_rate) + \
+ new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + \
(actual_qty * sle.incoming_rate)
- self.valuation_rate = new_stock_value / new_stock_qty
+ self.wh_data.valuation_rate = new_stock_value / new_stock_qty
elif sle.outgoing_rate:
if new_stock_qty:
- new_stock_value = (self.qty_after_transaction * self.valuation_rate) + \
+ new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + \
(actual_qty * sle.outgoing_rate)
- self.valuation_rate = new_stock_value / new_stock_qty
+ self.wh_data.valuation_rate = new_stock_value / new_stock_qty
else:
- self.valuation_rate = sle.outgoing_rate
+ self.wh_data.valuation_rate = sle.outgoing_rate
else:
- if flt(self.qty_after_transaction) >= 0 and sle.outgoing_rate:
- self.valuation_rate = sle.outgoing_rate
+ if flt(self.wh_data.qty_after_transaction) >= 0 and sle.outgoing_rate:
+ self.wh_data.valuation_rate = sle.outgoing_rate
- if not self.valuation_rate and actual_qty > 0:
- self.valuation_rate = sle.incoming_rate
+ if not self.wh_data.valuation_rate and actual_qty > 0:
+ self.wh_data.valuation_rate = sle.incoming_rate
# Get valuation rate from previous SLE or Item master, if item does not have the
# allow zero valuration rate flag set
- if not self.valuation_rate and sle.voucher_detail_no:
+ if not self.wh_data.valuation_rate and sle.voucher_detail_no:
allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(sle.voucher_type, sle.voucher_detail_no)
if not allow_zero_valuation_rate:
- self.valuation_rate = get_valuation_rate(sle.item_code, sle.warehouse,
+ self.wh_data.valuation_rate = get_valuation_rate(sle.item_code, sle.warehouse,
sle.voucher_type, sle.voucher_no, self.allow_zero_rate,
currency=erpnext.get_company_currency(sle.company))
@@ -329,22 +510,22 @@
outgoing_rate = flt(sle.outgoing_rate)
if actual_qty > 0:
- if not self.stock_queue:
- self.stock_queue.append([0, 0])
+ if not self.wh_data.stock_queue:
+ self.wh_data.stock_queue.append([0, 0])
# last row has the same rate, just updated the qty
- if self.stock_queue[-1][1]==incoming_rate:
- self.stock_queue[-1][0] += actual_qty
+ if self.wh_data.stock_queue[-1][1]==incoming_rate:
+ self.wh_data.stock_queue[-1][0] += actual_qty
else:
- if self.stock_queue[-1][0] > 0:
- self.stock_queue.append([actual_qty, incoming_rate])
+ if self.wh_data.stock_queue[-1][0] > 0:
+ self.wh_data.stock_queue.append([actual_qty, incoming_rate])
else:
- qty = self.stock_queue[-1][0] + actual_qty
- self.stock_queue[-1] = [qty, incoming_rate]
+ qty = self.wh_data.stock_queue[-1][0] + actual_qty
+ self.wh_data.stock_queue[-1] = [qty, incoming_rate]
else:
qty_to_pop = abs(actual_qty)
while qty_to_pop:
- if not self.stock_queue:
+ if not self.wh_data.stock_queue:
# Get valuation rate from last sle if exists or from valuation rate field in item master
allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(sle.voucher_type, sle.voucher_detail_no)
if not allow_zero_valuation_rate:
@@ -354,35 +535,35 @@
else:
_rate = 0
- self.stock_queue.append([0, _rate])
+ self.wh_data.stock_queue.append([0, _rate])
index = None
if outgoing_rate > 0:
# Find the entry where rate matched with outgoing rate
- for i, v in enumerate(self.stock_queue):
+ for i, v in enumerate(self.wh_data.stock_queue):
if v[1] == outgoing_rate:
index = i
break
# If no entry found with outgoing rate, collapse stack
if index == None:
- new_stock_value = sum((d[0]*d[1] for d in self.stock_queue)) - qty_to_pop*outgoing_rate
- new_stock_qty = sum((d[0] for d in self.stock_queue)) - qty_to_pop
- self.stock_queue = [[new_stock_qty, new_stock_value/new_stock_qty if new_stock_qty > 0 else outgoing_rate]]
+ new_stock_value = sum((d[0]*d[1] for d in self.wh_data.stock_queue)) - qty_to_pop*outgoing_rate
+ new_stock_qty = sum((d[0] for d in self.wh_data.stock_queue)) - qty_to_pop
+ self.wh_data.stock_queue = [[new_stock_qty, new_stock_value/new_stock_qty if new_stock_qty > 0 else outgoing_rate]]
break
else:
index = 0
# select first batch or the batch with same rate
- batch = self.stock_queue[index]
+ batch = self.wh_data.stock_queue[index]
if qty_to_pop >= batch[0]:
# consume current batch
qty_to_pop = qty_to_pop - batch[0]
- self.stock_queue.pop(index)
- if not self.stock_queue and qty_to_pop:
+ self.wh_data.stock_queue.pop(index)
+ if not self.wh_data.stock_queue and qty_to_pop:
# stock finished, qty still remains to be withdrawn
# negative stock, keep in as a negative batch
- self.stock_queue.append([-qty_to_pop, outgoing_rate or batch[1]])
+ self.wh_data.stock_queue.append([-qty_to_pop, outgoing_rate or batch[1]])
break
else:
@@ -391,14 +572,14 @@
batch[0] = batch[0] - qty_to_pop
qty_to_pop = 0
- stock_value = sum((flt(batch[0]) * flt(batch[1]) for batch in self.stock_queue))
- stock_qty = sum((flt(batch[0]) for batch in self.stock_queue))
+ stock_value = sum((flt(batch[0]) * flt(batch[1]) for batch in self.wh_data.stock_queue))
+ stock_qty = sum((flt(batch[0]) for batch in self.wh_data.stock_queue))
if stock_qty:
- self.valuation_rate = stock_value / flt(stock_qty)
+ self.wh_data.valuation_rate = stock_value / flt(stock_qty)
- if not self.stock_queue:
- self.stock_queue.append([0, sle.incoming_rate or sle.outgoing_rate or self.valuation_rate])
+ if not self.wh_data.stock_queue:
+ self.wh_data.stock_queue.append([0, sle.incoming_rate or sle.outgoing_rate or self.wh_data.valuation_rate])
def check_if_allow_zero_valuation_rate(self, voucher_type, voucher_detail_no):
ref_item_dt = ""
@@ -413,39 +594,56 @@
else:
return 0
- def get_sle_before_datetime(self):
+ def get_sle_before_datetime(self, args):
"""get previous stock ledger entry before current time-bucket"""
- if self.args.get('sle_id'):
- self.args['name'] = self.args.get('sle_id')
+ sle = get_stock_ledger_entries(args, "<", "desc", "limit 1", for_update=False)
+ sle = sle[0] if sle else frappe._dict()
+ return sle
- return get_stock_ledger_entries(self.args, "<=", "desc", "limit 1", for_update=False)
-
- def get_sle_after_datetime(self):
+ def get_sle_after_datetime(self, args):
"""get Stock Ledger Entries after a particular datetime, for reposting"""
- return get_stock_ledger_entries(self.previous_sle or frappe._dict({
- "item_code": self.args.get("item_code"), "warehouse": self.args.get("warehouse") }),
- ">", "asc", for_update=True, check_serial_no=False)
+ return get_stock_ledger_entries(args, ">", "asc", for_update=True, check_serial_no=False)
def raise_exceptions(self):
- deficiency = min(e["diff"] for e in self.exceptions)
+ msg_list = []
+ for warehouse, exceptions in iteritems(self.exceptions):
+ deficiency = min(e["diff"] for e in exceptions)
- if ((self.exceptions[0]["voucher_type"], self.exceptions[0]["voucher_no"]) in
- frappe.local.flags.currently_saving):
+ if ((exceptions[0]["voucher_type"], exceptions[0]["voucher_no"]) in
+ frappe.local.flags.currently_saving):
- msg = _("{0} units of {1} needed in {2} to complete this transaction.").format(
- abs(deficiency), frappe.get_desk_link('Item', self.item_code),
- frappe.get_desk_link('Warehouse', self.warehouse))
- else:
- msg = _("{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction.").format(
- abs(deficiency), frappe.get_desk_link('Item', self.item_code),
- frappe.get_desk_link('Warehouse', self.warehouse),
- self.exceptions[0]["posting_date"], self.exceptions[0]["posting_time"],
- frappe.get_desk_link(self.exceptions[0]["voucher_type"], self.exceptions[0]["voucher_no"]))
+ msg = _("{0} units of {1} needed in {2} to complete this transaction.").format(
+ abs(deficiency), frappe.get_desk_link('Item', self.item_code),
+ frappe.get_desk_link('Warehouse', warehouse))
+ else:
+ msg = _("{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction.").format(
+ abs(deficiency), frappe.get_desk_link('Item', self.item_code),
+ frappe.get_desk_link('Warehouse', warehouse),
+ exceptions[0]["posting_date"], exceptions[0]["posting_time"],
+ frappe.get_desk_link(exceptions[0]["voucher_type"], exceptions[0]["voucher_no"]))
- if self.verbose:
- frappe.throw(msg, NegativeStockError, title='Insufficient Stock')
- else:
- raise NegativeStockError(msg)
+ if msg:
+ msg_list.append(msg)
+
+ if msg_list:
+ message = "\n\n".join(msg_list)
+ if self.verbose:
+ frappe.throw(message, NegativeStockError, title='Insufficient Stock')
+ else:
+ raise NegativeStockError(message)
+
+ def update_bin(self):
+ # update bin for each warehouse
+ for warehouse, data in iteritems(self.data):
+ bin_doc = get_bin(self.item_code, warehouse)
+
+ bin_doc.update({
+ "valuation_rate": data.valuation_rate,
+ "actual_qty": data.qty_after_transaction,
+ "stock_value": data.stock_value
+ })
+ bin_doc.flags.via_stock_ledger_entry = True
+ bin_doc.save(ignore_permissions=True)
def get_previous_sle(args, for_update=False):
"""
@@ -489,6 +687,7 @@
select *, timestamp(posting_date, posting_time) as "timestamp"
from `tabStock Ledger Entry`
where item_code = %%(item_code)s
+ and is_cancelled = 0
%(conditions)s
order by timestamp(posting_date, posting_time) %(order)s, creation %(order)s
%(limit)s %(for_update)s""" % {
@@ -498,10 +697,11 @@
"order": order
}, previous_sle, as_dict=1, debug=debug)
-def get_sle_by_id(sle_id):
- return frappe.db.get_all('Stock Ledger Entry',
- fields=['*', 'timestamp(posting_date, posting_time) as timestamp'],
- filters={'name': sle_id})[0]
+def get_sle_by_voucher_detail_no(voucher_detail_no, excluded_sle=None):
+ return frappe.db.get_value('Stock Ledger Entry',
+ {'voucher_detail_no': voucher_detail_no, 'name': ['!=', excluded_sle]},
+ ['item_code', 'warehouse', 'posting_date', 'posting_time', 'timestamp(posting_date, posting_time) as timestamp'],
+ as_dict=1)
def get_valuation_rate(item_code, warehouse, voucher_type, voucher_no,
allow_zero_rate=False, currency=None, company=None, raise_error_if_no_rate=True):
@@ -529,7 +729,7 @@
order by posting_date desc, posting_time desc, name desc limit 1""", (item_code, voucher_no, voucher_type))
if last_valuation_rate:
- return flt(last_valuation_rate[0][0]) # as there is previous records, it might come with zero rate
+ return flt(last_valuation_rate[0][0])
# If negative stock allowed, and item delivered without any incoming entry,
# system does not found any SLE, then take valuation rate from Item
@@ -561,3 +761,54 @@
frappe.throw(msg=msg, title=_("Valuation Rate Missing"))
return valuation_rate
+
+def update_qty_in_future_sle(args, allow_negative_stock=None):
+ frappe.db.sql("""
+ update `tabStock Ledger Entry`
+ set qty_after_transaction = qty_after_transaction + {qty}
+ where
+ item_code = %(item_code)s
+ and warehouse = %(warehouse)s
+ and voucher_no != %(voucher_no)s
+ and is_cancelled = 0
+ and (timestamp(posting_date, posting_time) > timestamp(%(posting_date)s, %(posting_time)s)
+ or (
+ timestamp(posting_date, posting_time) = timestamp(%(posting_date)s, %(posting_time)s)
+ and creation > %(creation)s
+ )
+ )
+ """.format(qty=args.actual_qty), args)
+
+ validate_negative_qty_in_future_sle(args, allow_negative_stock)
+
+def validate_negative_qty_in_future_sle(args, allow_negative_stock=None):
+ allow_negative_stock = allow_negative_stock \
+ or cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock"))
+
+ if args.actual_qty < 0 and not allow_negative_stock:
+ sle = get_future_sle_with_negative_qty(args)
+ if sle:
+ message = _("{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction.").format(
+ abs(sle[0]["qty_after_transaction"]),
+ frappe.get_desk_link('Item', args.item_code),
+ frappe.get_desk_link('Warehouse', args.warehouse),
+ sle[0]["posting_date"], sle[0]["posting_time"],
+ frappe.get_desk_link(sle[0]["voucher_type"], sle[0]["voucher_no"]))
+
+ frappe.throw(message, NegativeStockError, title='Insufficient Stock')
+
+def get_future_sle_with_negative_qty(args):
+ return frappe.db.sql("""
+ select
+ qty_after_transaction, posting_date, posting_time,
+ voucher_type, voucher_no
+ from `tabStock Ledger Entry`
+ where
+ item_code = %(item_code)s
+ and warehouse = %(warehouse)s
+ and voucher_no != %(voucher_no)s
+ and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
+ and is_cancelled = 0
+ and qty_after_transaction < 0
+ limit 1
+ """, args, as_dict=1)
\ No newline at end of file
diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py
index f9ac254..4ea7e4f 100644
--- a/erpnext/stock/utils.py
+++ b/erpnext/stock/utils.py
@@ -63,6 +63,7 @@
SELECT item_code, stock_value, name, warehouse
FROM `tabStock Ledger Entry` sle
WHERE posting_date <= %s {0}
+ and is_cancelled = 0
ORDER BY timestamp(posting_date, posting_time) DESC, creation DESC
""".format(condition), values, as_dict=1)
@@ -211,7 +212,7 @@
currency=erpnext.get_company_currency(args.get('company')), company=args.get('company'),
raise_error_if_no_rate=raise_error_if_no_rate)
- return in_rate
+ return flt(in_rate)
def get_avg_purchase_rate(serial_nos):
"""get average value of serial numbers"""
@@ -375,4 +376,10 @@
outgoing_rate = outgoing_rate[0][0] if outgoing_rate else 0.0
- return outgoing_rate
\ No newline at end of file
+ return outgoing_rate
+
+def is_reposting_item_valuation_in_progress():
+ reposting_in_progress = frappe.db.exists("Repost Item Valuation",
+ {'docstatus': 1, 'status': ['in', ['Queued','In Progress']]})
+ if reposting_in_progress:
+ frappe.msgprint(_("Item valuation reposting in progress. Report might show incorrect item valuation."), alert=1)
\ No newline at end of file