mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-26 00:14:50 +00:00
Merge pull request #42354 from frappe/version-15-hotfix
chore: release v15
This commit is contained in:
@@ -204,6 +204,7 @@ class JournalEntry(AccountsController):
|
|||||||
if self.needs_repost:
|
if self.needs_repost:
|
||||||
self.validate_for_repost()
|
self.validate_for_repost()
|
||||||
self.db_set("repost_required", self.needs_repost)
|
self.db_set("repost_required", self.needs_repost)
|
||||||
|
self.repost_accounting_entries()
|
||||||
|
|
||||||
def on_cancel(self):
|
def on_cancel(self):
|
||||||
# References for this Journal are removed on the `on_cancel` event in accounts_controller
|
# References for this Journal are removed on the `on_cancel` event in accounts_controller
|
||||||
|
|||||||
@@ -454,12 +454,9 @@ class TestJournalEntry(unittest.TestCase):
|
|||||||
# Change cost center for bank account - _Test Cost Center for BS Account
|
# Change cost center for bank account - _Test Cost Center for BS Account
|
||||||
create_cost_center(cost_center_name="_Test Cost Center for BS Account", company="_Test Company")
|
create_cost_center(cost_center_name="_Test Cost Center for BS Account", company="_Test Company")
|
||||||
jv.accounts[1].cost_center = "_Test Cost Center for BS Account - _TC"
|
jv.accounts[1].cost_center = "_Test Cost Center for BS Account - _TC"
|
||||||
|
# Ledger reposted implicitly upon 'Update After Submit'
|
||||||
jv.save()
|
jv.save()
|
||||||
|
|
||||||
# Check if repost flag gets set on update after submit
|
|
||||||
self.assertTrue(jv.repost_required)
|
|
||||||
jv.repost_accounting_entries()
|
|
||||||
|
|
||||||
# Check GL entries after reposting
|
# Check GL entries after reposting
|
||||||
jv.load_from_db()
|
jv.load_from_db()
|
||||||
self.expected_gle[0]["cost_center"] = "_Test Cost Center for BS Account - _TC"
|
self.expected_gle[0]["cost_center"] = "_Test Cost Center for BS Account - _TC"
|
||||||
|
|||||||
@@ -808,6 +808,7 @@ class PurchaseInvoice(BuyingController):
|
|||||||
if self.needs_repost:
|
if self.needs_repost:
|
||||||
self.validate_for_repost()
|
self.validate_for_repost()
|
||||||
self.db_set("repost_required", self.needs_repost)
|
self.db_set("repost_required", self.needs_repost)
|
||||||
|
self.repost_accounting_entries()
|
||||||
|
|
||||||
def make_gl_entries(self, gl_entries=None, from_repost=False):
|
def make_gl_entries(self, gl_entries=None, from_repost=False):
|
||||||
update_outstanding = "No" if (cint(self.is_paid) or self.write_off_account) else "Yes"
|
update_outstanding = "No" if (cint(self.is_paid) or self.write_off_account) else "Yes"
|
||||||
|
|||||||
@@ -2005,10 +2005,9 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
|
|||||||
check_gl_entries(self, pi.name, expected_gle, nowdate())
|
check_gl_entries(self, pi.name, expected_gle, nowdate())
|
||||||
|
|
||||||
pi.items[0].expense_account = "Service - _TC"
|
pi.items[0].expense_account = "Service - _TC"
|
||||||
|
# Ledger reposted implicitly upon 'Update After Submit'
|
||||||
pi.save()
|
pi.save()
|
||||||
pi.load_from_db()
|
pi.load_from_db()
|
||||||
self.assertTrue(pi.repost_required)
|
|
||||||
pi.repost_accounting_entries()
|
|
||||||
|
|
||||||
expected_gle = [
|
expected_gle = [
|
||||||
["Creditors - _TC", 0.0, 1000, nowdate()],
|
["Creditors - _TC", 0.0, 1000, nowdate()],
|
||||||
|
|||||||
@@ -724,6 +724,7 @@ class SalesInvoice(SellingController):
|
|||||||
if self.needs_repost:
|
if self.needs_repost:
|
||||||
self.validate_for_repost()
|
self.validate_for_repost()
|
||||||
self.db_set("repost_required", self.needs_repost)
|
self.db_set("repost_required", self.needs_repost)
|
||||||
|
self.repost_accounting_entries()
|
||||||
|
|
||||||
def set_paid_amount(self):
|
def set_paid_amount(self):
|
||||||
paid_amount = 0.0
|
paid_amount = 0.0
|
||||||
|
|||||||
@@ -2940,13 +2940,9 @@ class TestSalesInvoice(FrappeTestCase):
|
|||||||
si.items[0].income_account = "Service - _TC"
|
si.items[0].income_account = "Service - _TC"
|
||||||
si.additional_discount_account = "_Test Account Sales - _TC"
|
si.additional_discount_account = "_Test Account Sales - _TC"
|
||||||
si.taxes[0].account_head = "TDS Payable - _TC"
|
si.taxes[0].account_head = "TDS Payable - _TC"
|
||||||
|
# Ledger reposted implicitly upon 'Update After Submit'
|
||||||
si.save()
|
si.save()
|
||||||
|
|
||||||
si.load_from_db()
|
|
||||||
self.assertTrue(si.repost_required)
|
|
||||||
|
|
||||||
si.repost_accounting_entries()
|
|
||||||
|
|
||||||
expected_gle = [
|
expected_gle = [
|
||||||
["_Test Account Sales - _TC", 22.0, 0.0, nowdate()],
|
["_Test Account Sales - _TC", 22.0, 0.0, nowdate()],
|
||||||
["Debtors - _TC", 88, 0.0, nowdate()],
|
["Debtors - _TC", 88, 0.0, nowdate()],
|
||||||
|
|||||||
@@ -123,19 +123,15 @@ def get_provisional_profit_loss(
|
|||||||
for period in period_list:
|
for period in period_list:
|
||||||
key = period if consolidated else period.key
|
key = period if consolidated else period.key
|
||||||
total_assets = flt(asset[0].get(key))
|
total_assets = flt(asset[0].get(key))
|
||||||
|
effective_liability = 0.00
|
||||||
|
|
||||||
if liability or equity:
|
if liability:
|
||||||
effective_liability = 0.0
|
effective_liability += flt(liability[0].get(key))
|
||||||
if liability:
|
if equity:
|
||||||
effective_liability += flt(liability[0].get(key))
|
effective_liability += flt(equity[0].get(key))
|
||||||
if equity:
|
|
||||||
effective_liability += flt(equity[0].get(key))
|
|
||||||
|
|
||||||
provisional_profit_loss[key] = total_assets - effective_liability
|
provisional_profit_loss[key] = total_assets - effective_liability
|
||||||
else:
|
total_row[key] = provisional_profit_loss[key] + effective_liability
|
||||||
provisional_profit_loss[key] = total_assets
|
|
||||||
|
|
||||||
total_row[key] = provisional_profit_loss[key]
|
|
||||||
|
|
||||||
if provisional_profit_loss[key]:
|
if provisional_profit_loss[key]:
|
||||||
has_value = True
|
has_value = True
|
||||||
|
|||||||
@@ -713,7 +713,8 @@ class GrossProfitGenerator:
|
|||||||
|
|
||||||
def get_average_buying_rate(self, row, item_code):
|
def get_average_buying_rate(self, row, item_code):
|
||||||
args = row
|
args = row
|
||||||
if item_code not in self.average_buying_rate:
|
key = (item_code, row.warehouse)
|
||||||
|
if key not in self.average_buying_rate:
|
||||||
args.update(
|
args.update(
|
||||||
{
|
{
|
||||||
"voucher_type": row.parenttype,
|
"voucher_type": row.parenttype,
|
||||||
@@ -727,9 +728,9 @@ class GrossProfitGenerator:
|
|||||||
args.update({"serial_and_batch_bundle": row.serial_and_batch_bundle})
|
args.update({"serial_and_batch_bundle": row.serial_and_batch_bundle})
|
||||||
|
|
||||||
average_buying_rate = get_incoming_rate(args)
|
average_buying_rate = get_incoming_rate(args)
|
||||||
self.average_buying_rate[item_code] = flt(average_buying_rate)
|
self.average_buying_rate[key] = flt(average_buying_rate)
|
||||||
|
|
||||||
return self.average_buying_rate[item_code]
|
return self.average_buying_rate[key]
|
||||||
|
|
||||||
def get_last_purchase_rate(self, item_code, row):
|
def get_last_purchase_rate(self, item_code, row):
|
||||||
purchase_invoice = frappe.qb.DocType("Purchase Invoice")
|
purchase_invoice = frappe.qb.DocType("Purchase Invoice")
|
||||||
|
|||||||
@@ -558,3 +558,50 @@ class TestGrossProfit(FrappeTestCase):
|
|||||||
}
|
}
|
||||||
gp_entry = [x for x in data if x.parent_invoice == sinv.name]
|
gp_entry = [x for x in data if x.parent_invoice == sinv.name]
|
||||||
self.assertDictContainsSubset(expected_entry, gp_entry[0])
|
self.assertDictContainsSubset(expected_entry, gp_entry[0])
|
||||||
|
|
||||||
|
def test_valuation_rate_without_previous_sle(self):
|
||||||
|
"""
|
||||||
|
Test Valuation rate calculation when stock ledger is empty and invoices are against different warehouses
|
||||||
|
"""
|
||||||
|
stock_settings = frappe.get_doc("Stock Settings")
|
||||||
|
stock_settings.valuation_method = "FIFO"
|
||||||
|
stock_settings.save()
|
||||||
|
|
||||||
|
item = create_item(
|
||||||
|
item_code="_Test Wirebound Notebook",
|
||||||
|
is_stock_item=1,
|
||||||
|
)
|
||||||
|
item.allow_negative_stock = True
|
||||||
|
item.save()
|
||||||
|
self.item = item.item_code
|
||||||
|
|
||||||
|
item.reload()
|
||||||
|
item.valuation_rate = 1900
|
||||||
|
item.save()
|
||||||
|
sinv1 = self.create_sales_invoice(qty=1, rate=2000, posting_date=nowdate(), do_not_submit=True)
|
||||||
|
sinv1.update_stock = 1
|
||||||
|
sinv1.set_warehouse = self.warehouse
|
||||||
|
sinv1.items[0].warehouse = self.warehouse
|
||||||
|
sinv1.save().submit()
|
||||||
|
|
||||||
|
item.reload()
|
||||||
|
item.valuation_rate = 1800
|
||||||
|
item.save()
|
||||||
|
sinv2 = self.create_sales_invoice(qty=1, rate=2000, posting_date=nowdate(), do_not_submit=True)
|
||||||
|
sinv2.update_stock = 1
|
||||||
|
sinv2.set_warehouse = self.finished_warehouse
|
||||||
|
sinv2.items[0].warehouse = self.finished_warehouse
|
||||||
|
sinv2.save().submit()
|
||||||
|
|
||||||
|
filters = frappe._dict(
|
||||||
|
company=self.company, from_date=nowdate(), to_date=nowdate(), group_by="Invoice"
|
||||||
|
)
|
||||||
|
columns, data = execute(filters=filters)
|
||||||
|
|
||||||
|
item_from_sinv1 = [x for x in data if x.parent_invoice == sinv1.name]
|
||||||
|
self.assertEqual(len(item_from_sinv1), 1)
|
||||||
|
self.assertEqual(1900, item_from_sinv1[0].valuation_rate)
|
||||||
|
|
||||||
|
item_from_sinv2 = [x for x in data if x.parent_invoice == sinv2.name]
|
||||||
|
self.assertEqual(len(item_from_sinv2), 1)
|
||||||
|
self.assertEqual(1800, item_from_sinv2[0].valuation_rate)
|
||||||
|
|||||||
@@ -187,7 +187,7 @@ frappe.ui.form.on("Asset", {
|
|||||||
if (frm.doc.docstatus == 0) {
|
if (frm.doc.docstatus == 0) {
|
||||||
frm.toggle_reqd("finance_books", frm.doc.calculate_depreciation);
|
frm.toggle_reqd("finance_books", frm.doc.calculate_depreciation);
|
||||||
|
|
||||||
if (frm.doc.is_composite_asset && !frm.doc.capitalized_in) {
|
if (frm.doc.is_composite_asset) {
|
||||||
$(".primary-action").prop("hidden", true);
|
$(".primary-action").prop("hidden", true);
|
||||||
$(".form-message").text("Capitalize this asset to confirm");
|
$(".form-message").text("Capitalize this asset to confirm");
|
||||||
|
|
||||||
@@ -511,6 +511,8 @@ frappe.ui.form.on("Asset", {
|
|||||||
frappe.call({
|
frappe.call({
|
||||||
args: {
|
args: {
|
||||||
asset: frm.doc.name,
|
asset: frm.doc.name,
|
||||||
|
asset_name: frm.doc.asset_name,
|
||||||
|
item_code: frm.doc.item_code,
|
||||||
},
|
},
|
||||||
method: "erpnext.assets.doctype.asset.asset.create_asset_capitalization",
|
method: "erpnext.assets.doctype.asset.asset.create_asset_capitalization",
|
||||||
callback: function (r) {
|
callback: function (r) {
|
||||||
|
|||||||
@@ -75,8 +75,7 @@
|
|||||||
"purchase_amount",
|
"purchase_amount",
|
||||||
"default_finance_book",
|
"default_finance_book",
|
||||||
"depr_entry_posting_status",
|
"depr_entry_posting_status",
|
||||||
"amended_from",
|
"amended_from"
|
||||||
"capitalized_in"
|
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@@ -222,7 +221,7 @@
|
|||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "eval:!(doc.is_composite_asset && !doc.capitalized_in)",
|
"depends_on": "eval:!doc.is_composite_asset",
|
||||||
"fieldname": "gross_purchase_amount",
|
"fieldname": "gross_purchase_amount",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"label": "Gross Purchase Amount",
|
"label": "Gross Purchase Amount",
|
||||||
@@ -508,14 +507,6 @@
|
|||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Is Composite Asset"
|
"label": "Is Composite Asset"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "capitalized_in",
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"hidden": 1,
|
|
||||||
"label": "Capitalized In",
|
|
||||||
"options": "Asset Capitalization",
|
|
||||||
"read_only": 1
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"depends_on": "eval:doc.docstatus > 0",
|
"depends_on": "eval:doc.docstatus > 0",
|
||||||
"fieldname": "total_asset_cost",
|
"fieldname": "total_asset_cost",
|
||||||
@@ -589,7 +580,7 @@
|
|||||||
"link_fieldname": "target_asset"
|
"link_fieldname": "target_asset"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2024-05-21 13:46:21.066483",
|
"modified": "2024-07-07 22:27:14.733839",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Assets",
|
"module": "Assets",
|
||||||
"name": "Asset",
|
"name": "Asset",
|
||||||
|
|||||||
@@ -60,7 +60,6 @@ class Asset(AccountsController):
|
|||||||
available_for_use_date: DF.Date | None
|
available_for_use_date: DF.Date | None
|
||||||
booked_fixed_asset: DF.Check
|
booked_fixed_asset: DF.Check
|
||||||
calculate_depreciation: DF.Check
|
calculate_depreciation: DF.Check
|
||||||
capitalized_in: DF.Link | None
|
|
||||||
company: DF.Link
|
company: DF.Link
|
||||||
comprehensive_insurance: DF.Data | None
|
comprehensive_insurance: DF.Data | None
|
||||||
cost_center: DF.Link | None
|
cost_center: DF.Link | None
|
||||||
@@ -162,7 +161,7 @@ class Asset(AccountsController):
|
|||||||
def on_cancel(self):
|
def on_cancel(self):
|
||||||
self.validate_cancellation()
|
self.validate_cancellation()
|
||||||
self.cancel_movement_entries()
|
self.cancel_movement_entries()
|
||||||
self.cancel_capitalization()
|
self.reload()
|
||||||
self.delete_depreciation_entries()
|
self.delete_depreciation_entries()
|
||||||
cancel_asset_depr_schedules(self)
|
cancel_asset_depr_schedules(self)
|
||||||
self.set_status()
|
self.set_status()
|
||||||
@@ -524,16 +523,6 @@ class Asset(AccountsController):
|
|||||||
movement = frappe.get_doc("Asset Movement", movement.get("name"))
|
movement = frappe.get_doc("Asset Movement", movement.get("name"))
|
||||||
movement.cancel()
|
movement.cancel()
|
||||||
|
|
||||||
def cancel_capitalization(self):
|
|
||||||
asset_capitalization = frappe.db.get_value(
|
|
||||||
"Asset Capitalization",
|
|
||||||
{"target_asset": self.name, "docstatus": 1, "entry_type": "Capitalization"},
|
|
||||||
)
|
|
||||||
|
|
||||||
if asset_capitalization:
|
|
||||||
asset_capitalization = frappe.get_doc("Asset Capitalization", asset_capitalization)
|
|
||||||
asset_capitalization.cancel()
|
|
||||||
|
|
||||||
def delete_depreciation_entries(self):
|
def delete_depreciation_entries(self):
|
||||||
if self.calculate_depreciation:
|
if self.calculate_depreciation:
|
||||||
for row in self.get("finance_books"):
|
for row in self.get("finance_books"):
|
||||||
@@ -872,10 +861,15 @@ def create_asset_repair(asset, asset_name):
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def create_asset_capitalization(asset):
|
def create_asset_capitalization(asset, asset_name, item_code):
|
||||||
asset_capitalization = frappe.new_doc("Asset Capitalization")
|
asset_capitalization = frappe.new_doc("Asset Capitalization")
|
||||||
asset_capitalization.update(
|
asset_capitalization.update(
|
||||||
{"target_asset": asset, "capitalization_method": "Choose a WIP composite asset"}
|
{
|
||||||
|
"target_asset": asset,
|
||||||
|
"capitalization_method": "Choose a WIP composite asset",
|
||||||
|
"target_asset_name": asset_name,
|
||||||
|
"target_item_code": item_code,
|
||||||
|
}
|
||||||
)
|
)
|
||||||
return asset_capitalization
|
return asset_capitalization
|
||||||
|
|
||||||
|
|||||||
@@ -138,22 +138,10 @@ class AssetCapitalization(StockController):
|
|||||||
"Asset",
|
"Asset",
|
||||||
"Asset Movement",
|
"Asset Movement",
|
||||||
)
|
)
|
||||||
self.cancel_target_asset()
|
|
||||||
self.update_stock_ledger()
|
self.update_stock_ledger()
|
||||||
self.make_gl_entries()
|
self.make_gl_entries()
|
||||||
self.restore_consumed_asset_items()
|
self.restore_consumed_asset_items()
|
||||||
|
|
||||||
def on_trash(self):
|
|
||||||
frappe.db.set_value("Asset", self.target_asset, "capitalized_in", None)
|
|
||||||
super().on_trash()
|
|
||||||
|
|
||||||
def cancel_target_asset(self):
|
|
||||||
if self.entry_type == "Capitalization" and self.target_asset:
|
|
||||||
asset_doc = frappe.get_doc("Asset", self.target_asset)
|
|
||||||
asset_doc.db_set("capitalized_in", None)
|
|
||||||
if asset_doc.docstatus == 1:
|
|
||||||
asset_doc.cancel()
|
|
||||||
|
|
||||||
def set_title(self):
|
def set_title(self):
|
||||||
self.title = self.target_asset_name or self.target_item_name or self.target_item_code
|
self.title = self.target_asset_name or self.target_item_name or self.target_item_code
|
||||||
|
|
||||||
@@ -329,8 +317,12 @@ class AssetCapitalization(StockController):
|
|||||||
if not self.target_is_fixed_asset and not self.get("asset_items"):
|
if not self.target_is_fixed_asset and not self.get("asset_items"):
|
||||||
frappe.throw(_("Consumed Asset Items is mandatory for Decapitalization"))
|
frappe.throw(_("Consumed Asset Items is mandatory for Decapitalization"))
|
||||||
|
|
||||||
if not self.get("stock_items") and not self.get("asset_items"):
|
if not (self.get("stock_items") or self.get("asset_items") or self.get("service_items")):
|
||||||
frappe.throw(_("Consumed Stock Items or Consumed Asset Items is mandatory for Capitalization"))
|
frappe.throw(
|
||||||
|
_(
|
||||||
|
"Consumed Stock Items, Consumed Asset Items or Consumed Service Items is mandatory for Capitalization"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def validate_item(self, item):
|
def validate_item(self, item):
|
||||||
from erpnext.stock.doctype.item.item import validate_end_of_life
|
from erpnext.stock.doctype.item.item import validate_end_of_life
|
||||||
@@ -617,7 +609,6 @@ class AssetCapitalization(StockController):
|
|||||||
asset_doc.purchase_date = self.posting_date
|
asset_doc.purchase_date = self.posting_date
|
||||||
asset_doc.gross_purchase_amount = total_target_asset_value
|
asset_doc.gross_purchase_amount = total_target_asset_value
|
||||||
asset_doc.purchase_amount = total_target_asset_value
|
asset_doc.purchase_amount = total_target_asset_value
|
||||||
asset_doc.capitalized_in = self.name
|
|
||||||
asset_doc.flags.ignore_validate = True
|
asset_doc.flags.ignore_validate = True
|
||||||
asset_doc.flags.asset_created_via_asset_capitalization = True
|
asset_doc.flags.asset_created_via_asset_capitalization = True
|
||||||
asset_doc.insert()
|
asset_doc.insert()
|
||||||
@@ -653,7 +644,6 @@ class AssetCapitalization(StockController):
|
|||||||
asset_doc = frappe.get_doc("Asset", self.target_asset)
|
asset_doc = frappe.get_doc("Asset", self.target_asset)
|
||||||
asset_doc.gross_purchase_amount = total_target_asset_value
|
asset_doc.gross_purchase_amount = total_target_asset_value
|
||||||
asset_doc.purchase_amount = total_target_asset_value
|
asset_doc.purchase_amount = total_target_asset_value
|
||||||
asset_doc.capitalized_in = self.name
|
|
||||||
asset_doc.flags.ignore_validate = True
|
asset_doc.flags.ignore_validate = True
|
||||||
asset_doc.save()
|
asset_doc.save()
|
||||||
|
|
||||||
|
|||||||
@@ -386,6 +386,56 @@ class TestAssetCapitalization(unittest.TestCase):
|
|||||||
self.assertFalse(get_actual_gle_dict(asset_capitalization.name))
|
self.assertFalse(get_actual_gle_dict(asset_capitalization.name))
|
||||||
self.assertFalse(get_actual_sle_dict(asset_capitalization.name))
|
self.assertFalse(get_actual_sle_dict(asset_capitalization.name))
|
||||||
|
|
||||||
|
def test_capitalize_only_service_item(self):
|
||||||
|
company = "_Test Company"
|
||||||
|
# Variables
|
||||||
|
|
||||||
|
service_rate = 500
|
||||||
|
service_qty = 2
|
||||||
|
service_amount = 1000
|
||||||
|
|
||||||
|
total_amount = 1000
|
||||||
|
|
||||||
|
wip_composite_asset = create_asset(
|
||||||
|
asset_name="Asset Capitalization WIP Composite Asset",
|
||||||
|
is_composite_asset=1,
|
||||||
|
warehouse="Stores - TCP1",
|
||||||
|
company=company,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create and submit Asset Captitalization
|
||||||
|
asset_capitalization = create_asset_capitalization(
|
||||||
|
entry_type="Capitalization",
|
||||||
|
capitalization_method="Choose a WIP composite asset",
|
||||||
|
target_asset=wip_composite_asset.name,
|
||||||
|
target_asset_location="Test Location",
|
||||||
|
service_qty=service_qty,
|
||||||
|
service_rate=service_rate,
|
||||||
|
service_expense_account="Expenses Included In Asset Valuation - _TC",
|
||||||
|
company=company,
|
||||||
|
submit=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(asset_capitalization.service_items[0].amount, service_amount)
|
||||||
|
self.assertEqual(asset_capitalization.service_items_total, service_amount)
|
||||||
|
|
||||||
|
target_asset = frappe.get_doc("Asset", asset_capitalization.target_asset)
|
||||||
|
self.assertEqual(target_asset.gross_purchase_amount, total_amount)
|
||||||
|
self.assertEqual(target_asset.purchase_amount, total_amount)
|
||||||
|
|
||||||
|
expected_gle = {
|
||||||
|
"_Test Fixed Asset - _TC": 1000.0,
|
||||||
|
"Expenses Included In Asset Valuation - _TC": -1000.0,
|
||||||
|
}
|
||||||
|
|
||||||
|
actual_gle = get_actual_gle_dict(asset_capitalization.name)
|
||||||
|
self.assertEqual(actual_gle, expected_gle)
|
||||||
|
|
||||||
|
# Cancel Asset Capitalization and make test entries and status are reversed
|
||||||
|
asset_capitalization.cancel()
|
||||||
|
self.assertFalse(get_actual_gle_dict(asset_capitalization.name))
|
||||||
|
self.assertFalse(get_actual_sle_dict(asset_capitalization.name))
|
||||||
|
|
||||||
|
|
||||||
def create_asset_capitalization_data():
|
def create_asset_capitalization_data():
|
||||||
create_item("Capitalization Target Stock Item", is_stock_item=1, is_fixed_asset=0, is_purchase_item=0)
|
create_item("Capitalization Target Stock Item", is_stock_item=1, is_fixed_asset=0, is_purchase_item=0)
|
||||||
|
|||||||
@@ -541,7 +541,9 @@ class BuyingController(SubcontractingController):
|
|||||||
"actual_qty": flt(pr_qty),
|
"actual_qty": flt(pr_qty),
|
||||||
"serial_and_batch_bundle": (
|
"serial_and_batch_bundle": (
|
||||||
d.serial_and_batch_bundle
|
d.serial_and_batch_bundle
|
||||||
if not self.is_internal_transfer() or self.is_return
|
if not self.is_internal_transfer()
|
||||||
|
or self.is_return
|
||||||
|
or (self.is_internal_transfer() and self.docstatus == 2)
|
||||||
else self.get_package_for_target_warehouse(
|
else self.get_package_for_target_warehouse(
|
||||||
d, type_of_transaction=type_of_transaction
|
d, type_of_transaction=type_of_transaction
|
||||||
)
|
)
|
||||||
@@ -580,6 +582,14 @@ class BuyingController(SubcontractingController):
|
|||||||
(not cint(self.is_return) and self.docstatus == 2)
|
(not cint(self.is_return) and self.docstatus == 2)
|
||||||
or (cint(self.is_return) and self.docstatus == 1)
|
or (cint(self.is_return) and self.docstatus == 1)
|
||||||
):
|
):
|
||||||
|
serial_and_batch_bundle = None
|
||||||
|
if self.is_internal_transfer() and self.docstatus == 2:
|
||||||
|
serial_and_batch_bundle = frappe.db.get_value(
|
||||||
|
"Stock Ledger Entry",
|
||||||
|
{"voucher_detail_no": d.name, "warehouse": d.warehouse},
|
||||||
|
"serial_and_batch_bundle",
|
||||||
|
)
|
||||||
|
|
||||||
from_warehouse_sle = self.get_sl_entries(
|
from_warehouse_sle = self.get_sl_entries(
|
||||||
d,
|
d,
|
||||||
{
|
{
|
||||||
@@ -589,7 +599,7 @@ class BuyingController(SubcontractingController):
|
|||||||
"serial_and_batch_bundle": (
|
"serial_and_batch_bundle": (
|
||||||
self.get_package_for_target_warehouse(d, d.from_warehouse, "Inward")
|
self.get_package_for_target_warehouse(d, d.from_warehouse, "Inward")
|
||||||
if self.is_internal_transfer() and self.is_return
|
if self.is_internal_transfer() and self.is_return
|
||||||
else None
|
else serial_and_batch_bundle
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -41,7 +41,8 @@ def get_variant(template, args=None, variant=None, manufacturer=None, manufactur
|
|||||||
if isinstance(args, str):
|
if isinstance(args, str):
|
||||||
args = json.loads(args)
|
args = json.loads(args)
|
||||||
|
|
||||||
if not args:
|
attribute_args = {k: v for k, v in args.items() if k != "use_template_image"}
|
||||||
|
if not attribute_args:
|
||||||
frappe.throw(_("Please specify at least one attribute in the Attributes table"))
|
frappe.throw(_("Please specify at least one attribute in the Attributes table"))
|
||||||
|
|
||||||
return find_variant(template, args, variant)
|
return find_variant(template, args, variant)
|
||||||
@@ -197,7 +198,8 @@ def find_variant(template, args, variant_item_code=None):
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def create_variant(item, args):
|
def create_variant(item, args, use_template_image=False):
|
||||||
|
use_template_image = frappe.parse_json(use_template_image)
|
||||||
if isinstance(args, str):
|
if isinstance(args, str):
|
||||||
args = json.loads(args)
|
args = json.loads(args)
|
||||||
|
|
||||||
@@ -211,13 +213,18 @@ def create_variant(item, args):
|
|||||||
|
|
||||||
variant.set("attributes", variant_attributes)
|
variant.set("attributes", variant_attributes)
|
||||||
copy_attributes_to_variant(template, variant)
|
copy_attributes_to_variant(template, variant)
|
||||||
|
|
||||||
|
if use_template_image and template.image:
|
||||||
|
variant.image = template.image
|
||||||
|
|
||||||
make_variant_item_code(template.item_code, template.item_name, variant)
|
make_variant_item_code(template.item_code, template.item_name, variant)
|
||||||
|
|
||||||
return variant
|
return variant
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def enqueue_multiple_variant_creation(item, args):
|
def enqueue_multiple_variant_creation(item, args, use_template_image=False):
|
||||||
|
use_template_image = frappe.parse_json(use_template_image)
|
||||||
# There can be innumerable attribute combinations, enqueue
|
# There can be innumerable attribute combinations, enqueue
|
||||||
if isinstance(args, str):
|
if isinstance(args, str):
|
||||||
variants = json.loads(args)
|
variants = json.loads(args)
|
||||||
@@ -228,27 +235,31 @@ def enqueue_multiple_variant_creation(item, args):
|
|||||||
frappe.throw(_("Please do not create more than 500 items at a time"))
|
frappe.throw(_("Please do not create more than 500 items at a time"))
|
||||||
return
|
return
|
||||||
if total_variants < 10:
|
if total_variants < 10:
|
||||||
return create_multiple_variants(item, args)
|
return create_multiple_variants(item, args, use_template_image)
|
||||||
else:
|
else:
|
||||||
frappe.enqueue(
|
frappe.enqueue(
|
||||||
"erpnext.controllers.item_variant.create_multiple_variants",
|
"erpnext.controllers.item_variant.create_multiple_variants",
|
||||||
item=item,
|
item=item,
|
||||||
args=args,
|
args=args,
|
||||||
|
use_template_image=use_template_image,
|
||||||
now=frappe.flags.in_test,
|
now=frappe.flags.in_test,
|
||||||
)
|
)
|
||||||
return "queued"
|
return "queued"
|
||||||
|
|
||||||
|
|
||||||
def create_multiple_variants(item, args):
|
def create_multiple_variants(item, args, use_template_image=False):
|
||||||
count = 0
|
count = 0
|
||||||
if isinstance(args, str):
|
if isinstance(args, str):
|
||||||
args = json.loads(args)
|
args = json.loads(args)
|
||||||
|
|
||||||
|
template_item = frappe.get_doc("Item", item)
|
||||||
args_set = generate_keyed_value_combinations(args)
|
args_set = generate_keyed_value_combinations(args)
|
||||||
|
|
||||||
for attribute_values in args_set:
|
for attribute_values in args_set:
|
||||||
if not get_variant(item, args=attribute_values):
|
if not get_variant(item, args=attribute_values):
|
||||||
variant = create_variant(item, attribute_values)
|
variant = create_variant(item, attribute_values)
|
||||||
|
if use_template_image and template_item.image:
|
||||||
|
variant.image = template_item.image
|
||||||
variant.save()
|
variant.save()
|
||||||
count += 1
|
count += 1
|
||||||
|
|
||||||
|
|||||||
@@ -538,7 +538,9 @@ class SellingController(StockController):
|
|||||||
self.make_sl_entries(sl_entries)
|
self.make_sl_entries(sl_entries)
|
||||||
|
|
||||||
def get_sle_for_source_warehouse(self, item_row):
|
def get_sle_for_source_warehouse(self, item_row):
|
||||||
serial_and_batch_bundle = item_row.serial_and_batch_bundle
|
serial_and_batch_bundle = (
|
||||||
|
item_row.serial_and_batch_bundle if not self.is_internal_transfer() else None
|
||||||
|
)
|
||||||
if serial_and_batch_bundle and self.is_internal_transfer() and self.is_return:
|
if serial_and_batch_bundle and self.is_internal_transfer() and self.is_return:
|
||||||
if self.docstatus == 1:
|
if self.docstatus == 1:
|
||||||
serial_and_batch_bundle = self.make_package_for_transfer(
|
serial_and_batch_bundle = self.make_package_for_transfer(
|
||||||
|
|||||||
@@ -908,6 +908,7 @@ class SubcontractingController(StockController):
|
|||||||
item,
|
item,
|
||||||
{
|
{
|
||||||
"item_code": item.rm_item_code,
|
"item_code": item.rm_item_code,
|
||||||
|
"incoming_rate": item.rate if self.is_return else 0,
|
||||||
"warehouse": self.supplier_warehouse,
|
"warehouse": self.supplier_warehouse,
|
||||||
"actual_qty": -1 * flt(item.consumed_qty, item.precision("consumed_qty")),
|
"actual_qty": -1 * flt(item.consumed_qty, item.precision("consumed_qty")),
|
||||||
"dependant_sle_voucher_detail_no": item.reference_name,
|
"dependant_sle_voucher_detail_no": item.reference_name,
|
||||||
|
|||||||
@@ -362,7 +362,6 @@ erpnext.stock.doctype.delivery_note.patches.drop_unused_return_against_index # 2
|
|||||||
erpnext.patches.v14_0.set_maintain_stock_for_bom_item
|
erpnext.patches.v14_0.set_maintain_stock_for_bom_item
|
||||||
erpnext.patches.v15_0.delete_orphaned_asset_movement_item_records
|
erpnext.patches.v15_0.delete_orphaned_asset_movement_item_records
|
||||||
erpnext.patches.v15_0.fix_debit_credit_in_transaction_currency
|
erpnext.patches.v15_0.fix_debit_credit_in_transaction_currency
|
||||||
erpnext.patches.v15_0.remove_cancelled_asset_capitalization_from_asset
|
|
||||||
erpnext.patches.v15_0.rename_purchase_receipt_amount_to_purchase_amount
|
erpnext.patches.v15_0.rename_purchase_receipt_amount_to_purchase_amount
|
||||||
erpnext.patches.v14_0.enable_set_priority_for_pricing_rules #1
|
erpnext.patches.v14_0.enable_set_priority_for_pricing_rules #1
|
||||||
erpnext.patches.v15_0.rename_number_of_depreciations_booked_to_opening_booked_depreciations
|
erpnext.patches.v15_0.rename_number_of_depreciations_booked_to_opening_booked_depreciations
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
import frappe
|
|
||||||
|
|
||||||
|
|
||||||
def execute():
|
|
||||||
cancelled_asset_capitalizations = frappe.get_all(
|
|
||||||
"Asset Capitalization",
|
|
||||||
filters={"docstatus": 2},
|
|
||||||
fields=["name", "target_asset"],
|
|
||||||
)
|
|
||||||
for asset_capitalization in cancelled_asset_capitalizations:
|
|
||||||
frappe.db.set_value("Asset", asset_capitalization.target_asset, "capitalized_in", None)
|
|
||||||
@@ -603,6 +603,17 @@ class SalesOrder(SellingController):
|
|||||||
if total_picked_qty and total_qty:
|
if total_picked_qty and total_qty:
|
||||||
per_picked = total_picked_qty / total_qty * 100
|
per_picked = total_picked_qty / total_qty * 100
|
||||||
|
|
||||||
|
pick_percentage = frappe.db.get_single_value("Stock Settings", "over_picking_allowance")
|
||||||
|
if pick_percentage:
|
||||||
|
total_qty += flt(total_qty) * (pick_percentage / 100)
|
||||||
|
|
||||||
|
if total_picked_qty > total_qty:
|
||||||
|
frappe.throw(
|
||||||
|
_(
|
||||||
|
"Total Picked Quantity {0} is more than ordered qty {1}. You can set the Over Picking Allowance in Stock Settings."
|
||||||
|
).format(total_picked_qty, total_qty)
|
||||||
|
)
|
||||||
|
|
||||||
self.db_set("per_picked", flt(per_picked), update_modified=False)
|
self.db_set("per_picked", flt(per_picked), update_modified=False)
|
||||||
|
|
||||||
def set_indicator(self):
|
def set_indicator(self):
|
||||||
|
|||||||
@@ -1918,6 +1918,93 @@ class TestDeliveryNote(FrappeTestCase):
|
|||||||
returned_serial_nos = get_serial_nos_from_bundle(dn_return.items[0].serial_and_batch_bundle)
|
returned_serial_nos = get_serial_nos_from_bundle(dn_return.items[0].serial_and_batch_bundle)
|
||||||
self.assertEqual(serial_nos, returned_serial_nos)
|
self.assertEqual(serial_nos, returned_serial_nos)
|
||||||
|
|
||||||
|
def test_same_posting_date_and_posting_time(self):
|
||||||
|
item_code = make_item(
|
||||||
|
"Test Same Posting Datetime Item",
|
||||||
|
properties={
|
||||||
|
"has_batch_no": 1,
|
||||||
|
"create_new_batch": 1,
|
||||||
|
"batch_number_series": "SS-ART11-TESTBATCH.#####",
|
||||||
|
"is_stock_item": 1,
|
||||||
|
},
|
||||||
|
).name
|
||||||
|
|
||||||
|
se = make_stock_entry(
|
||||||
|
item_code=item_code,
|
||||||
|
target="_Test Warehouse - _TC",
|
||||||
|
qty=100,
|
||||||
|
basic_rate=50,
|
||||||
|
posting_date=add_days(nowdate(), -1),
|
||||||
|
)
|
||||||
|
|
||||||
|
batch_no = get_batch_from_bundle(se.items[0].serial_and_batch_bundle)
|
||||||
|
|
||||||
|
posting_date = today()
|
||||||
|
posting_time = nowtime()
|
||||||
|
dn1 = create_delivery_note(
|
||||||
|
posting_date=posting_date,
|
||||||
|
posting_time=posting_time,
|
||||||
|
item_code=item_code,
|
||||||
|
rate=300,
|
||||||
|
qty=25,
|
||||||
|
batch_no=batch_no,
|
||||||
|
use_serial_batch_fields=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
dn2 = create_delivery_note(
|
||||||
|
posting_date=posting_date,
|
||||||
|
posting_time=posting_time,
|
||||||
|
item_code=item_code,
|
||||||
|
rate=300,
|
||||||
|
qty=25,
|
||||||
|
batch_no=batch_no,
|
||||||
|
use_serial_batch_fields=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
dn3 = create_delivery_note(
|
||||||
|
posting_date=posting_date,
|
||||||
|
posting_time=posting_time,
|
||||||
|
item_code=item_code,
|
||||||
|
rate=300,
|
||||||
|
qty=25,
|
||||||
|
batch_no=batch_no,
|
||||||
|
use_serial_batch_fields=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
dn4 = create_delivery_note(
|
||||||
|
posting_date=posting_date,
|
||||||
|
posting_time=posting_time,
|
||||||
|
item_code=item_code,
|
||||||
|
rate=300,
|
||||||
|
qty=25,
|
||||||
|
batch_no=batch_no,
|
||||||
|
use_serial_batch_fields=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
for dn in [dn1, dn2, dn3, dn4]:
|
||||||
|
sles = frappe.get_all(
|
||||||
|
"Stock Ledger Entry",
|
||||||
|
fields=["stock_value_difference", "actual_qty"],
|
||||||
|
filters={"is_cancelled": 0, "voucher_no": dn.name, "docstatus": 1},
|
||||||
|
)
|
||||||
|
|
||||||
|
for sle in sles:
|
||||||
|
self.assertEqual(sle.actual_qty, 25.0 * -1)
|
||||||
|
self.assertEqual(sle.stock_value_difference, 25.0 * 50 * -1)
|
||||||
|
|
||||||
|
dn5 = create_delivery_note(
|
||||||
|
posting_date=posting_date,
|
||||||
|
posting_time=posting_time,
|
||||||
|
item_code=item_code,
|
||||||
|
rate=300,
|
||||||
|
qty=25,
|
||||||
|
batch_no=batch_no,
|
||||||
|
use_serial_batch_fields=1,
|
||||||
|
do_not_submit=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertRaises(frappe.ValidationError, dn5.submit)
|
||||||
|
|
||||||
|
|
||||||
def create_delivery_note(**args):
|
def create_delivery_note(**args):
|
||||||
dn = frappe.new_doc("Delivery Note")
|
dn = frappe.new_doc("Delivery Note")
|
||||||
|
|||||||
@@ -587,6 +587,14 @@ $.extend(erpnext.item, {
|
|||||||
me.multiple_variant_dialog = new frappe.ui.Dialog({
|
me.multiple_variant_dialog = new frappe.ui.Dialog({
|
||||||
title: __("Select Attribute Values"),
|
title: __("Select Attribute Values"),
|
||||||
fields: [
|
fields: [
|
||||||
|
frm.doc.image
|
||||||
|
? {
|
||||||
|
fieldtype: "Check",
|
||||||
|
label: __("Create a variant with the template image."),
|
||||||
|
fieldname: "use_template_image",
|
||||||
|
default: 0,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
{
|
{
|
||||||
fieldtype: "HTML",
|
fieldtype: "HTML",
|
||||||
fieldname: "help",
|
fieldname: "help",
|
||||||
@@ -594,11 +602,14 @@ $.extend(erpnext.item, {
|
|||||||
${__("Select at least one value from each of the attributes.")}
|
${__("Select at least one value from each of the attributes.")}
|
||||||
</label>`,
|
</label>`,
|
||||||
},
|
},
|
||||||
].concat(fields),
|
]
|
||||||
|
.concat(fields)
|
||||||
|
.filter(Boolean),
|
||||||
});
|
});
|
||||||
|
|
||||||
me.multiple_variant_dialog.set_primary_action(__("Create Variants"), () => {
|
me.multiple_variant_dialog.set_primary_action(__("Create Variants"), () => {
|
||||||
let selected_attributes = get_selected_attributes();
|
let selected_attributes = get_selected_attributes();
|
||||||
|
let use_template_image = me.multiple_variant_dialog.get_value("use_template_image");
|
||||||
|
|
||||||
me.multiple_variant_dialog.hide();
|
me.multiple_variant_dialog.hide();
|
||||||
frappe.call({
|
frappe.call({
|
||||||
@@ -606,6 +617,7 @@ $.extend(erpnext.item, {
|
|||||||
args: {
|
args: {
|
||||||
item: frm.doc.name,
|
item: frm.doc.name,
|
||||||
args: selected_attributes,
|
args: selected_attributes,
|
||||||
|
use_template_image: use_template_image,
|
||||||
},
|
},
|
||||||
callback: function (r) {
|
callback: function (r) {
|
||||||
if (r.message === "queued") {
|
if (r.message === "queued") {
|
||||||
@@ -720,6 +732,15 @@ $.extend(erpnext.item, {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (frm.doc.image) {
|
||||||
|
fields.push({
|
||||||
|
fieldtype: "Check",
|
||||||
|
label: __("Create a variant with the template image."),
|
||||||
|
fieldname: "use_template_image",
|
||||||
|
default: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
var d = new frappe.ui.Dialog({
|
var d = new frappe.ui.Dialog({
|
||||||
title: __("Create Variant"),
|
title: __("Create Variant"),
|
||||||
fields: fields,
|
fields: fields,
|
||||||
@@ -761,6 +782,7 @@ $.extend(erpnext.item, {
|
|||||||
args: {
|
args: {
|
||||||
item: frm.doc.name,
|
item: frm.doc.name,
|
||||||
args: d.get_values(),
|
args: d.get_values(),
|
||||||
|
use_template_image: args.use_template_image,
|
||||||
},
|
},
|
||||||
callback: function (r) {
|
callback: function (r) {
|
||||||
var doclist = frappe.model.sync(r.message);
|
var doclist = frappe.model.sync(r.message);
|
||||||
|
|||||||
@@ -1174,3 +1174,34 @@ class TestPickList(FrappeTestCase):
|
|||||||
row.qty = row.qty + 10
|
row.qty = row.qty + 10
|
||||||
|
|
||||||
self.assertRaises(frappe.ValidationError, pl.save)
|
self.assertRaises(frappe.ValidationError, pl.save)
|
||||||
|
|
||||||
|
def test_over_allowance_picking(self):
|
||||||
|
warehouse = "_Test Warehouse - _TC"
|
||||||
|
item = make_item(
|
||||||
|
"Test Over Allowance Picking Item",
|
||||||
|
properties={
|
||||||
|
"is_stock_item": 1,
|
||||||
|
},
|
||||||
|
).name
|
||||||
|
|
||||||
|
make_stock_entry(item=item, to_warehouse=warehouse, qty=100)
|
||||||
|
|
||||||
|
so = make_sales_order(item_code=item, qty=10, rate=100)
|
||||||
|
|
||||||
|
pl_doc = create_pick_list(so.name)
|
||||||
|
pl_doc.save()
|
||||||
|
self.assertEqual(pl_doc.locations[0].qty, 10)
|
||||||
|
|
||||||
|
pl_doc.locations[0].qty = 15
|
||||||
|
pl_doc.locations[0].stock_qty = 15
|
||||||
|
pl_doc.save()
|
||||||
|
|
||||||
|
self.assertEqual(pl_doc.locations[0].qty, 15)
|
||||||
|
self.assertRaises(frappe.ValidationError, pl_doc.submit)
|
||||||
|
|
||||||
|
frappe.db.set_single_value("Stock Settings", "over_picking_allowance", 50)
|
||||||
|
|
||||||
|
pl_doc.reload()
|
||||||
|
pl_doc.submit()
|
||||||
|
|
||||||
|
frappe.db.set_single_value("Stock Settings", "over_picking_allowance", 0)
|
||||||
|
|||||||
@@ -3350,6 +3350,122 @@ class TestPurchaseReceipt(FrappeTestCase):
|
|||||||
self.assertEqual(pr.grand_total, 0.0)
|
self.assertEqual(pr.grand_total, 0.0)
|
||||||
self.assertEqual(pr.status, "Completed")
|
self.assertEqual(pr.status, "Completed")
|
||||||
|
|
||||||
|
def test_internal_transfer_for_batch_items_with_cancel(self):
|
||||||
|
from erpnext.controllers.sales_and_purchase_return import make_return_doc
|
||||||
|
from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt
|
||||||
|
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
||||||
|
|
||||||
|
frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 0)
|
||||||
|
|
||||||
|
prepare_data_for_internal_transfer()
|
||||||
|
|
||||||
|
customer = "_Test Internal Customer 2"
|
||||||
|
company = "_Test Company with perpetual inventory"
|
||||||
|
|
||||||
|
batch_item_doc = make_item(
|
||||||
|
"_Test Batch Item For Stock Transfer Cancel Case",
|
||||||
|
{"has_batch_no": 1, "create_new_batch": 1, "batch_number_series": "USBF-BT-CANBIFST-.####"},
|
||||||
|
)
|
||||||
|
|
||||||
|
serial_item_doc = make_item(
|
||||||
|
"_Test Serial No Item For Stock Transfer Cancel Case",
|
||||||
|
{"has_serial_no": 1, "serial_no_series": "USBF-BT-CANBIFST-.####"},
|
||||||
|
)
|
||||||
|
|
||||||
|
inward_entry = make_purchase_receipt(
|
||||||
|
item_code=batch_item_doc.name,
|
||||||
|
qty=10,
|
||||||
|
rate=150,
|
||||||
|
warehouse="Stores - TCP1",
|
||||||
|
company="_Test Company with perpetual inventory",
|
||||||
|
use_serial_batch_fields=0,
|
||||||
|
do_not_submit=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
inward_entry.append(
|
||||||
|
"items",
|
||||||
|
{
|
||||||
|
"item_code": serial_item_doc.name,
|
||||||
|
"qty": 15,
|
||||||
|
"rate": 250,
|
||||||
|
"item_name": serial_item_doc.item_name,
|
||||||
|
"conversion_factor": 1.0,
|
||||||
|
"uom": serial_item_doc.stock_uom,
|
||||||
|
"stock_uom": serial_item_doc.stock_uom,
|
||||||
|
"warehouse": "Stores - TCP1",
|
||||||
|
"use_serial_batch_fields": 0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
inward_entry.submit()
|
||||||
|
inward_entry.reload()
|
||||||
|
|
||||||
|
for row in inward_entry.items:
|
||||||
|
self.assertTrue(row.serial_and_batch_bundle)
|
||||||
|
|
||||||
|
inter_transfer_dn = create_delivery_note(
|
||||||
|
item_code=inward_entry.items[0].item_code,
|
||||||
|
company=company,
|
||||||
|
customer=customer,
|
||||||
|
cost_center="Main - TCP1",
|
||||||
|
expense_account="Cost of Goods Sold - TCP1",
|
||||||
|
qty=10,
|
||||||
|
rate=500,
|
||||||
|
warehouse="Stores - TCP1",
|
||||||
|
target_warehouse="Work In Progress - TCP1",
|
||||||
|
batch_no=get_batch_from_bundle(inward_entry.items[0].serial_and_batch_bundle),
|
||||||
|
use_serial_batch_fields=0,
|
||||||
|
do_not_submit=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
inter_transfer_dn.append(
|
||||||
|
"items",
|
||||||
|
{
|
||||||
|
"item_code": serial_item_doc.name,
|
||||||
|
"qty": 15,
|
||||||
|
"rate": 350,
|
||||||
|
"item_name": serial_item_doc.item_name,
|
||||||
|
"conversion_factor": 1.0,
|
||||||
|
"uom": serial_item_doc.stock_uom,
|
||||||
|
"stock_uom": serial_item_doc.stock_uom,
|
||||||
|
"warehouse": "Stores - TCP1",
|
||||||
|
"target_warehouse": "Work In Progress - TCP1",
|
||||||
|
"serial_no": "\n".join(
|
||||||
|
get_serial_nos_from_bundle(inward_entry.items[1].serial_and_batch_bundle)
|
||||||
|
),
|
||||||
|
"use_serial_batch_fields": 0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
inter_transfer_dn.submit()
|
||||||
|
inter_transfer_dn.reload()
|
||||||
|
for row in inter_transfer_dn.items:
|
||||||
|
if row.item_code == batch_item_doc.name:
|
||||||
|
self.assertEqual(row.rate, 150.0)
|
||||||
|
else:
|
||||||
|
self.assertEqual(row.rate, 250.0)
|
||||||
|
|
||||||
|
self.assertTrue(row.serial_and_batch_bundle)
|
||||||
|
|
||||||
|
inter_transfer_pr = make_inter_company_purchase_receipt(inter_transfer_dn.name)
|
||||||
|
for row in inter_transfer_pr.items:
|
||||||
|
row.from_warehouse = "Work In Progress - TCP1"
|
||||||
|
row.warehouse = "Stores - TCP1"
|
||||||
|
inter_transfer_pr.submit()
|
||||||
|
|
||||||
|
for row in inter_transfer_pr.items:
|
||||||
|
if row.item_code == batch_item_doc.name:
|
||||||
|
self.assertEqual(row.rate, 150.0)
|
||||||
|
else:
|
||||||
|
self.assertEqual(row.rate, 250.0)
|
||||||
|
|
||||||
|
self.assertTrue(row.serial_and_batch_bundle)
|
||||||
|
|
||||||
|
inter_transfer_pr.cancel()
|
||||||
|
inter_transfer_dn.cancel()
|
||||||
|
|
||||||
|
frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 1)
|
||||||
|
|
||||||
|
|
||||||
def prepare_data_for_internal_transfer():
|
def prepare_data_for_internal_transfer():
|
||||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
|
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
|
||||||
|
|||||||
@@ -300,6 +300,7 @@ class SerialandBatchBundle(Document):
|
|||||||
"batch_nos": {row.batch_no: row for row in self.entries if row.batch_no},
|
"batch_nos": {row.batch_no: row for row in self.entries if row.batch_no},
|
||||||
"voucher_type": self.voucher_type,
|
"voucher_type": self.voucher_type,
|
||||||
"voucher_detail_no": self.voucher_detail_no,
|
"voucher_detail_no": self.voucher_detail_no,
|
||||||
|
"creation": self.creation,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -2756,7 +2756,7 @@ def make_stock_in_entry(source_name, target_doc=None):
|
|||||||
"batch_no": "batch_no",
|
"batch_no": "batch_no",
|
||||||
},
|
},
|
||||||
"postprocess": update_item,
|
"postprocess": update_item,
|
||||||
"condition": lambda doc: flt(doc.qty) - flt(doc.transferred_qty) > 0.01,
|
"condition": lambda doc: flt(doc.qty) - flt(doc.transferred_qty) > 0.00001,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
target_doc,
|
target_doc,
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
"section_break_9",
|
"section_break_9",
|
||||||
"over_delivery_receipt_allowance",
|
"over_delivery_receipt_allowance",
|
||||||
"mr_qty_allowance",
|
"mr_qty_allowance",
|
||||||
|
"over_picking_allowance",
|
||||||
"column_break_121",
|
"column_break_121",
|
||||||
"role_allowed_to_over_deliver_receive",
|
"role_allowed_to_over_deliver_receive",
|
||||||
"allow_negative_stock",
|
"allow_negative_stock",
|
||||||
@@ -446,6 +447,12 @@
|
|||||||
"fieldname": "do_not_use_batchwise_valuation",
|
"fieldname": "do_not_use_batchwise_valuation",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Do Not Use Batch-wise Valuation"
|
"label": "Do Not Use Batch-wise Valuation"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "The percentage you are allowed to pick more items in the pick list than the ordered quantity.",
|
||||||
|
"fieldname": "over_picking_allowance",
|
||||||
|
"fieldtype": "Percent",
|
||||||
|
"label": "Over Picking Allowance"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "icon-cog",
|
"icon": "icon-cog",
|
||||||
@@ -453,7 +460,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-07-04 12:45:09.811280",
|
"modified": "2024-07-15 17:18:23.872161",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Stock Settings",
|
"name": "Stock Settings",
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ class StockSettings(Document):
|
|||||||
mr_qty_allowance: DF.Float
|
mr_qty_allowance: DF.Float
|
||||||
naming_series_prefix: DF.Data | None
|
naming_series_prefix: DF.Data | None
|
||||||
over_delivery_receipt_allowance: DF.Float
|
over_delivery_receipt_allowance: DF.Float
|
||||||
|
over_picking_allowance: DF.Percent
|
||||||
pick_serial_and_batch_based_on: DF.Literal["FIFO", "LIFO", "Expiry"]
|
pick_serial_and_batch_based_on: DF.Literal["FIFO", "LIFO", "Expiry"]
|
||||||
reorder_email_notify: DF.Check
|
reorder_email_notify: DF.Check
|
||||||
role_allowed_to_create_edit_back_dated_transactions: DF.Link | None
|
role_allowed_to_create_edit_back_dated_transactions: DF.Link | None
|
||||||
|
|||||||
@@ -77,9 +77,8 @@ def get_columns(filters):
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": _("Party Type"),
|
"label": _("Party Type"),
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Data",
|
||||||
"fieldname": "party_type",
|
"fieldname": "party_type",
|
||||||
"options": "DocType",
|
|
||||||
"width": 90,
|
"width": 90,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -599,9 +599,15 @@ class BatchNoValuation(DeprecatedBatchNoValuation):
|
|||||||
|
|
||||||
timestamp_condition = ""
|
timestamp_condition = ""
|
||||||
if self.sle.posting_date and self.sle.posting_time:
|
if self.sle.posting_date and self.sle.posting_time:
|
||||||
timestamp_condition = CombineDatetime(
|
timestamp_condition = CombineDatetime(parent.posting_date, parent.posting_time) < CombineDatetime(
|
||||||
parent.posting_date, parent.posting_time
|
self.sle.posting_date, self.sle.posting_time
|
||||||
) <= CombineDatetime(self.sle.posting_date, self.sle.posting_time)
|
)
|
||||||
|
|
||||||
|
if self.sle.creation:
|
||||||
|
timestamp_condition |= (
|
||||||
|
CombineDatetime(parent.posting_date, parent.posting_time)
|
||||||
|
== CombineDatetime(self.sle.posting_date, self.sle.posting_time)
|
||||||
|
) & (parent.creation < self.sle.creation)
|
||||||
|
|
||||||
query = (
|
query = (
|
||||||
frappe.qb.from_(parent)
|
frappe.qb.from_(parent)
|
||||||
|
|||||||
@@ -200,7 +200,7 @@ def get_bin(item_code, warehouse):
|
|||||||
if not bin:
|
if not bin:
|
||||||
bin_obj = _create_bin(item_code, warehouse)
|
bin_obj = _create_bin(item_code, warehouse)
|
||||||
else:
|
else:
|
||||||
bin_obj = frappe.get_doc("Bin", bin, for_update=True)
|
bin_obj = frappe.get_doc("Bin", bin)
|
||||||
bin_obj.flags.ignore_permissions = True
|
bin_obj.flags.ignore_permissions = True
|
||||||
return bin_obj
|
return bin_obj
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ frappe.ui.form.on("Subcontracting Order", {
|
|||||||
setup: (frm) => {
|
setup: (frm) => {
|
||||||
frm.get_field("items").grid.cannot_add_rows = true;
|
frm.get_field("items").grid.cannot_add_rows = true;
|
||||||
frm.get_field("items").grid.only_sortable();
|
frm.get_field("items").grid.only_sortable();
|
||||||
|
frm.trigger("set_queries");
|
||||||
|
|
||||||
frm.set_indicator_formatter("item_code", (doc) => (doc.qty <= doc.received_qty ? "green" : "orange"));
|
frm.set_indicator_formatter("item_code", (doc) => (doc.qty <= doc.received_qty ? "green" : "orange"));
|
||||||
|
|
||||||
@@ -93,6 +94,17 @@ frappe.ui.form.on("Subcontracting Order", {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
set_queries: (frm) => {
|
||||||
|
frm.set_query("contact_person", erpnext.queries.contact_query);
|
||||||
|
frm.set_query("supplier_address", erpnext.queries.address_query);
|
||||||
|
|
||||||
|
frm.set_query("billing_address", erpnext.queries.company_address_query);
|
||||||
|
|
||||||
|
frm.set_query("shipping_address", () => {
|
||||||
|
return erpnext.queries.company_address_query(frm.doc);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
onload: (frm) => {
|
onload: (frm) => {
|
||||||
if (!frm.doc.transaction_date) {
|
if (!frm.doc.transaction_date) {
|
||||||
frm.set_value("transaction_date", frappe.datetime.get_today());
|
frm.set_value("transaction_date", frappe.datetime.get_today());
|
||||||
@@ -116,6 +128,8 @@ frappe.ui.form.on("Subcontracting Order", {
|
|||||||
},
|
},
|
||||||
|
|
||||||
refresh: function (frm) {
|
refresh: function (frm) {
|
||||||
|
frappe.dynamic_link = { doc: frm.doc, fieldname: "supplier", doctype: "Supplier" };
|
||||||
|
|
||||||
if (frm.doc.docstatus == 1 && frm.has_perm("submit")) {
|
if (frm.doc.docstatus == 1 && frm.has_perm("submit")) {
|
||||||
if (frm.doc.status == "Closed") {
|
if (frm.doc.status == "Closed") {
|
||||||
frm.add_custom_button(
|
frm.add_custom_button(
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ frappe.ui.form.on("Subcontracting Receipt", {
|
|||||||
},
|
},
|
||||||
|
|
||||||
refresh: (frm) => {
|
refresh: (frm) => {
|
||||||
|
frappe.dynamic_link = { doc: frm.doc, fieldname: "supplier", doctype: "Supplier" };
|
||||||
|
|
||||||
if (frm.doc.docstatus === 1) {
|
if (frm.doc.docstatus === 1) {
|
||||||
frm.add_custom_button(
|
frm.add_custom_button(
|
||||||
__("Stock Ledger"),
|
__("Stock Ledger"),
|
||||||
@@ -165,6 +167,15 @@ frappe.ui.form.on("Subcontracting Receipt", {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
frm.set_query("contact_person", erpnext.queries.contact_query);
|
||||||
|
frm.set_query("supplier_address", erpnext.queries.address_query);
|
||||||
|
|
||||||
|
frm.set_query("billing_address", erpnext.queries.company_address_query);
|
||||||
|
|
||||||
|
frm.set_query("shipping_address", () => {
|
||||||
|
return erpnext.queries.company_address_query(frm.doc);
|
||||||
|
});
|
||||||
|
|
||||||
frm.set_query("rejected_warehouse", () => {
|
frm.set_query("rejected_warehouse", () => {
|
||||||
return {
|
return {
|
||||||
filters: {
|
filters: {
|
||||||
|
|||||||
Reference in New Issue
Block a user